3. Linux 内核网络协议栈:数据包接收与发送全流程
本章深入剖析 Linux 内核网络协议栈的核心数据路径,包括数据包的接收(NAPI 与软中断)、发送流程,以及关键数据结构 sk_buff 的内存管理机制。理解这些底层原理是进行容器网络性能调优与 eBPF 观测的基础。
3.1 数据包接收流程:从网卡到 socket
数据包接收是网络栈中最复杂、性能最敏感的路径。现代 Linux 使用 NAPI(New API) 机制,结合硬中断与软中断,实现高效收包。
3.1.1 接收路径核心步骤
- DMA 写入:网卡通过 DMA 将数据包直接写入主存中的 Ring Buffer(环形缓冲区),无需 CPU 干预。
- 硬中断触发:网卡产生硬件中断,通知 CPU 有新数据到达。
- NAPI 调度:中断处理程序关闭该网卡的中断,并将设备的 NAPI 结构体添加到软中断的 poll 列表中。
- 软中断处理(ksoftirqd):CPU 在
do_softirq()中执行网络 RX 软中断(NET_RX_SOFTIRQ),调用 NAPI 的 poll 方法。 - 协议栈处理:从 Ring Buffer 取出数据,构建
sk_buff,依次经过链路层(eth_type)、网络层(ip_rcv)、传输层(tcp_v4_rcv)。 - socket 交付:数据放入对应 socket 的接收队列,唤醒等待的用户态进程。
性能关键点:NAPI 的核心优势在于 中断收敛。在高吞吐场景下,网卡中断被关闭,CPU 通过轮询(polling)批量收包,避免频繁中断导致的上下文切换开销。
观测提示:使用
cat /proc/net/softnet_stat 可以查看每个 CPU 的软中断统计,包括处理包数、丢包数(由于 backlog 队列满)和挤压次数。
3.1.2 NAPI 与软中断交互流程
下图展示了从硬件中断到软中断,再到协议栈的完整调用链。
3.1.3 接收路径关键数据结构
| 数据结构 | 作用 | 所在位置 |
|---|---|---|
| Ring Buffer | 网卡与内核共享的 DMA 环形队列,存储数据包描述符 | 网卡驱动 |
| napi_struct | NAPI 实例,包含 poll 函数指针、设备状态、权重 | 驱动 / 内核 |
| softnet_data | 每个 CPU 的软中断数据,包含 backlog、处理统计 | 内核 /proc/net/softnet_stat |
| sk_buff | 数据包在内核中的核心表示,包含各层协议头指针 | 内核网络栈 |
3.2 数据包发送流程:从 socket 到网卡
发送路径相对接收路径更直接,但同样涉及复杂的排队、GSO(Generic Segmentation Offload)和驱动传输。
3.2.1 发送路径核心步骤
- 系统调用:用户态调用
send()/sendmsg(),进入内核 socket 层。 - 传输层处理:TCP 分段(或 GSO 延迟分段)、添加 TCP 头、计算校验和。
- 网络层处理:路由查找、IP 分片(如果需要)、添加 IP 头、Netfilter 钩子。
- 邻居子系统:通过 ARP/ND 解析下一跳 MAC 地址,将数据帧放入 Qdisc(队列规则)。
- Qdisc 排队:根据流量控制规则(如 pfifo_fast、fq_codel)排队。
- 驱动发送:从 Qdisc 出队,调用驱动
ndo_start_xmit(),将数据放入 Ring Buffer,触发 DMA 传输。
常见性能陷阱:发送路径的 Qdisc 排队是容器网络限速和延迟抖动的常见来源。使用
tc -s qdisc 可以查看丢包和 backlog 情况。容器网络场景下,建议使用 fq_codel 或 cake 队列算法以降低 Bufferbloat。
3.2.2 发送路径架构图
3.3 sk_buff 结构体与内存管理
sk_buff 是 Linux 网络栈中最核心的数据结构,贯穿整个收/发路径。理解其内存布局和操作方式,对于编写 eBPF 程序进行数据包观测和修改至关重要。
3.3.1 sk_buff 结构体布局
sk_buff 采用 分片式 内存管理,通过指针指向实际的数据区域,避免数据拷贝。
| 字段 | 类型 | 说明 |
|---|---|---|
| head | unsigned char * | 数据缓冲区起始地址 |
| data | unsigned char * | 当前协议层数据起始地址(随协议层变化) |
| tail | unsigned char * | 当前协议层数据结束地址 |
| end | unsigned char * | 数据缓冲区结束地址 |
| len | unsigned int | 当前数据长度(tail - data) |
| cb | char[48] | 控制块,用于各层私有数据(如 TCP 控制块) |
| dev | struct net_device * | 接收/发送的网络设备 |
| sk | struct sock * | 关联的 socket |
3.3.2 内存管理机制:共享与克隆
为了减少内存拷贝,sk_buff 支持 克隆(clone) 和 共享(shared) 机制。
- skb_clone():创建一个新的
sk_buff结构体,但共享同一个数据缓冲区。引用计数增加,适用于多播、tcpdump 等场景。 - pskb_copy():部分拷贝,只拷贝
sk_buff头部和数据的前len字节,适用于需要修改协议头的场景。 - skb_copy():完全拷贝,包括数据缓冲区,适用于需要独立修改完整数据的场景。
eBPF 观测要点:在 eBPF 程序中访问
sk_buff 时,需要注意数据指针的偏移。例如,在 XDP 程序中,data 指向帧头(L2);在 TC 程序中,data 指向网络层头(L3)。使用 bpf_skb_load_bytes() 和 bpf_skb_store_bytes() 可以安全地读写数据。
3.3.3 sk_buff 内存布局示意图
3.4 本章小结
- 接收路径:NAPI 通过中断收敛 + 软中断轮询,实现高吞吐收包。关键瓶颈在软中断 CPU 占用和 Ring Buffer 大小。
- 发送路径:Qdisc 排队是延迟和丢包的主要来源,容器网络场景需关注队列算法选择。
- sk_buff:通过指针操作和克隆/共享机制,实现零拷贝数据传递。eBPF 程序需注意 data 指针的协议层偏移。