写在前面

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 或开发板实验写成已验证结论。

Linux sk_buff 深度解析(四):从 sendmsg 到 TX completion

阅读前先建立四个问题

阅读任何 skb 代码路径,都建议反复追问:

  1. 当前 skb->data 指向哪一层协议头,linear 与 non-linear 数据分别在哪里?
  2. 哪些字段刚被修改,下一层为什么依赖这些字段?
  3. 当前谁持有 skb shell、head、frag page 以及 socket/dst/conntrack 等外部引用?
  4. 成功、失败、重试、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
2
3
4
5
6
7
8
9
10
11
sock_sendmsg()
→ inet_sendmsg()
→ udp_sendmsg()
→ ip_make_skb() 或 ip_append_data()
→ udp_send_skb()
→ ip_send_skb()
→ ip_local_out()
→ ip_output()
→ ip_finish_output()
→ neighbour output
→ dev_queue_xmit()

关键位置:

  • 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_allocsock_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

主要工作:

  1. 确定输出设备和 TX queue;
  2. 运行 egress tc;
  3. 处理 VLAN/offload feature;
  4. 选择 qdisc;
  5. enqueue 或 noqueue 直接发送;
  6. 调度 dequeue;
  7. 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

主要步骤:

  1. 选择 TX queue/ring;
  2. 检查可用 descriptor;不足则 stop queue/BUSY;
  3. 处理 GSO/TSO、VLAN、checksum offload;
  4. DMA map skb linear head;
  5. 遍历 frags[],逐片 DMA map;
  6. 填写 descriptor 长度、地址和 offload flags;
  7. 在软件 tx_skbuff[]/相关数组保存 skb 与 mapping;
  8. 设置 OWN,使用屏障保证 descriptor 内容先可见;
  9. 更新 cur_tx、BQL sent bytes;
  10. 通知 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
2
3
4
5
6
7
8
检查 descriptor OWN
→ 读取 TX status
→ dma_unmap_single/page
→ 到 packet last descriptor 时取回 skb
→ napi_consume_skb()
→ netdev_tx_completed_queue() / BQL
→ 推进 dirty_tx
→ 队列有空间时 wake

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
2
3
4
5
6
7
8
9
10
用户 buffer
→ socket/transport owns skb
→ IP/Netfilter/routing owns skb
→ qdisc owns skb
→ ndo_start_xmit:
OK → driver owns skb
BUSY → qdisc/stack still owns skb
→ DMA owns packet bytes(driver 保存软件引用)
→ TX completion
→ driver unmap + consume skb

十二、常见误区

  1. dev_queue_xmit() 返回后不能默认 skb 仍有效。
  2. NETDEV_TX_OK 不等于包已经上线路,只表示驱动接管。
  3. completion 才是普通异步驱动释放 skb 的安全点。
  4. skb linear head 和 frags 需要不同 DMA mapping/unmap API。
  5. TCP ACK 释放的通常是协议发送队列引用,与网卡 completion 的发送副本生命周期不能混淆。
  6. BQL 统计字节完成不替代 skb 释放,两者必须同时正确。

字段与所有权统一核对表

对象或字段 主要修改者/使用者 所有权与释放要点
sk/truesize sendmsg 与协议队列 socket write memory 持有直到相应生命周期结束
queue_mapping queue select/qdisc qdisc 或设备队列持有 skb
DMA mapping/descriptor stmmac xmit/clean 驱动提交后由 TX completion 归还

最小调用链

1
2
3
4
5
6
sendmsg()
→ UDP/TCP 构造和排队 skb
→ IP output
→ dev_queue_xmit/qdisc
→ stmmac_xmit/DMA
→ TX completion/clean/free

2. qdisc 排队、调度与驱动所有权

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
2
3
4
5
6
7
8
9
10
11
12
__dev_queue_xmit()
→ egress tc
→ netdev_pick_tx()/queue_mapping
→ qdisc lookup
→ __dev_xmit_skb()
├─ enqueue
└─ qdisc_run()
→ __qdisc_run()
→ dequeue_skb()
→ sch_direct_xmit()
→ dev_hard_start_xmit()
→ ndo_start_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()

  1. 从 qdisc 取出的 skb 交给 dev_hard_start_xmit()
  2. 驱动 OK:skb 被驱动消费;
  3. BUSY:skb 未消费,重新入队;
  4. 其它错误/部分 segment:按返回和剩余 skb 链处理;
  5. 更新 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
