3. 数据包从网卡到应用层的完整路径:NAPI、sk_buff、协议栈分层
理解 XDP 的插入位置和性能优势,必须先掌握 Linux 内核网络栈的经典收包路径。本节将沿着数据包从物理网卡到达用户态应用的全流程,拆解 NAPI 轮询机制、sk_buff 结构体以及协议栈分层处理的核心逻辑。
3.1 整体收包路径概览
数据包从硬件到应用层,依次经过以下关键阶段:
- 硬件接收:网卡(NIC)通过 DMA 将数据写入内存中的 Ring Buffer。
- 中断触发:网卡产生硬件中断,通知 CPU 有新的数据包到达。
- NAPI 轮询:中断处理程序关闭该中断,注册 NAPI 轮询机制,由内核在软中断上下文中批量拉取数据包。
- 构造 sk_buff:驱动层将 Ring Buffer 中的数据封装为内核网络栈的标准结构体
sk_buff。 - 协议栈分层处理:
sk_buff依次经过链路层(L2)、网络层(L3)、传输层(L4)的处理。 - Socket 投递:处理后的数据被放入对应 Socket 的接收队列。
- 用户态读取:应用通过
read()、recvfrom()等系统调用从 Socket 复制数据到用户空间。
XDP 的插入点: XDP 程序挂载在驱动层之后、构造
sk_buff 之前。这意味着 XDP 可以在内核为数据包分配昂贵的 sk_buff 结构体之前就做出丢弃或转发决策,这是其高性能的核心原因。
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() 方法 | 软中断上下文 |
| 5 | poll() 从 Ring Buffer 批量拉取数据包,直到预算耗尽或队列为空 | 软中断上下文 |
| 6 | 重新启用硬件中断 | 软中断上下文 |
关键参数: NAPI 的
budget(预算)和 weight(权重)控制每次轮询处理的数据包数量上限,防止软中断独占 CPU 时间。
3.3 sk_buff:内核网络栈的通用数据载体
sk_buff(Socket Buffer)是 Linux 内核网络子系统中最重要的数据结构,贯穿整个协议栈处理流程。它不仅仅是一个数据缓冲区,更是一个包含丰富元数据的控制结构。
3.3.1 sk_buff 的核心组成
| 组件 | 说明 | 关键字段示例 |
|---|---|---|
| 数据指针 | 指向实际网络数据的指针,包括 head、data、tail、end | skb->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 处理的关键。
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 等程序类型。
3.5 本章小结
- NAPI 通过中断+轮询的混合模式,解决了高速网络下的中断风暴问题,是高性能收包的基础。
- sk_buff 是内核网络栈的核心数据结构,其分配和操作是主要的性能开销来源。
- 协议栈分层 提供了清晰的模块化设计,每一层都有对应的钩子点供 eBPF 程序介入。
- XDP 的插入点 位于驱动层之后、sk_buff 分配之前,使其能够以极低的代价处理数据包,非常适合 DDoS 防御场景。