4. VxWorks网络协议栈结构剖析

VxWorks 的网络协议栈并不是“socket 直接调用网卡”的简单结构,而是在 socket API、协议层(TCP/IP)、MUX 层与网络驱动之间形成了一条分层、可挂接、可替换的调用链。理解这条链的关键,是分清“谁调用谁”和“数据往哪个方向走”这两件事。前者是控制路径,后者是数据路径,二者方向相反、职责不同。

一、四层结构的分工

从上层往下看,VxWorks 网络栈大致分为四层:

  • socket API 层:提供 socket()、bind()、connect()、send()、recv() 等标准 BSD 接口,是应用代码唯一直接接触的层。它不关心底层是以太网、串口还是共享内存。
  • 协议层:由 TCP、UDP、IP、ICMP、ARP 等协议模块组成。TCP 负责连接管理、确认重传、滑动窗口;IP 负责寻址与路由;ARP 负责 IP 到 MAC 的解析。
  • MUX 层:即网络多路复用层(Network Multiplexer),是协议层与驱动层之间的“中间件”。它向上为协议提供统一的发送入口,向下管理多个网络接口,并负责协议与驱动之间的绑定、解绑和接口切换。
  • 网络驱动层:即 END(Enhanced Network Driver)驱动,直接操作网卡寄存器、DMA 描述符和收发缓冲区,完成帧的实际收发。

这四层之间不是简单的函数调用堆叠,而是通过 MUX 提供的标准接口(如 muxBind、muxSend、muxDevStart)和回调函数(如 stackRcvRtn、stackTxRestartRtn)连接起来的。协议层通过 MUX 注册自己的接收回调,驱动层通过 MUX 把收到的帧交给协议层。

二、控制路径:从 socket 到驱动的调用关系

当应用调用 socket(AF_INET, SOCK_STREAM, 0) 时,控制流从 socket API 进入协议层,创建一个 TCP 控制块(TCB),并返回一个文件描述符。随后 bind() 把本地 IP 和端口写入 TCB,connect() 触发 TCP 三次握手,最终通过 IP 层查路由、ARP 解析下一跳 MAC,再经 MUX 找到对应网络接口,调用驱动的发送函数把 SYN 帧发出去。

这条控制路径的核心是“逐层向下传递参数”:socket 层传的是描述符和地址结构,协议层传的是协议控制块和报文,MUX 层传的是网络接口指针和 M_BLK 数据块,驱动层传的是 DMA 描述符和物理地址。每一层只关心自己需要的参数,不越界操作。

三、数据路径:从网卡到应用的反向流动

数据接收方向与控制方向相反。网卡收到帧后触发中断,驱动在中断服务程序或轮询任务中把帧从 DMA 缓冲区取出,封装成 M_BLK 链,然后调用 MUX 注册的接收回调 stackRcvRtn。MUX 根据帧类型(以太网类型字段)判断是 IP、ARP 还是其他协议,再分发给对应的协议层处理函数。

IP 层收到后检查目的地址,若是本机则继续向上交给 TCP 或 UDP;TCP 层根据四元组找到对应连接,把数据放入接收缓冲区,并唤醒阻塞在 recv() 上的应用任务。应用最终从 socket 缓冲区读到数据。整个过程是“驱动→MUX→协议→socket”的自下而上流动。

VxWorks 网络协议栈调用关系与数据流路径 控制路径(自上而下) 数据路径(自下而上) socket API 层 socket() / bind() / connect() / send() / recv() 协议层(TCP / UDP / IP / ARP / ICMP) TCP 连接管理、IP 路由、ARP 解析、校验和计算 MUX 层(网络多路复用) muxBind / muxSend / muxDevStart / stackRcvRtn 网络驱动层(END 驱动) 网卡寄存器 / DMA 描述符 / 收发缓冲区 / 中断处理 协议注册 protoRcvRtn protoSendRtn MUX 接口 muxBind muxSend 关键结构 TCB / PCB M_BLK 链 接口管理 netif 结构 END 对象 控制路径逐层向下传递参数;数据路径逐层向上递交 M_BLK,最终唤醒 recv() 阻塞任务

四、MUX 层的关键作用

MUX 层是理解整个栈的枢纽。它解决两个问题:一是“协议如何找到驱动”,二是“驱动如何找到协议”。前者通过 muxBind 完成——协议层调用 muxBind 把协议号、接收回调、发送回调注册到 MUX,MUX 再根据接口名或接口号绑定到具体 END 驱动。后者通过 stackRcvRtn 完成——驱动收到帧后调用 MUX 提供的统一接收入口,MUX 再根据帧类型分发给已注册的协议。

这种设计带来的直接好处是:更换网卡时,只需替换 END 驱动并重新 muxBind,协议层和 socket 层代码完全不用改。同样,增加一个新协议(如自定义 EtherType)也只需向 MUX 注册新的回调,不影响已有驱动。

五、一次 send 调用的完整数据流

以 TCP 发送为例,假设应用调用 send(sock, buf, 1024, 0),数据流路径如下:

  1. socket 层把用户缓冲区数据拷贝到内核 M_BLK 链,并调用 TCP 发送函数。
  2. TCP 层根据连接状态决定是否立即发送、是否等待窗口,给数据加上 TCP 头,计算校验和,然后交给 IP 层。
  3. IP 层查路由表确定下一跳和出接口,加上 IP 头,若下一跳 MAC 未知则先触发 ARP 请求,把数据包暂存队列。
  4. IP 层调用 muxSend,把 M_BLK 和出接口信息交给 MUX。
  5. MUX 根据接口找到对应 END 驱动的发送函数,调用 endSend。
  6. 驱动把 M_BLK 数据映射到 DMA 缓冲区,填写发送描述符,启动网卡发送。
  7. 发送完成后,驱动释放 M_BLK,并通过 stackTxRestartRtn 通知协议层可以继续发送。

接收方向则相反:网卡中断→驱动组装 M_BLK→stackRcvRtn→MUX 按类型分发→IP 层校验→TCP 层查找连接→放入接收队列→唤醒 recv()。整个过程中,M_BLK 是贯穿始终的数据载体,MUX 是贯穿始终的“分发器”。

理解要点:控制路径回答“怎么建立连接、怎么找到对方”,数据路径回答“数据怎么搬进去、怎么搬出来”。MUX 层不产生数据,也不修改数据,它只做“按接口和协议类型正确转发”。把 MUX 理解成一个带注册表的交换机,比理解成一个协议层更准确。

六、常见误区与排查线索

实际调试中,很多网络不通的问题出在 MUX 绑定环节。例如:muxBind 返回 NULL 说明协议号或接口名不匹配;muxSend 返回 ERROR 说明接口未启动或驱动发送队列满;stackRcvRtn 未被调用说明驱动中断未使能或 DMA 描述符未正确初始化。排查时可以先确认 ifShow 或 muxShow 输出的接口状态,再检查协议注册回调是否与帧类型一致。

另一个常见问题是缓冲区不足。VxWorks 的 M_BLK 来自网络内存池(netPool),如果池太小,高流量下会出现丢包,表现为 send() 阻塞或 recv() 收不到数据。此时应检查 netPoolShow 的剩余块数,并适当增大 END_MEM_SIZE 或调整 M_BLK 数量。

掌握这四层结构和两条路径的方向,就掌握了 VxWorks 网络栈的骨架。后续无论是移植新网卡、增加自定义协议,还是定位丢包和延迟问题,都可以沿着这条链逐层排查。