2
netif_tx_stop_queue()
→ qdisc 不继续向该 TX queue 下发

completion 释放足够 descriptor 后:

1
2
netif_tx_wake_queue()
→ qdisc 可重新运行

stop 必须在真正没有空间前与 producer 竞态正确同步,否则可能返回 BUSY 或 ring overflow。

八、BQL

Byte Queue Limits 按字节控制驱动硬件队列在途数据:

  • 提交时 netdev_tx_sent_queue()
  • completion 时 netdev_tx_completed_queue()
  • 动态调整允许的在途字节。

BQL 目标是减少网卡 ring 过深造成的 bufferbloat,同时保持设备不饿。它不替代 qdisc:

1
2
qdisc:选择哪些 skb 何时下发
BQL:限制驱动/硬件当前在途多少字节

九、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

十一、常见误区

  1. qdisc 不只是一条 FIFO;它可以分类、整形和 AQM。
  2. 驱动返回 OK 只表示接管,不表示已发送完成。
  3. BUSY 时驱动不能释放 skb。
  4. qdisc drop 的 skb 可能已被 enqueue API 消费,调用者需遵守返回合同。
  5. BQL 不是 qdisc,也不是按 packet 数量限制。
  6. 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
2
3
4
5
6
dev_queue_xmit()
→ 选择 netdev queue/qdisc
→ enqueue
→ schedule/dequeue
→ ndo_start_xmit()
→ OK 消费或 BUSY 退还所有权

从局部机制回到完整路径

完整路径关系

前面的局部机制只有放回完整生命周期才不容易误判:数据布局决定 helper 能否直接访问;引用计数决定能否原地修改;队列和驱动返回值决定谁仍然拥有 skb。遇到异常路径时,应按“外部引用 → 数据区 → shell”的逆序检查回收。

源码阅读路线

建议不要从 struct sk_buff 声明开始逐字段背诵,而应使用下面的阅读顺序:

  1. 先在入口函数确认 skb 是新分配、clone、队列取出还是从驱动 buffer 构造;
  2. 沿成功主线记录 data/len/data_len、header offset、checksum、hash、queue mapping 等字段变化;
  3. 再回到每个失败分支,确认 skb、page、DMA、socket 和记账引用是否回滚;
  4. 最后对照 5.10、6.1 或 BSP 的同名 symbol,区分语义变化、性能优化和 vendor backport。

本篇总结

  1. 协议层、qdisc、驱动和硬件 descriptor 依次成为 skb 的持有者。
  2. dev_queue_xmit() 不等于立即发包,绝大多数 skb 会先经历 queue selection 与 qdisc。
  3. NETDEV_TX_OK 表示驱动已经消费 skb;NETDEV_TX_BUSY 表示所有权仍归上层。
  4. TX completion 负责 DMA unmap、descriptor 回收和最终 skb 消费,不能与 TCP ACK 生命周期混淆。

sk_buff 的难点从来不只是字段多,而是同一个对象不断改变数据视图、元数据状态和持有者。只要把“布局、字段、所有权、释放”四条线同时画出来,绝大多数网络栈源码都会从零散函数变成可推导的生命周期。

系列目录

  1. Linux sk_buff 深度解析(一):数据结构、四指针与核心字段
  2. Linux sk_buff 深度解析(二):非线性数据、引用计数与生命周期
  3. Linux sk_buff 深度解析(三):从网卡 DMA 到 Socket 的 RX 路径
  4. Linux sk_buff 深度解析(四):从 sendmsg 到 TX completion(本文)
  5. Linux sk_buff 深度解析(五):GRO、GSO、Checksum 与多核流调度
  6. Linux sk_buff 深度解析(六):Netfilter、tc、eBPF 与 XDP
  7. Linux sk_buff 深度解析(七):VLAN、隧道、分片与重组
  8. Linux sk_buff 深度解析(八):Socket 内存、零拷贝、丢包与版本演进