3. 内核协议栈的UDP处理路径:软中断、IP层与UDP层处理

上一章我们聊了网卡怎么把数据扔进内存。那之后呢?数据在内存里躺着,CPU怎么知道该干活了?

答案是——硬中断 + 软中断。我刚开始调优UDP性能时,总以为瓶颈在应用层。后来才发现,很多时候问题出在中断处理路径上。说白了,数据从网卡到应用,中间要过好几道关卡。

3.1 硬中断与软中断的分工

网卡收到数据包后,会触发一个硬中断。硬中断处理程序非常短——它得赶紧跑完,不然会阻塞其他中断。它只做一件事:把数据包挂到CPU的软中断队列上,然后告诉系统「该处理网络数据了」。

嗯,这里有个关键点:硬中断处理程序不会做协议解析。它只是搬运工。

核心流程:

  • 硬中断:通知CPU有数据到达,触发软中断调度
  • 软中断:执行实际的网络协议栈处理(IP层、UDP层)

我遇到过一个问题:某个高并发UDP服务,CPU0的软中断占用飙到80%,其他核心却闲着。为什么?因为网卡把所有中断都发到了同一个CPU。后来我调整了RSS(Receive Side Scaling)队列,把中断分散到多个核心,吞吐量直接翻倍。

调优提示:检查 /proc/interrupts 看中断分布。如果某个CPU的中断数远高于其他核心,考虑调整RSS或使用 irqbalance 服务。

3.2 软中断处理:从队列到协议栈

软中断处理函数是 net_rx_action。它从每个CPU的 softnet_data 队列中取出数据包,然后调用协议栈注册的处理函数。

流程大致是这样的:

  1. net_rx_action 遍历 poll_list,找到注册的网卡驱动轮询函数
  2. 驱动轮询函数从Ring Buffer中取出一批数据包(NAPI机制)
  3. 每个数据包被送入 __netif_receive_skb 函数
  4. 这里会调用 ip_rcv 进入IP层处理

你想想看,如果一次软中断处理的数据包太多,会导致其他软中断(比如定时器、任务调度)被饿死。所以内核有个预算机制——每次软中断最多处理 netdev_budget 个包(默认300)。

我曾经踩过的坑:把 netdev_budget 调大到1000,以为能提高吞吐。结果网络延迟反而变大了,因为其他软中断被阻塞太久。后来我改成每次处理200个包,配合多队列,效果更好。

3.3 IP层处理:路由查找与分片重组

数据包进入IP层后,主要做三件事:

  • 校验和检查:验证IP头是否完整
  • 路由查找:判断这个包是发给本机还是需要转发
  • 分片重组:如果数据包被分片了,需要等所有分片到齐再重组

对于UDP通信,我们最关心的是路由查找。内核会查 fib_lookup 表,判断目的IP是不是本机地址。如果是,就交给 ip_local_deliver 处理。

这里有个性能陷阱:分片重组。UDP本身没有流控机制,如果发送端发的包超过MTU,IP层就会分片。接收端需要等所有分片到齐才能重组。如果某个分片丢了,整个包都得重传——但UDP不保证重传!

避坑指南:UDP应用最好控制数据包大小不超过MTU(通常1500字节),避免IP分片。我见过一个视频流服务,因为没注意这个,丢包率从0.1%飙升到5%。

3.4 UDP层处理:端口匹配与数据交付

IP层确认是发给本机的UDP包后,调用 udp_rcv 进入UDP层。这里主要做:

  1. UDP校验和验证(如果启用的话)
  2. 查找对应的Socket:根据目的端口号,在 udp_table 哈希表中找到对应的 struct sock
  3. 数据入队:把数据放入Socket的接收队列
  4. 唤醒等待进程:如果有进程在 recvfrom 上阻塞,就唤醒它

Socket查找是个哈希操作,按理说很快。但我在项目中遇到过一个问题:当UDP端口数非常多(比如几万个)时,哈希冲突会导致查找变慢。后来我查了内核代码,发现 udp_table 的哈希桶大小是固定的(UDP_HTABLE_SIZE_MIN,默认128)。

参数 默认值 说明
UDP_HTABLE_SIZE_MIN 128 UDP哈希表最小桶数
net.core.rmem_default 212992 UDP接收缓冲区默认大小(字节)
net.core.rmem_max 2088960 UDP接收缓冲区最大大小(字节)

调优建议:如果应用需要管理大量UDP Socket,可以考虑增大 net.ipv4.udp_hash_entries 来减少哈希冲突。不过这个参数需要重新编译内核才能生效。

3.5 完整路径的SVG流程图

下面这张图展示了UDP数据包从网卡到应用层的完整路径。我建议你仔细看看软中断和IP层之间的衔接——这是很多性能问题的根源。

UDP数据包内核协议栈处理路径 网卡接收数据包 DMA到Ring Buffer 硬中断触发 软中断 net_rx_action IP层 ip_rcv UDP层 udp_rcv 关键性能点 • 硬中断要快,不能做协议解析 • 软中断有预算限制(netdev_budget) • IP层路由查找可能成为瓶颈 • UDP层哈希冲突影响查找效率 常见优化方向 • RSS多队列分散中断 • 增大Ring Buffer大小 • 调整软中断预算

3.6 性能关键点总结

从上面的路径可以看出,UDP接收性能受多个环节影响。我个人习惯在排查问题时,按这个顺序检查:

  • 中断分布:检查 /proc/interrupts,确保中断均匀分布在多个CPU上
  • 软中断占用:用 top 看 si 列,如果某个CPU的软中断占用超过50%,说明需要优化
  • Ring Buffer丢包:用 ethtool -S eth0 看 rx_missed_errors 和 rx_fifo_errors
  • UDP接收缓冲区:检查 /proc/net/udp 中的 drops 字段

我曾经遇到的一个典型案例:某游戏服务器UDP丢包严重,排查发现是Ring Buffer太小(默认256),在高并发下溢出。把Ring Buffer增大到4096后,丢包率从3%降到0.01%。

嗯,到这里你应该对内核协议栈的UDP处理路径有了清晰的认识。下一层我们聊聊应用层怎么高效地读取这些数据——但那是后面的事了。先把今天的内容消化掉,尤其是软中断和IP层之间的衔接,这是很多性能问题的藏身之处。