Linux sk_buff 深度解析(四):从 sendmsg 到 TX completion
写在前面
sk_buff 是 Linux 网络栈最核心、也最容易被误解的数据结构之一。它并不是“装着一个完整网络包的结构体”,而是贯穿驱动、协议栈、队列、Socket、Netfilter、tc、BPF 与硬件 offload 的控制对象和所有权载体。
打通 UDP/TCP sendmsg、IPv4 output、qdisc、ndo_start_xmit、stmmac DMA 映射与 TX clean,重点解释队列和驱动的 skb 所有权契约。
本文以本地 Linux 5.10.209 源码为主线;涉及演进时对照 Linux 6.1.99,涉及网卡驱动时结合 Firefly RK3588 SDK Linux 5.10.198。本文是源码分析,不把尚未执行的 QEMU、tracepoint 或开发板实验写成已验证结论。
阅读前先建立四个问题
阅读任何 skb 代码路径,都建议反复追问:
- 当前
skb->data指向哪一层协议头,linear 与 non-linear 数据分别在哪里? - 哪些字段刚被修改,下一层为什么依赖这些字段?
- 当前谁持有 skb shell、head、frag page 以及 socket/dst/conntrack 等外部引用?
- 成功、失败、重试、redirect 和丢弃分支分别由谁消费或释放 skb?
本篇核心结论
- 协议层、qdisc、驱动和硬件 descriptor 依次成为 skb 的持有者。
dev_queue_xmit()不等于立即发包,绝大多数 skb 会先经历 queue selection 与 qdisc。NETDEV_TX_OK表示驱动已经消费 skb;NETDEV_TX_BUSY表示所有权仍归上层。- TX completion 负责 DMA unmap、descriptor 回收和最终 skb 消费,不能与 TCP ACK 生命周期混淆。
1. 从 sendmsg 到网卡完成的完整 TX 生命周期
结论摘要
发送方向由 socket/传输层创建并拥有 skb,经 IP 构造 header、Netfilter/routing、邻居与二层输出进入 dev_queue_xmit()。qdisc 排队后把 skb 交给驱动 ndo_start_xmit。驱动成功接收后拥有 skb,完成 DMA mapping 与 descriptor 发布;TX completion 才 unmap、完成 BQL 记账并 napi_consume_skb()。NETDEV_TX_BUSY 是关键例外:驱动不得消费 skb,调用者仍需保留/重试。
一、UDP 主链
1 | sock_sendmsg() |
关键位置:
udp_sendmsg():kernel-5.10/net/ipv4/udp.c:1040;__ip_append_data():kernel-5.10/net/ipv4/ip_output.c:967;ip_append_data():kernel-5.10/net/ipv4/ip_output.c:1314;ip_make_skb():kernel-5.10/net/ipv4/ip_output.c:1633;ip_local_out():kernel-5.10/net/ipv4/ip_output.c:120;ip_output():kernel-5.10/net/ipv4/ip_output.c:430。
二、UDP 构造 skb
UDP 需要决定:
- connected/unconnected 目的地址;
- route 与 MTU;
- cork/MSG_MORE;
- checksum;
- GSO/UDP segmentation;
- 是否从用户 iterator 复制或 zerocopy。
ip_append_data() 可把数据追加到 socket write queue,形成一个或多个 skb;ip_make_skb() 使用临时 queue 一次构造并返回 skb。构造过程中预留 L2/L3/L4 headroom,payload 可在线性区或 frags 中。
socket 发送内存通过 skb_set_owner_w()、sk_wmem_alloc 与 sock_wfree() 记账。skb 进入不再需要 socket owner 的路径时可能 orphan,但 TCP 重传队列会长期保留 owner/计量。
三、TCP 与 UDP 的差异
TCP 不是每次 sendmsg 都立即生成并发送一个独立 datagram:
- 数据进入 TCP write queue;
TCP_SKB_CB保存 seq/end_seq/flags;- skb 可能被合并、分段、clone;
- 原 skb/clone 可能留在 retransmit queue;
- ACK 后才释放发送内存;
- 实际发送副本与重传状态必须分开管理引用。
因此 TCP skb 生命周期可能跨多个 RTT,远长于 UDP 单次输出。
四、IP output
IP 层完成:
- 设置 network header;
- 填写 IPv4 header、total length、ID、protocol、checksum;
- 附着 dst;
- 运行 LOCAL_OUT/POST_ROUTING Netfilter;
- 检查 MTU、分片或 GSO;
- 通过 neighbour/ARP 添加二层 header。
headroom 不足或 header 共享时,需要 COW/expand。任何扩展后旧 header 指针必须重新获取。
五、dev_queue_xmit 与 qdisc
入口:
__dev_queue_xmit():kernel-5.10/net/core/dev.c:4108;dev_queue_xmit():kernel-5.10/net/core/dev.c:4219。
主要工作:
- 确定输出设备和 TX queue;
- 运行 egress tc;
- 处理 VLAN/offload feature;
- 选择 qdisc;
- enqueue 或 noqueue 直接发送;
- 调度 dequeue;
dev_hard_start_xmit()调用ndo_start_xmit。
进入 qdisc 后 skb owner 是 qdisc;drop 时 qdisc 负责释放。dequeue 交给驱动后 owner 转移取决于驱动返回值。
六、驱动所有权合同
ndo_start_xmit() 典型返回:
NETDEV_TX_OK:驱动已经消费/接管 skb,调用者不能再访问;NETDEV_TX_BUSY:驱动没有消费 skb,网络栈仍持有并重试。
驱动在返回 BUSY 前不能:
- 释放 skb;
- 部分不可逆地提交 descriptor;
- 丢失 DMA mapping 回滚信息。
现代驱动应尽量提前 stop queue,避免频繁 BUSY。
七、stmmac_xmit
入口:firefly_rk3588_SDK/kernel/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c:3468。
主要步骤:
- 选择 TX queue/ring;
- 检查可用 descriptor;不足则 stop queue/BUSY;
- 处理 GSO/TSO、VLAN、checksum offload;
- DMA map skb linear head;
- 遍历
frags[],逐片 DMA map; - 填写 descriptor 长度、地址和 offload flags;
- 在软件
tx_skbuff[]/相关数组保存 skb 与 mapping; - 设置 OWN,使用屏障保证 descriptor 内容先可见;
- 更新
cur_tx、BQL sent bytes; - 通知 DMA tail pointer。
八、为什么 skb 常挂在最后一个 descriptor
一个 skb 可能占多个 descriptor。completion 按 descriptor 顺序清理;只有最后一个 descriptor 完成,才能确定整个 skb 的所有 DMA segment 都不再被硬件访问。
因此常见做法:
- 每个 descriptor 保存 DMA mapping 信息,便于逐个 unmap;
- 只在 packet 的最后一个 descriptor 保存 skb 指针;
- clean 到最后一个时 consume skb。
若过早释放 skb,frag page 可能在 DMA 仍读取时被复用,造成数据损坏。
九、stmmac_tx_clean
入口:firefly_rk3588_SDK/kernel/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c:2087。
1 | 检查 descriptor OWN |
TX completion 是正常消费,语义上优先 napi_consume_skb()/consume_skb(),不是 drop 型 kfree_skb()。
十、错误与回滚
- route/MTU/Netfilter 失败:IP/socket 层释放或返回错误;
- qdisc enqueue drop:qdisc 释放;
- driver ring 不足:BUSY,不消费;
- DMA map 中途失败:驱动必须 unmap 已成功部分并释放/返回正确状态;
- hardware TX error completion:仍需 unmap、记账并释放 skb;
- timeout/reset:驱动 reset 路径必须清理所有 outstanding skb/mapping。
十一、所有权时间线
1 | 用户 buffer |
十二、常见误区
dev_queue_xmit()返回后不能默认 skb 仍有效。NETDEV_TX_OK不等于包已经上线路,只表示驱动接管。- completion 才是普通异步驱动释放 skb 的安全点。
- skb linear head 和 frags 需要不同 DMA mapping/unmap API。
- TCP ACK 释放的通常是协议发送队列引用,与网卡 completion 的发送副本生命周期不能混淆。
- BQL 统计字节完成不替代 skb 释放,两者必须同时正确。
字段与所有权统一核对表
| 对象或字段 | 主要修改者/使用者 | 所有权与释放要点 |
|---|---|---|
sk/truesize |
sendmsg 与协议队列 | socket write memory 持有直到相应生命周期结束 |
queue_mapping |
queue select/qdisc | qdisc 或设备队列持有 skb |
| DMA mapping/descriptor | stmmac xmit/clean | 驱动提交后由 TX completion 归还 |
最小调用链
1 | sendmsg() |
2. qdisc 排队、调度与驱动所有权
结论摘要
qdisc 位于协议栈和驱动之间,持有 skb 并实施分类、整形、调度、拥塞和丢包。__dev_queue_xmit() 选择 TX queue/qdisc,enqueue 成功后 qdisc 成为 owner;__qdisc_run() dequeue,sch_direct_xmit() 交给驱动。驱动返回 BUSY 时 skb 必须 requeue,返回 OK 时驱动接管。BQL 控制驱动队列在途字节,与 qdisc 队列形成上下游反馈。
一、主路径
1 | __dev_queue_xmit() |
关键位置:
__dev_queue_xmit():kernel-5.10/net/core/dev.c:4108;dev_hard_start_xmit():kernel-5.10/net/core/dev.c:3611;sch_direct_xmit():kernel-5.10/net/sched/sch_generic.c:308;__qdisc_run():kernel-5.10/net/sched/sch_generic.c:404。
二、qdisc 的职责
- FIFO/优先级/公平队列;
- shaping/pacing;
- classful 分类;
- AQM,如丢包/ECN;
- 多队列设备的 queue qdisc;
- 统计 backlog、drops、overlimits。
skb 中参与决策的字段包括:
priority;mark;queue_mapping;protocol;hash;tc_index;- qdisc cb 中的 packet length/classid 等。
三、enqueue 所有权
qdisc enqueue 的返回语义决定 skb 是否:
- 已入队;
- 被丢弃并释放;
- 被替换/重排;
- 需要调用者释放。
通用调用层根据 NET_XMIT_* 处理统计和返回。不能看到 enqueue 失败就无条件再 kfree_skb(),否则可能 double free。
入队后 qdisc 长期持有 skb,协议层不能再修改。qdisc dequeue 前可根据时间、token、flow、公平策略决定顺序。
四、运行与锁
qdisc_run() 使用运行状态位/锁确保同一 qdisc 不被并发重复执行。__qdisc_run() 在 quota 内循环 dequeue,避免一次占用 CPU 无上限。
dequeue 返回 skb 后,qdisc backlog accounting 相应减少;若后续驱动 BUSY,需要 requeue 并恢复相应状态。
五、sch_direct_xmit
sch_direct_xmit():
- 从 qdisc 取出的 skb 交给
dev_hard_start_xmit(); - 驱动 OK:skb 被驱动消费;
- BUSY:skb 未消费,重新入队;
- 其它错误/部分 segment:按返回和剩余 skb 链处理;
- 更新 queue state 和统计。
源码在 kernel-5.10/net/sched/sch_generic.c:308-401,其中 351 附近明确说明 BUSY requeue。
六、noqueue
loopback、某些虚拟设备或明确 noqueue 设备可绕过普通 qdisc 排队直接调用驱动。noqueue 不代表无任何同步或不会 drop,只是没有标准软件排队层。
错误给需要排队的物理设备配置 noqueue,会把背压直接暴露到驱动 BUSY 路径。
七、queue stop/wake
驱动 ring 接近满时:
1 | netif_tx_stop_queue() |
completion 释放足够 descriptor 后:
1 | netif_tx_wake_queue() |
stop 必须在真正没有空间前与 producer 竞态正确同步,否则可能返回 BUSY 或 ring overflow。
八、BQL
Byte Queue Limits 按字节控制驱动硬件队列在途数据:
- 提交时
netdev_tx_sent_queue(); - completion 时
netdev_tx_completed_queue(); - 动态调整允许的在途字节。
BQL 目标是减少网卡 ring 过深造成的 bufferbloat,同时保持设备不饿。它不替代 qdisc:
1 | qdisc:选择哪些 skb 何时下发 |
九、watchdog 与 timeout
若 TX queue 长时间 stop 且没有 completion,watchdog 触发 ndo_tx_timeout。reset 路径必须清理:
- outstanding descriptor;
- DMA mappings;
- 保存的 skb;
- BQL/accounting;
- queue state。
否则会泄漏 skb/page 或重复 completion。
十、字段与所有权
| 阶段 | owner | 关键字段 |
|---|---|---|
__dev_queue_xmit 前 |
IP/stack | dev、dst、priority、mark |
| egress tc | tc/stack | mark、priority、redirect |
| qdisc enqueue 后 | qdisc | queue_mapping、qdisc cb、hash |
| dequeue | qdisc/发送临界区 | packet len、class/accounting |
| ndo OK | driver | DMA mappings、descriptor |
| ndo BUSY | qdisc | skb 必须 intact/requeue |
| completion | driver | unmap/BQL/consume |
十一、常见误区
- qdisc 不只是一条 FIFO;它可以分类、整形和 AQM。
- 驱动返回 OK 只表示接管,不表示已发送完成。
- BUSY 时驱动不能释放 skb。
- qdisc drop 的 skb 可能已被 enqueue API 消费,调用者需遵守返回合同。
- BQL 不是 qdisc,也不是按 packet 数量限制。
- queue mapping 选择错误会把流量送到错误 ring/qdisc,增加乱序和锁竞争。
字段与所有权统一核对表
| 对象或字段 | 主要修改者/使用者 | 所有权与释放要点 |
|---|---|---|
queue_mapping/priority |
TX select/tc | 选择 netdev queue 与 class |
| qdisc node/cb | enqueue/dequeue | 排队期间 qdisc 持有 skb |
| driver return code | ndo_start_xmit |
OK 消费;BUSY 时所有权仍归上层 |
最小调用链
1 | dev_queue_xmit() |
从局部机制回到完整路径
前面的局部机制只有放回完整生命周期才不容易误判:数据布局决定 helper 能否直接访问;引用计数决定能否原地修改;队列和驱动返回值决定谁仍然拥有 skb。遇到异常路径时,应按“外部引用 → 数据区 → shell”的逆序检查回收。
源码阅读路线
建议不要从 struct sk_buff 声明开始逐字段背诵,而应使用下面的阅读顺序:
- 先在入口函数确认 skb 是新分配、clone、队列取出还是从驱动 buffer 构造;
- 沿成功主线记录
data/len/data_len、header offset、checksum、hash、queue mapping 等字段变化; - 再回到每个失败分支,确认 skb、page、DMA、socket 和记账引用是否回滚;
- 最后对照 5.10、6.1 或 BSP 的同名 symbol,区分语义变化、性能优化和 vendor backport。
本篇总结
- 协议层、qdisc、驱动和硬件 descriptor 依次成为 skb 的持有者。
dev_queue_xmit()不等于立即发包,绝大多数 skb 会先经历 queue selection 与 qdisc。NETDEV_TX_OK表示驱动已经消费 skb;NETDEV_TX_BUSY表示所有权仍归上层。- TX completion 负责 DMA unmap、descriptor 回收和最终 skb 消费,不能与 TCP ACK 生命周期混淆。
sk_buff 的难点从来不只是字段多,而是同一个对象不断改变数据视图、元数据状态和持有者。只要把“布局、字段、所有权、释放”四条线同时画出来,绝大多数网络栈源码都会从零散函数变成可推导的生命周期。
系列目录
- Linux sk_buff 深度解析(一):数据结构、四指针与核心字段
- Linux sk_buff 深度解析(二):非线性数据、引用计数与生命周期
- Linux sk_buff 深度解析(三):从网卡 DMA 到 Socket 的 RX 路径
- Linux sk_buff 深度解析(四):从 sendmsg 到 TX completion(本文)
- Linux sk_buff 深度解析(五):GRO、GSO、Checksum 与多核流调度
- Linux sk_buff 深度解析(六):Netfilter、tc、eBPF 与 XDP
- Linux sk_buff 深度解析(七):VLAN、隧道、分片与重组
- Linux sk_buff 深度解析(八):Socket 内存、零拷贝、丢包与版本演进
