2. Linux 内核协议栈基础:网络分层模型、套接字缓冲区(skb)、协议栈数据流路径
在深入 DPDK KNI 之前,必须理解 Linux 内核协议栈的核心机制。本章将剖析网络分层模型、套接字缓冲区(sk_buff)结构以及数据包在内核中的完整流经路径。这些知识是理解 KNI 如何与内核交互、以及性能瓶颈所在的基础。
2.1 网络分层模型:内核视角的 TCP/IP 协议栈
Linux 内核协议栈严格遵循 TCP/IP 分层模型,但实现上进行了优化与合并。从数据包处理的角度,主要分为以下三层:
| 层次 | 内核模块/子系统 | 核心数据结构 | 主要功能 |
|---|---|---|---|
| L2 链路层 | 网络设备子系统 (net_device) | sk_buff、net_device |
驱动接口、MAC 地址解析、ARP、桥接、VLAN |
| L3 网络层 | IPv4/IPv6 协议栈 | sk_buff、dst_entry |
路由查找、IP 分片与重组、Netfilter 钩子 |
| L4 传输层 | TCP/UDP 协议模块 | sk_buff、sock、tcp_sock |
连接管理、流量控制、校验和计算、数据分段 |
关键认知: 内核协议栈的每一层都通过
sk_buff 传递数据,并利用 net_device 和 sock 结构体维护状态。理解这些结构的布局是优化数据路径的前提。
下图展示了数据包从网卡驱动到用户态套接字的完整分层路径:
2.2 套接字缓冲区 (sk_buff):协议栈的核心数据单元
sk_buff 是 Linux 内核网络子系统中最关键的数据结构。它贯穿整个协议栈,承载着数据包的所有元数据和控制信息。理解其布局是进行性能优化的第一步。
2.2.1 sk_buff 的内存布局
每个 sk_buff 实例管理一个数据缓冲区,该缓冲区包含头部空间、数据区和尾部空间。指针操作是零拷贝的关键:
struct sk_buff {
/* 链表管理 */
struct sk_buff *next;
struct sk_buff *prev;
struct sk_buff_head *list;
/* 协议层指针 */
union {
struct tcphdr *th; /* 传输层头部 */
struct udphdr *uh;
struct icmphdr *icmph;
};
struct iphdr *ip_hdr; /* 网络层头部 */
struct ethhdr *mac_header; /* 链路层头部 */
/* 数据指针 */
unsigned char *head; /* 缓冲区起始 */
unsigned char *data; /* 数据起始 */
unsigned char *tail; /* 数据结束 */
unsigned char *end; /* 缓冲区结束 */
/* 长度信息 */
unsigned int len; /* 实际数据长度 */
unsigned int data_len; /* 分片数据长度 */
unsigned int truesize; /* 总分配大小 */
/* 设备与路由 */
struct net_device *dev; /* 关联网络设备 */
struct dst_entry *dst; /* 路由缓存 */
/* 时间戳 */
ktime_t tstamp; /* 接收时间戳 */
};
性能要点:
sk_buff 的 head、data、tail、end 四个指针构成了一个滑动窗口。协议栈在向下传递时,通过 skb_push() 在数据前添加头部;向上传递时,通过 skb_pull() 剥离头部。这种设计避免了数据拷贝,但频繁的指针操作会消耗 CPU 缓存。
2.2.2 sk_buff 的指针操作与数据流
下图展示了数据包在协议栈各层处理时,sk_buff 指针的变化过程:
2.3 协议栈数据流路径:从网卡到套接字
数据包从物理网卡到达用户态应用,经历了一条精心设计的函数调用链。理解这条路径上的每个关键节点,是定位性能瓶颈的基础。
2.3.1 接收路径 (RX) 关键函数链
| 步骤 | 函数/模块 | 位置 | 关键操作 |
|---|---|---|---|
| 1 | ixgbe_clean_rx_irq() |
驱动层 | 从 DMA ring 获取数据,分配 sk_buff |
| 2 | napi_gro_receive() |
NAPI 层 | GRO 合并,减少上层处理次数 |
| 3 | netif_receive_skb() |
设备子系统 | 分发到协议层,触发 Netfilter 钩子 |
| 4 | ip_rcv() |
网络层 | IP 校验、路由查找、Netfilter NF_INET_PRE_ROUTING |
| 5 | ip_local_deliver() |
网络层 | 分片重组,Netfilter NF_INET_LOCAL_IN |
| 6 | tcp_v4_rcv() / udp_rcv() |
传输层 | 查找套接字,处理 TCP 状态机 |
| 7 | tcp_v4_do_rcv() |
传输层 | 将数据放入接收队列,唤醒用户进程 |
性能陷阱: 接收路径上的 Netfilter 钩子(如 iptables 规则)和路由缓存查找是主要的 CPU 消耗点。在高速率场景下,这些操作会导致严重的缓存缺失和锁竞争。
2.3.2 发送路径 (TX) 关键函数链
| 步骤 | 函数/模块 | 位置 | 关键操作 |
|---|---|---|---|
| 1 | tcp_sendmsg() / udp_sendmsg() |
传输层 | 从用户态拷贝数据,构建 sk_buff |
| 2 | ip_local_out() |
网络层 | Netfilter NF_INET_LOCAL_OUT |
| 3 | ip_output() |
网络层 | 路由查找、分片、Netfilter NF_INET_POST_ROUTING |
| 4 | dev_queue_xmit() |
设备子系统 | 排队规则 (Qdisc) 处理,触发 Netfilter NF_INET_POST_ROUTING |
| 5 | sch_direct_xmit() |
Qdisc 层 | 调用驱动发送函数 |
| 6 | ixgbe_xmit_frame() |
驱动层 | 将 sk_buff 映射到 DMA ring,触发发送 |
2.3.3 完整数据流路径图
下图整合了接收与发送路径,展示了数据包在内核协议栈中的完整生命周期:
2.4 协议栈性能开销分析
理解协议栈的数据路径后,可以量化其主要性能开销来源。下表总结了各层的典型开销:
| 开销来源 | 所在层次 | 典型耗时 (CPU cycles) | 优化方向 |
|---|---|---|---|
| DMA 中断处理 | 驱动层 | 500-2000 | NAPI 轮询、中断合并 |
| sk_buff 分配与释放 | 驱动层 | 200-500 | slab 缓存、预分配 |
| Netfilter 钩子遍历 | 网络层 | 1000-5000 | 规则精简、nftables |
| 路由查找 | 网络层 | 300-800 | 路由缓存、FIB 优化 |
| TCP 校验和计算 | 传输层 | 200-600 | 硬件 offload |
| 用户态数据拷贝 | 系统调用层 | 1000-3000 | mmap、零拷贝 |
| 上下文切换 | 系统调用层 | 2000-5000 | 批处理、busy polling |
实践建议: 在评估 KNI 性能时,可以将上述开销作为基线。KNI 通过绕过部分内核路径来降低延迟,但同时也引入了
kni_net_rx 和 kni_net_tx 的额外处理开销。后续章节将详细对比这些差异。