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 队列中取出数据包,然后调用协议栈注册的处理函数。
流程大致是这样的:
net_rx_action遍历poll_list,找到注册的网卡驱动轮询函数- 驱动轮询函数从Ring Buffer中取出一批数据包(NAPI机制)
- 每个数据包被送入
__netif_receive_skb函数 - 这里会调用
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层。这里主要做:
- UDP校验和验证(如果启用的话)
- 查找对应的Socket:根据目的端口号,在
udp_table哈希表中找到对应的struct sock - 数据入队:把数据放入Socket的接收队列
- 唤醒等待进程:如果有进程在
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层之间的衔接——这是很多性能问题的根源。
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层之间的衔接,这是很多性能问题的藏身之处。