3. Linux 网络协议栈基础:数据包收发、Socket缓冲区与NAPI

这一章,我们聊聊Linux网络协议栈最核心的几个机制。说白了,就是数据包从网线进来,到应用程序读到它,这中间到底发生了什么。反过来,应用发数据,又是怎么一步步送到网卡上的。

我在RK平台上调网络性能时,发现很多人一上来就调中断、调队列,但连NAPI和传统中断的区别都说不清楚。嗯,这样调优很容易跑偏。我们先打好基础。

3.1 网络数据包收发全流程

先看一张整体流程图,把数据包的“一生”串起来。

Linux网络数据包收发流程(接收方向) ① 网卡接收数据包 ② DMA写入Ring Buffer ③ 硬中断触发 ④ NAPI轮询(软中断) ⑤ 协议栈处理(IP/TCP) ⑥ Socket缓冲区 → 应用 关键点 • 硬中断只做最轻量的事 • 实际收包在软中断中完成 • NAPI合并中断,减少CPU开销 避坑 • 中断太多 → 活锁 • 轮询太久 → 其他任务饿死

接收方向,我习惯把它分成六个阶段。每个阶段都有坑,我们一个一个说。

3.1.1 从网线到Ring Buffer

数据包从网线进来,网卡通过DMA直接把数据写到内存里的Ring Buffer。这个过程CPU完全不参与。你想想看,如果每个包都要CPU去搬,那千兆网卡早就把CPU撑爆了。

小技巧: 在RK平台上,可以用 ethtool -g eth0 查看Ring Buffer大小。默认值往往偏小,大流量下容易丢包。我一般会调大Rx队列深度。

3.1.2 硬中断与NAPI的配合

DMA写完数据后,网卡触发硬中断。注意,硬中断里只做一件事:告诉内核“有数据来了”,然后立刻返回。真正的收包工作,交给NAPI在软中断中轮询完成。

为什么要这么设计?因为硬中断优先级太高,如果每个包都触发硬中断,系统会陷入“中断风暴”,也就是活锁——CPU光顾着响应中断,没时间处理数据了。

我曾经踩过的坑: 在某款RK3568板子上,开启多队列网卡后,没做RSS哈希均衡,结果所有中断都挤在CPU0上。网络延迟直接飙升到毫秒级。后来用 irqbalance 或者手动绑中断亲和性才解决。

3.2 Socket缓冲区管理

数据包经过协议栈处理后,最终要放到Socket的接收缓冲区里,等待应用程序来读。这个缓冲区,就是 sk_buff 结构体。

sk_buff 是Linux网络子系统中最重要的数据结构,没有之一。它贯穿整个协议栈,从驱动层到应用层,所有操作都围绕着它。

3.2.1 sk_buff 的核心字段

字段 作用 我的备注
dev 接收/发送的网络设备 多网卡时靠它区分
data / head 数据指针范围 head是起始,data是当前协议头
len 数据长度 不包括协议头预留空间
cb[] 控制块,各层私有数据 TCP重传、IP分片都用它
sk 所属Socket 没有这个,数据就找不到应用

我个人习惯在调试时,用 perf probe 或者 ftrace 跟踪 __alloc_skb 和 kfree_skb,看看谁在频繁分配和释放sk_buff。如果分配次数异常高,说明可能有内存泄漏或者小包攻击。

3.2.2 缓冲区大小与调优

Socket接收缓冲区的大小,直接影响吞吐量。太小了,数据包会被丢弃;太大了,浪费内存,而且延迟会变高。

查看当前值:

# 查看接收缓冲区大小(字节)
cat /proc/sys/net/ipv4/tcp_rmem
# 输出示例:4096  131072  6291456
# 分别对应:最小值、默认值、最大值

在RK平台上,如果跑视频流或大数据传输,我建议把默认值调大:

echo '4096 262144 8388608' > /proc/sys/net/ipv4/tcp_rmem
注意: 调大缓冲区不是万能的。如果应用层读取速度跟不上,缓冲区再大也只是延迟丢包。我曾经在RK3399上遇到过,缓冲区调大了,但应用层用阻塞读,结果内存占用飙升,OOM killer把进程杀了。

3.3 NAPI与新API机制

NAPI(New API)是Linux网络子系统中里程碑式的改进。它解决了传统中断方式在高吞吐下的性能瓶颈。

3.3.1 传统中断 vs NAPI

特性 传统中断方式 NAPI方式
触发方式 每个包都触发硬中断 第一个包触发中断,后续轮询
高吞吐表现 中断风暴,CPU满载 轮询模式,CPU利用率平稳
低延迟场景 响应快,适合低流量 轮询有微秒级延迟
适用场景 低速网络(< 1Gbps) 高速网络(≥ 1Gbps)

说白了,NAPI的核心思想就是:中断来了,先关中断,然后轮询收包,直到收完或者达到配额,再开中断。这样避免了每个包都打断CPU。

3.3.2 NAPI的工作流程

  1. 网卡收到第一个包,触发硬中断。
  2. 中断处理程序调用 napi_schedule(),把NAPI结构体加入软中断队列。
  3. 软中断(NET_RX_SOFTIRQ)执行 napi_poll()。
  4. 轮询函数从Ring Buffer取包,送入协议栈。
  5. 如果收完所有包,或者达到 budget(默认64个包),退出轮询,重新开启硬中断。
调优参数: /proc/sys/net/core/netdev_budget 控制每次NAPI轮询最多处理多少个包。默认300(较新内核),在RK平台上如果CPU核数少,可以适当降低到128,防止软中断占用太多CPU时间。

3.3.3 多队列与RPS/RFS

现代网卡支持多队列(RSS),每个队列可以绑定到不同CPU。但RK平台有些网卡(比如内置的GMAC)不支持硬件RSS。这时候可以用软件的RPS(Receive Packet Steering)来模拟。

开启RPS:

# 假设有4个CPU,把eth0的队列0分发到CPU0-3
echo 'f' > /sys/class/net/eth0/queues/rx-0/rps_cpus

RFS(Receive Flow Steering)更进一步,它根据流的哈希值,把同一个流的数据包送到同一个CPU,提高缓存命中率。我建议在数据库或视频流场景下开启RFS。

注意: RPS/RFS会消耗额外的CPU做哈希计算。如果CPU本身已经是瓶颈,开RPS可能适得其反。我在RK3588上测试过,8核CPU开RPS后,小包吞吐量提升了15%,但CPU利用率也高了10%。需要权衡。

3.4 本章小结

这一章我们覆盖了三个核心知识点:数据包从网卡到应用的完整路径、sk_buff缓冲区的管理、以及NAPI如何拯救高吞吐场景。这些是后续调优的基础,也是排查网络问题的第一站。

记住一句话:网络性能调优,本质上是平衡中断、轮询、内存和CPU之间的关系。没有银弹,只有理解原理后的针对性调整。