3. Linux 内核网络协议栈:数据包接收与发送全流程

本章深入剖析 Linux 内核网络协议栈的核心数据路径,包括数据包的接收(NAPI 与软中断)、发送流程,以及关键数据结构 sk_buff 的内存管理机制。理解这些底层原理是进行容器网络性能调优与 eBPF 观测的基础。

3.1 数据包接收流程:从网卡到 socket

数据包接收是网络栈中最复杂、性能最敏感的路径。现代 Linux 使用 NAPI(New API) 机制,结合硬中断与软中断,实现高效收包。

3.1.1 接收路径核心步骤

  1. DMA 写入:网卡通过 DMA 将数据包直接写入主存中的 Ring Buffer(环形缓冲区),无需 CPU 干预。
  2. 硬中断触发:网卡产生硬件中断,通知 CPU 有新数据到达。
  3. NAPI 调度:中断处理程序关闭该网卡的中断,并将设备的 NAPI 结构体添加到软中断的 poll 列表中。
  4. 软中断处理(ksoftirqd):CPU 在 do_softirq() 中执行网络 RX 软中断(NET_RX_SOFTIRQ),调用 NAPI 的 poll 方法。
  5. 协议栈处理:从 Ring Buffer 取出数据,构建 sk_buff,依次经过链路层(eth_type)、网络层(ip_rcv)、传输层(tcp_v4_rcv)。
  6. socket 交付:数据放入对应 socket 的接收队列,唤醒等待的用户态进程。
性能关键点:NAPI 的核心优势在于 中断收敛。在高吞吐场景下,网卡中断被关闭,CPU 通过轮询(polling)批量收包,避免频繁中断导致的上下文切换开销。
观测提示:使用 cat /proc/net/softnet_stat 可以查看每个 CPU 的软中断统计,包括处理包数、丢包数(由于 backlog 队列满)和挤压次数。

3.1.2 NAPI 与软中断交互流程

下图展示了从硬件中断到软中断,再到协议栈的完整调用链。

NAPI 数据包接收流程 NAPI 数据包接收流程 网卡 DMA 硬中断处理 NAPI 调度 软中断 (ksoftirqd) NAPI poll() 协议栈处理 Socket 队列 关键说明: 1. 硬中断处理程序关闭当前网卡中断,避免中断风暴。 2. NAPI poll() 在软中断上下文中运行,可被抢占。 3. 协议栈处理包括 IP 分片重组、路由查找、Netfilter 钩子等。 4. 最终数据通过 tcp_queue_rcv() 放入 socket 接收缓冲区。

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 发送路径核心步骤

  1. 系统调用:用户态调用 send() / sendmsg(),进入内核 socket 层。
  2. 传输层处理:TCP 分段(或 GSO 延迟分段)、添加 TCP 头、计算校验和。
  3. 网络层处理:路由查找、IP 分片(如果需要)、添加 IP 头、Netfilter 钩子。
  4. 邻居子系统:通过 ARP/ND 解析下一跳 MAC 地址,将数据帧放入 Qdisc(队列规则)。
  5. Qdisc 排队:根据流量控制规则(如 pfifo_fast、fq_codel)排队。
  6. 驱动发送:从 Qdisc 出队,调用驱动 ndo_start_xmit(),将数据放入 Ring Buffer,触发 DMA 传输。
常见性能陷阱:发送路径的 Qdisc 排队是容器网络限速和延迟抖动的常见来源。使用 tc -s qdisc 可以查看丢包和 backlog 情况。容器网络场景下,建议使用 fq_codel 或 cake 队列算法以降低 Bufferbloat。

3.2.2 发送路径架构图

数据包发送流程 数据包发送流程 用户态 send() Socket 层 TCP/IP 协议栈 邻居子系统 Qdisc 排队 驱动 xmit() Ring Buffer 发送路径优化点: GSO/GRO 卸载、TSO 卸载、XDP 发送重定向、Qdisc 选择(fq_codel vs pfifo_fast)。

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 内存布局示意图

sk_buff 内存布局 sk_buff 内存布局与指针操作 数据缓冲区 (skb->head ~ skb->end) head data (L2) data (L3) data (L4) tail end 指针移动规则: 协议栈向下传递时,data 指针后移(push 协议头);向上传递时,data 指针前移(pull 协议头)。

3.4 本章小结

  • 接收路径:NAPI 通过中断收敛 + 软中断轮询,实现高吞吐收包。关键瓶颈在软中断 CPU 占用和 Ring Buffer 大小。
  • 发送路径:Qdisc 排队是延迟和丢包的主要来源,容器网络场景需关注队列算法选择。
  • sk_buff:通过指针操作和克隆/共享机制,实现零拷贝数据传递。eBPF 程序需注意 data 指针的协议层偏移。