2. Linux网络子系统概览

要理解 Linux 网络设备驱动的性能优化,必须先建立一张“全局地图”:一个数据包从网卡进入内核,到最终被应用程序读取,中间究竟经过了哪些层次、哪些关键数据结构、哪些同步与调度机制。本节围绕四个核心对象展开:协议栈分层、net_device、NAPI 与 sk_buff,并串联出完整的收发流程。

一、网络协议栈的分层结构

Linux 网络子系统采用经典的分层模型,但与传统 OSI 七层不同,它更贴近实现:自下而上依次是网络设备驱动层、链路层、网络层、传输层与套接字层。驱动层负责与硬件交互,通过 DMA 将报文搬入/搬出内存;链路层处理以太网帧、ARP、VLAN 等;网络层负责 IP 路由与转发;传输层实现 TCP/UDP;套接字层则向用户态提供 read/write、send/recv 等接口。

分层带来的关键性能含义是:每一层都可能引入内存拷贝、锁竞争与上下文切换。例如,传统收包路径中,驱动把数据放入内核缓冲区,协议栈再逐层解析,最后拷贝到用户空间。零拷贝、NAPI、中断合并等优化,本质上都是在减少层间开销或批量处理层间交互。

二、net_device:网卡在软件世界的“身份证”

每个网络接口在内核中对应一个 struct net_device,它既是设备的抽象,也是驱动与协议栈之间的契约。驱动在初始化时填充其中的关键字段与回调函数,协议栈则通过这些回调把数据交给硬件。

struct net_device {
    char            name[IFNAMSIZ];   // 如 eth0、ens33
    unsigned long   state;            // __LINK_STATE_START 等状态位
    unsigned int    flags;            // IFF_UP、IFF_RUNNING 等
    unsigned int    mtu;              // 最大传输单元
    const struct net_device_ops *netdev_ops;
    const struct ethtool_ops    *ethtool_ops;
    struct net_device_stats     stats;
    /* ... 省略大量字段 ... */
};

其中 netdev_ops 是最核心的操作集,包含 ndo_open、ndo_stop、ndo_start_xmit、ndo_set_rx_mode 等。发送路径上,协议栈最终调用 ndo_start_xmit 把 sk_buff 交给驱动;接收路径上,驱动通过 netif_rx 或 NAPI 的 napi_gro_receive 把数据上报。理解 net_device 的字段与回调,是定位“包卡在驱动还是协议栈”的第一步。

三、NAPI:中断与轮询的混合收包机制

早期网卡每收到一个包就触发一次硬中断,高流量下会出现“中断风暴”,CPU 大量时间耗在中断上下文,吞吐反而下降。NAPI(New API)的核心思想是:第一个包用中断通知,随后关闭该队列中断,改为轮询批量收包,直到预算耗尽或队列为空再重新开启中断。

NAPI 的关键对象是 struct napi_struct,驱动通过 netif_napi_add 注册轮询函数,并在中断处理中调用 napi_schedule 触发软中断。软中断上下文执行 poll 回调,每次处理受 weight 与 budget 限制,避免单次占用 CPU 过久。

static int my_poll(struct napi_struct *napi, int budget)
{
    int work = 0;
    while (work < budget) {
        skb = my_rx_one();
        if (!skb) break;
        napi_gro_receive(napi, skb);
        work++;
    }
    if (work < budget) {
        napi_complete_done(napi, work);
        enable_irq();   // 重新开启中断
    }
    return work;
}
NAPI 适合高吞吐场景;低延迟小流量场景下,纯中断可能更及时。实际调优常结合中断合并(ethtool -C)与多队列 RSS,让每个 CPU 处理独立队列,减少锁竞争。

四、sk_buff:数据包在内核中的载体

struct sk_buff 是网络子系统最核心的数据结构,它并不直接存放数据本身,而是通过指针描述数据在内存中的位置与各层头部偏移。关键字段包括:

  • head、data、tail、end:指向缓冲区边界与当前数据区,支持头部空间的预留与剥离。
  • len、data_len:线性区长度与分片长度,用于处理非线性数据(如 SG-DMA)。
  • dev、sk:关联的 net_device 与 socket,决定后续走向。
  • protocol、mark、priority:协议类型、流标记与优先级,影响路由与 QoS。

sk_buff 的设计精髓在于“移动指针而非拷贝数据”。例如,收到帧后,驱动把 skb->data 指向以太网头;进入网络层时,通过 skb_pull 上移 data 指针剥离以太头;发送时则用 skb_push 预留各层头部空间。这种指针操作让协议栈分层处理几乎不产生数据拷贝,是零拷贝优化的基础。

五、数据包收发流程全景

下面用一张图把上述对象串联起来,展示从网卡到应用、再从应用到网卡的完整路径。

Linux 网络子系统数据包收发路径 接收路径(RX) 网卡 NIC DMA 写入内存 硬中断 / NAPI napi_schedule 软中断 poll napi_gro_receive 协议栈处理 IP / TCP 解析 Socket 队列 唤醒用户进程 核心数据结构 net_device netdev_ops 回调 napi_struct poll / weight sk_buff head/data/tail 发送路径(TX) 用户 send() 拷贝到内核 skb TCP / IP 封装 skb_push 加头 qdisc 排队 流量控制 ndo_start_xmit 驱动发送 网卡 TX 环 DMA 读取发送 优化切入点:减少拷贝(零拷贝)、批量收包(NAPI)、中断合并、多队列 RSS、skb 复用与缓存

六、关键机制对比与常见误区

机制 核心作用 典型参数/接口 性能影响
net_device 设备抽象与回调契约 netdev_ops、ethtool_ops 决定驱动能力与可调项
NAPI 中断+轮询混合收包 weight、budget、napi_schedule 降低中断风暴,提升吞吐
sk_buff 报文载体与指针操作 head/data/tail、skb_pull/push 减少拷贝,影响内存带宽
GRO/GSO 收包聚合与发包分段卸载 ethtool -K gro/gso on 减少协议栈处理次数
RSS/RPS 多队列分流到多 CPU ethtool -L、/proc/sys/net/core/rps 提升多核并行度
常见误区一:认为“零拷贝”就是完全不拷贝。实际上,零拷贝通常指避免内核与用户态之间的数据拷贝,驱动 DMA 与协议栈头部处理仍可能涉及内存操作。误区二:盲目开启所有 offload。某些网卡或虚拟化环境下,GRO/GSO/TSO 组合可能引发分片异常或延迟抖动,需要结合 ethtool -k 与抓包验证。误区三:只调 NAPI budget 而忽略中断亲和性,导致单核瓶颈。

七、场景复盘:一次收包吞吐上不去的排查

某服务器使用 10G 网卡,单流 TCP 收包只能到 3Gbps。排查步骤:先用 ethtool -S eth0 查看 rx_dropped 与 rx_missed,发现丢包集中在单一队列;再用 cat /proc/interrupts | grep eth0 确认中断都落在 CPU0。调整方案:开启多队列 RSS,将中断亲和性分散到多个 CPU;同时用 ethtool -C eth0 rx-usecs 32 做适度中断合并,并确认 NAPI weight 与 budget 匹配。调整后单流吞吐提升到 7Gbps 以上,CPU 软中断分布也更均衡。

这个案例说明:网络性能优化不是单点调参,而是协议栈分层、net_device 能力、NAPI 调度与 sk_buff 内存路径协同的结果。只有先看清数据包在每一层的形态与开销,后续的零拷贝与中断合并策略才能有的放矢。