干过几年 Linux 服务器和应用开发的人,大概都遇到过这种场景:业务高峰期,用户反馈“页面卡了”;你上去一看,CPU 不高、磁盘不忙、内存也够,可 ssh 进去敲个命令都要转两秒。查到最后,问题往往不在应用,而在内核的网络路径上。这就是 Linux 内核网络子系统的怪脾气——它平时安静得像不存在,可一旦哪个环节失速,整个系统的服务质量都会被打回原形。
这篇文章想聊的,就是我踩过这些坑之后沉淀下来的网络子系统排查套路和源码阅读路径。我会从一次真实的收发包性能事故讲起,拆开网卡中断、软中断、协议栈、socket 队列这条完整链路,再把 TCP 调参、网卡多队列、NAPI、ftrace/perf/dropwatch 观测这些高级话题串起来。无论是做系统运维、内核开发,还是嵌入式 Linux 上的网络驱动移植,这条“从收包到应用”的流水线都是绕不开的主干道。
1. 开局先看一场真实事故:CPU 空闲但服务抖动
有次线上 Nginx 集群晚高峰出现诡异现象:带宽没跑满,总 CPU 使用率也就 20% 出头,但客户端超时率明显上升。我用 sar -n DEV 看流量,再用 top -H 看线程,发现一个规律——CPU0 的软中断(si)占用已经接近 100%,其他核心全部闲着。再配合 ethtool -S eth0 检查网卡计数,rx_missed 和 rx_dropped 都在缓慢上涨。
这就是非常典型的“中断收包集中在单核”导致的背压。这台机器是千兆网卡,默认只开了一个队列,硬中断基本落在 CPU0 上,小包并发一上来,CPU0 光处理软中断就忙不过来,内核的 netdev_max_backlog 队列一旦堆满,包就开始丢。应用层看到的表现是连接时断时续,请求偶发超时。
这个问题不复杂,但很能说明一件事:Linux 内核网络子系统不是黑盒,它更像一条流水线。硬中断只是按门铃,真正干活的是软中断和协议栈;任何一个“工位”堵住,整条线的吞吐都会降级。后面的内容,我就按这条流水线的顺序来拆解,从收包路径讲到 TCP 参数,再讲到观测工具和排查手法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 网络包在内核里到底跑了多远
2.1 先建立“收包流水线”的整体认知
一个数据包从网线进入 socket,中间经历的环节远比你想象的多。简单梳理一下主干路径:
- 网卡通过 DMA 把包写入内存中的 ring buffer,然后触发硬中断。
- CPU 响应硬中断,执行中断处理函数。这个函数不会做太多事,核心是调用
napi_schedule(),把后续处理交给软中断。 - 软中断(
NET_RX_SOFTIRQ)唤醒,进入net_rx_action(),开始轮询网卡的poll回调。 - 驱动从 ring buffer 取出
sk_buff,调用netif_receive_skb()把包交给协议栈。 - 协议栈按顺序处理:
ip_rcv()做 IP 层解析,路由查找,netfilter 钩子,最终进入tcp_v4_rcv()或udp_rcv()。 - 包进入 socket 的接收队列,唤醒等待在
read()上的进程。
理解这条链路的第一个关键点,是“硬中断只做最小工作”。真正消耗 CPU 的是软中断和协议栈处理。很多人查网络性能时只盯硬中断,其实大多数瓶颈都出在硬中断之后的阶段,尤其是软中断分配不平衡、协议栈锁竞争、netfilter 规则过多这几类问题上。
第二个关键点是“每一层都可能丢弃包”。驱动 ring buffer 满了会丢,netdev_max_backlog 满了会丢,netfilter 规则拒绝会丢,socket 接收队列满了也会丢。排查时如果只看应用日志,往往什么异常都看不到,必须一层一层查计数。
2.2 sk_buff:贯穿全程的“快递箱”
如果说网络子系统是一条流水线,那 sk_buff 就是在这条线上跑来跑去的快递箱。协议栈里的函数几乎都在跟它打交道,源码里见到 struct sk_buff *skb 的频率高到你不想记住都难。
很多人误以为 sk_buff 只是“一大块存数据的内存”,这理解偏差很大。它真正的设计精髓在于“分片视图”:数据本身可能存放在多个不连续的内存页里,而 sk_buff 通过 head、data、tail、end 这些指针描述当前协议层能看到的数据范围。TCP 层给包加头,就执行 skb_push() 把 data 指针往前移;IP 层拆掉头,就执行 skb_pull() 把 data 指针往后移。整条链路上数据基本不拷贝,只是指针在“拨来拨去”。
这套机制带来的实际收益是巨大的。一次标准的 TCP 收包,从网卡驱动到应用层,真正发生的数据拷贝可能只有最后那次从内核 socket 缓冲区拷到用户态缓冲区的动作。中间穿越 IP 层、TCP 层时,全是指针操作。所以如果你在读网络驱动源码,看到一堆 skb_reserve()、skb_put()、skb_pull() 的调用,不用慌,它们都是在调整那个“快递箱”的盖子位置,而不是把货倒来倒去。
2.3 协议栈的“分诊台”:路由查找与 netfilter
包到达 IP 层后,内核要做一个关键决策:这个包是发给本机的,还是需要转发的?这个决策由路由查找完成,核心函数是 ip_rcv() 之后的 ip_route_input_noref()
