4. 内核网络协议栈发包路径总览:从 socket 到网卡驱动的完整数据流

本章从全局视角梳理 Linux 内核网络协议栈的 发包(TX)路径。理解这条路径是后续优化 sk_buff 队列调度与网卡驱动协同的基础。数据从用户态应用写入 socket,经过传输层、网络层、邻居子系统、流量控制层,最终由网卡驱动发送到物理介质。

4.1 发包路径核心阶段

整个发包流程可划分为以下 6 个关键阶段,每个阶段都有对应的内核函数和数据结构参与。

阶段 入口函数 核心数据结构 主要职责
1. Socket 层 sendmsg() / sendto() struct socket, struct sock 用户态数据拷贝到内核,构造 sk_buff
2. 传输层 tcp_sendmsg() / udp_sendmsg() struct sk_buff, struct tcp_sock 分段、校验和、添加传输层头部
3. 网络层 ip_local_out() / ip_output() struct iphdr, struct rtable 路由查找、分片、添加 IP 头部
4. 邻居子系统 neigh_output() / dev_queue_xmit() struct neighbour, struct hh_cache ARP/ND 解析,填充链路层头部
5. 流量控制层 __dev_queue_xmit() struct Qdisc, struct netdev_queue 排队、整形、调度(qdisc)
6. 网卡驱动层 ndo_start_xmit() struct net_device, struct sk_buff DMA 映射、触发硬件发送
关键认知: 数据流是“逐层封装”的过程。每层在 sk_buff 的头部添加自己的协议头,最终形成一个完整的帧。优化点往往集中在层与层之间的队列传递和锁竞争上。

4.2 发包路径全景流程图

下图展示了从用户态 send() 到网卡发送的完整数据流,包含关键函数调用和数据结构传递关系。

内核网络协议栈发包路径总览 内核网络协议栈发包路径总览 用户态应用 send() / sendmsg() Socket 层:sys_sendmsg() 传输层:tcp_sendmsg() / udp_sendmsg() 网络层:ip_local_out() / ip_output() 邻居子系统:neigh_output() 流量控制层:__dev_queue_xmit() 网卡驱动:ndo_start_xmit() 物理介质(网线/无线) 关键数据结构传递 • struct sk_buff:核心数据包 • struct sock:套接字状态 • struct dst_entry:路由缓存 • struct Qdisc:排队规则 • struct net_device:网卡设备 • struct skb_shared_info:分片

4.3 关键路径函数调用链

以 TCP 为例,实际的函数调用链如下(省略了部分锁和错误处理):

send() / sendto()
  └─ sys_sendmsg()                    // 系统调用入口
       └─ tcp_sendmsg()               // TCP 协议发送
            └─ tcp_push()             // 推送数据到发送队列
                 └─ __tcp_transmit_skb()  // 构建 TCP 头部
                      └─ ip_local_out()   // 进入网络层
                           └─ __ip_local_out()  // 添加 IP 头部
                                └─ ip_output()  // 路由输出
                                     └─ ip_finish_output()  // 完成输出
                                          └─ neigh_output()  // 邻居输出
                                               └─ dev_queue_xmit()  // 进入流量控制
                                                    └─ __dev_queue_xmit()  // 核心排队函数
                                                         └─ sch_direct_xmit()  // 直接发送
                                                              └─ dev_hard_start_xmit()  // 驱动入口
                                                                   └─ ndo_start_xmit()  // 网卡驱动发送
优化关注点: 这条调用链中,__dev_queue_xmit() 和 ndo_start_xmit() 之间的交互是性能瓶颈的高发区。qdisc 锁、TX 队列锁、DMA 映射开销都集中在此。

4.4 各层队列与缓冲区

数据在每层都可能经历排队或缓冲。理解这些队列的位置和特性,是后续调度优化的前提。

层级 队列/缓冲区 容量控制 典型问题
Socket 层 发送缓冲区 (sk_wmem_queued) tcp_wmem / sk_sndbuf 缓冲区膨胀、内存压力
传输层 TCP 发送队列 (sk_write_queue) 拥塞窗口 (cwnd) HoL 阻塞、重传队列管理
网络层 IP 分片队列 (ip_frag_queue) ipfrag_high_thresh 分片重组开销、内存碎片
流量控制层 qdisc 队列 (struct Qdisc) txqueuelen / qdisc 参数 队列溢出、锁竞争、延迟抖动
网卡驱动层 TX 描述符环 (ring buffer) 硬件队列深度 (txqueuesize) DMA 映射失败、ring full 丢包

4.5 数据流中的 sk_buff 生命周期

sk_buff 是整个发包路径的核心载体。它的状态变化反映了数据流的进展。

  1. 分配: 在 tcp_sendmsg() 或 udp_sendmsg() 中通过 alloc_skb() 分配,并从用户态拷贝数据。
  2. 封装: 每经过一层,通过 skb_push() 在头部预留空间并填充协议头。
  3. 排队: 进入 qdisc 队列或 TCP 重传队列,等待调度。
  4. 发送: 驱动通过 dev_hard_start_xmit() 获取 sk_buff,进行 DMA 映射。
  5. 释放: 硬件发送完成后,驱动调用 dev_kfree_skb_any() 释放 sk_buff。
常见误区: 很多人认为 sk_buff 在 ndo_start_xmit() 返回后立即释放。实际上,对于支持 NETIF_F_LLTX 或使用 TSQ(TCP Small Queues)的驱动,sk_buff 的生命周期会延长到硬件完成中断之后。理解这一点对避免 UAF(Use-After-Free)和正确实现 TX 完成回调至关重要。

4.6 发包路径的锁与同步

多核环境下,发包路径涉及多个锁,它们是性能优化的主要战场。

  • Socket 锁 (lock_sock): 保护单个 socket 的发送缓冲区,粒度较粗。
  • qdisc 锁 (qdisc_run): 保护排队规则内部的数据结构,高并发下竞争激烈。
  • 设备 TX 锁 (netif_tx_lock): 保护网卡的发送队列,现已逐步被 MQ(多队列)和 XPS 取代。
  • RCU 锁: 用于路由缓存、邻居表等读多写少的场景。
发包路径锁竞争示意图 发包路径锁竞争示意图 CPU 0 (App A) CPU 1 (App B) CPU 2 (App C) qdisc 锁 (竞争热点) 网卡 TX 队列 锁竞争影响 • 多个 CPU 同时发送到同一 qdisc • 自旋锁导致 CPU 空转 • 缓存行 bouncing • 解决方案:多队列 (MQ) + XPS • 或使用 lockless qdisc (如 fq_codel)

4.7 本章小结

内核网络协议栈发包路径是一条从用户态到硬件的多层流水线。每一层都承担着封装、排队、调度或映射的职责。理解这条路径上的关键函数、数据结构、队列和锁,是后续章节进行 sk_buff 队列调度优化和网卡驱动协同优化的基础。

  • 核心路径: socket → 传输层 → 网络层 → 邻居 → qdisc → 驱动。
  • 核心数据结构: sk_buff 贯穿始终,每层通过指针操作添加头部。
  • 主要瓶颈: qdisc 锁竞争、DMA 映射开销、缓存行 bouncing。
  • 优化方向: 多队列分流、锁粒度降低、零拷贝、批量发送(TSO/GSO)。