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() 到网卡发送的完整数据流,包含关键函数调用和数据结构传递关系。
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 是整个发包路径的核心载体。它的状态变化反映了数据流的进展。
- 分配: 在
tcp_sendmsg()或udp_sendmsg()中通过alloc_skb()分配,并从用户态拷贝数据。 - 封装: 每经过一层,通过
skb_push()在头部预留空间并填充协议头。 - 排队: 进入 qdisc 队列或 TCP 重传队列,等待调度。
- 发送: 驱动通过
dev_hard_start_xmit()获取sk_buff,进行 DMA 映射。 - 释放: 硬件发送完成后,驱动调用
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 锁: 用于路由缓存、邻居表等读多写少的场景。
4.7 本章小结
内核网络协议栈发包路径是一条从用户态到硬件的多层流水线。每一层都承担着封装、排队、调度或映射的职责。理解这条路径上的关键函数、数据结构、队列和锁,是后续章节进行 sk_buff 队列调度优化和网卡驱动协同优化的基础。
- 核心路径: socket → 传输层 → 网络层 → 邻居 → qdisc → 驱动。
- 核心数据结构:
sk_buff贯穿始终,每层通过指针操作添加头部。 - 主要瓶颈: qdisc 锁竞争、DMA 映射开销、缓存行 bouncing。
- 优化方向: 多队列分流、锁粒度降低、零拷贝、批量发送(TSO/GSO)。