3. 数据包从网卡到应用层的完整路径:NAPI、sk_buff、协议栈分层

理解 XDP 的插入位置和性能优势,必须先掌握 Linux 内核网络栈的经典收包路径。本节将沿着数据包从物理网卡到达用户态应用的全流程,拆解 NAPI 轮询机制、sk_buff 结构体以及协议栈分层处理的核心逻辑。

3.1 整体收包路径概览

数据包从硬件到应用层,依次经过以下关键阶段:

  1. 硬件接收:网卡(NIC)通过 DMA 将数据写入内存中的 Ring Buffer。
  2. 中断触发:网卡产生硬件中断,通知 CPU 有新的数据包到达。
  3. NAPI 轮询:中断处理程序关闭该中断,注册 NAPI 轮询机制,由内核在软中断上下文中批量拉取数据包。
  4. 构造 sk_buff:驱动层将 Ring Buffer 中的数据封装为内核网络栈的标准结构体 sk_buff。
  5. 协议栈分层处理:sk_buff 依次经过链路层(L2)、网络层(L3)、传输层(L4)的处理。
  6. Socket 投递:处理后的数据被放入对应 Socket 的接收队列。
  7. 用户态读取:应用通过 read()、recvfrom() 等系统调用从 Socket 复制数据到用户空间。
XDP 的插入点: XDP 程序挂载在驱动层之后、构造 sk_buff 之前。这意味着 XDP 可以在内核为数据包分配昂贵的 sk_buff 结构体之前就做出丢弃或转发决策,这是其高性能的核心原因。
数据包从网卡到应用层路径 网卡 NIC Ring Buffer NAPI 轮询 sk_buff 协议栈分层 Socket 队列 用户态应用 XDP 挂载点

3.2 NAPI:中断与轮询的平衡艺术

NAPI(New API)是 Linux 内核为高性能网络设计的收包机制,旨在解决传统中断方式在高速网络下的“中断风暴”问题。

3.2.1 传统中断模式的缺陷

  • 中断开销高:每个数据包都触发一次硬件中断,导致 CPU 频繁上下文切换。
  • 缓存污染:中断处理会刷新 CPU 缓存,影响整体系统吞吐量。
  • 无法批量处理:每次中断只处理一个数据包,无法利用批量收包的效率优势。

3.2.2 NAPI 的工作流程

步骤操作上下文
1网卡产生硬件中断硬中断上下文
2中断处理程序关闭该网卡的中断,将设备加入 NAPI 轮询列表硬中断上下文
3触发软中断(NET_RX_SOFTIRQ)硬中断上下文
4软中断处理程序调用 NAPI 的 poll() 方法软中断上下文
5poll() 从 Ring Buffer 批量拉取数据包,直到预算耗尽或队列为空软中断上下文
6重新启用硬件中断软中断上下文
关键参数: NAPI 的 budget(预算)和 weight(权重)控制每次轮询处理的数据包数量上限,防止软中断独占 CPU 时间。
NAPI 状态机 中断使能 中断触发 NAPI 轮询 轮询结束,重新使能中断

3.3 sk_buff:内核网络栈的通用数据载体

sk_buff(Socket Buffer)是 Linux 内核网络子系统中最重要的数据结构,贯穿整个协议栈处理流程。它不仅仅是一个数据缓冲区,更是一个包含丰富元数据的控制结构。

3.3.1 sk_buff 的核心组成

组件说明关键字段示例
数据指针指向实际网络数据的指针,包括 head、data、tail、endskb->data, skb->len
协议信息记录 L2/L3/L4 协议类型skb->protocol (ETH_P_IP)
控制信息用于控制数据包处理行为的标志位skb->pkt_type (PACKET_HOST)
时间戳记录数据包到达时间,用于 RTT 计算等skb->tstamp
安全标记用于 Netfilter 和防火墙的标记skb->mark
分片信息管理 IP 分片和重组skb_shinfo(skb)

3.3.2 sk_buff 的内存布局

sk_buff 采用灵活的线性缓冲区和分片缓冲区(frag_list)组合,支持零拷贝和高效的数据操作。

