一粟 · yisu.app
· 14 min read
linux网络

写给工程师的 Linux 网络栈:从 skb 到 socket

很多时候"网络问题"其实是内核里几个数据结构的故事。把 skb、socket buffer、softirq、NAPI 串成一条线,你才会知道为什么 NIC 中断分布不均是性能杀手。

"网络抖动"是工程师最爱甩的锅之一。但每次深入排查,问题最终都落在某个具体的内核机制上。要真正理解 Linux 网络栈,至少要把几个数据结构与流程串起来。

从网卡到 socket:一次接收的旅程

当一个网络包到达网卡,Linux 会做这些事:NIC 通过 DMA 把包写到 ring buffer;网卡触发硬中断;NAPI 关闭 IRQ 开始轮询,一次收多个包;每个包分配一个 sk_buff 结构;进入协议栈依次走 IP、TCP;根据五元组找到对应 sock,把 skb 挂到接收队列;唤醒等待在 epoll_wait 的进程。每一步都可能成为瓶颈。

skb:网络栈的通用货币

sk_buff 是 Linux 网络栈里数据包的统一表示。它不存数据本身,只存指针——指向包的各层 header。这让协议栈各层可以高效地"加头"或"去头",不用拷贝。skb 的分配与释放是网络栈开销的大头。高 PPS 场景下,slab allocator 的命中率直接决定性能。

struct sk_buff {
    struct sk_buff      *next;
    struct sk_buff      *prev;
    struct sock         *sk;          // 所属 socket
    struct net_device   *dev;         // 入/出口网卡
    unsigned int         len,         // 总长度
                         data_len;    // 分段数据长度
    __u32                hash;        // 用于 RPS/RFS
};

NAPI:开关 IRQ 的智慧

早期 Linux 每收一个包就触发一次硬中断,高 PPS 下 CPU 全在处理中断,没空处理协议栈。NAPI 的思路是:"先关掉 IRQ,轮询拿一批,再开 IRQ"。这样在高负载时一次性处理多个包,低负载时及时响应。

NAPI 是 Linux 网络栈性能的基石。理解它就理解了"为什么 softirq 占用高是正常的"。

中断分布:被低估的瓶颈

多队列网卡的 IRQ 由 /proc/IRQ/{irq}/smp_affinity 控制。如果所有 IRQ 都落在 CPU 0,就会出现 CPU 0 跑满、其他 CPU 闲着、应用进程被 softirq 抢占、延迟飙升。解决方法:用 set_irq_affinity.sh 把 NIC 队列的 IRQ 均匀分布到不同 CPU。云上实例一般默认配置好,但裸机经常没有。

RPS / RFS:把包分发到多个 CPU

如果网卡队列数少于 CPU 数,单队列就会成为瓶颈。RPS 用软件方式把收到的包分发到多个 CPU 的 backlog 队列。RFS 更进一步,把同一 flow 的包分发到正在处理该 socket 的 CPU,提高 cache 命中率。

echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

SO_REUSEPORT:让多进程共享端口

单个 socket 在多核下是瓶颈——所有连接都从一个 sock 队列读。SO_REUSEPORT 让多个进程/线程各自持有独立的 socket 但共享端口,内核做 hash 分发。Nginx 1.9.1+ 用它实现了真正意义上的多核扩展。

性能调优清单

  • 检查 IRQ 分布是否均匀。
  • 启用 RPS/RFS(如果网卡队列少)。
  • 启用 SO_REUSEPORT(应用侧)。
  • 调大 net.core.netdev_max_backlog。
  • 查看 /proc/net/softnet_stat 的第三列(是否丢包)。
  • 用 perf top 看 softirq 占比是否正常。

把 Linux 网络栈拆开看,"网络问题"就不再是黑箱。每一次抖动背后都有具体的数据结构与机制。理解它们,你才能从"甩锅"变成"定位"。