// sk_buff 结构体简化示意
struct sk_buff {
    /* 数据指针 */
    unsigned char   *head;      // 缓冲区起始
    unsigned char   *data;      // 当前协议层数据起始
    unsigned char   *tail;      // 当前协议层数据结束
    unsigned char   *end;       // 缓冲区结束

    /* 长度信息 */
    unsigned int    len;        // 当前数据长度
    unsigned int    data_len;   // 分片数据长度

    /* 协议信息 */
    __be16          protocol;   // L3 协议类型

    /* 控制信息 */
    u32             mark;       // 防火墙标记
    u16             pkt_type;   // 数据包类型

    /* 时间戳 */
    struct skb_timeval tstamp;

    /* 分片信息 */
    skb_frag_t      frags[MAX_SKB_FRAGS];
    struct sk_buff  *next;      // 分片链表
};
性能陷阱: sk_buff 的分配和释放是内核网络栈的主要开销之一。XDP 在 sk_buff 分配之前介入,避免了这一开销,这也是 XDP 能实现百万级 PPS 处理的关键。
sk_buff 内存布局 线性缓冲区 (Linear Buffer) head data 协议头 (L2/L3/L4) 负载 (Payload) tail end 分片缓冲区 (frag_list / skb_shared_info) 用于处理大数据包、GSO/TSO 卸载、零拷贝

3.4 协议栈分层处理

Linux 内核网络协议栈严格遵循 OSI 模型的层次划分,每一层负责特定的协议处理逻辑。

3.4.1 各层处理函数与职责

层级入口函数核心职责
L2 链路层__netif_receive_skb_core()以太网帧头解析、VLAN 处理、网桥转发、TC(Traffic Control)分类器
L3 网络层ip_rcv() (IPv4) / ipv6_rcv() (IPv6)IP 头校验、路由查找、TTL 处理、IP 分片重组、Netfilter 钩子(PREROUTING)
L4 传输层tcp_v4_rcv() / udp_rcv()端口匹配、TCP 状态机、拥塞控制、校验和验证、Socket 查找
Socket 层sock_queue_rcv_skb()将数据包放入对应 Socket 的接收队列,唤醒等待的进程

3.4.2 关键处理路径中的钩子点

协议栈在每一层都提供了钩子机制,允许内核模块或 eBPF 程序介入处理:

  • L2 层: TC(Traffic Control)的 cls_bpf 分类器,可以挂载 eBPF 程序进行流量控制。
  • L3 层: Netfilter 的 NF_INET_PRE_ROUTING 钩子,是 iptables/nftables 的入口点。
  • L4 层: tcp_v4_rcv() 中的 BPF_SOCK_OPS 钩子,用于 TCP 拥塞控制优化。
XDP 与协议栈的关系: XDP 程序在 L2 层处理之前、__netif_receive_skb_core() 之前执行。这意味着 XDP 可以看到原始的网络帧,但无法直接访问 TCP/UDP 的 Socket 状态。如果需要更上层的上下文,可以结合 TC BPF 或 BPF_PROG_TYPE_SK_LOOKUP 等程序类型。
协议栈分层处理 L2 链路层:__netif_receive_skb_core() 以太网帧解析 | VLAN | 网桥 | TC 分类器 L3 网络层:ip_rcv() / ipv6_rcv() IP 头校验 | 路由查找 | 分片重组 | Netfilter PREROUTING L4 传输层:tcp_v4_rcv() / udp_rcv() 端口匹配 | TCP 状态机 | 校验和 | Socket 查找 Socket 层:sock_queue_rcv_skb() XDP 在此层之前执行

3.5 本章小结

  • NAPI 通过中断+轮询的混合模式,解决了高速网络下的中断风暴问题,是高性能收包的基础。
  • sk_buff 是内核网络栈的核心数据结构,其分配和操作是主要的性能开销来源。
  • 协议栈分层 提供了清晰的模块化设计,每一层都有对应的钩子点供 eBPF 程序介入。
  • XDP 的插入点 位于驱动层之后、sk_buff 分配之前,使其能够以极低的代价处理数据包,非常适合 DDoS 防御场景。