第一百一十三章:收帆
问道期涉及内核源码:
一
清晨,海面上驶来一支船队。
林小源站在港口的栈桥上,看着第一艘船缓缓靠近。船还没停稳,一个信号灯就亮了——硬件中断。港口管理员从控制室里冲出来,看了一眼信号灯,然后按下一个开关,把信号灯关掉了。
"别急。" 管理员对林小源说,"这是 NAPI 的流程。信号灯亮了——说明有数据包到了。我先关掉信号灯,免得后面的包来了又亮又亮,搞得我手忙脚乱。然后我调度一个轮询任务,批量处理。"
他指了指港口内部的一条传送带。传送带开始转动,一个接一个的 sk_buff 从网卡方向滑出来。每个 sk_buff 都是刚分配的—— 从 slab 缓存里取出来,DMA 把网卡缓冲区里的数据直接映射过来,然后提交给 。
"从这里开始,就是协议栈的事了。" 管理员说。
第一个 sk_buff 滑到了一扇门前,门上写着"MAC 层"。一个戴着面罩的身影从门后走出来,看了看 sk_buff 的头部,检查了一下目标 MAC 地址。"是给我的。" 他撕掉 MAC 头——skb_pull(14)——然后把 sk_buff 推向下一扇门。
下一扇门写着"IP 层"。另一个身影走出来,解析 IP 头,查路由表,检查 TTL。"转发还是本地?" 他看了看目标 IP,"本地的。" 他撕掉 IP 头——skb_pull(20)——把 sk_buff 推向最后一扇门。
最后一扇门写着"传输层"。第三个身影走出来,看了看协议字段。"TCP。" 他找到对应的 socket,把 sk_buff 放进了 socket 的接收队列。
"三扇门,三层协议。" 管理员说,"每一层只看自己的报文头,处理完就撕掉,交给下一层。这就是数据包接收的路径。"
三扇门、三层协议——每一层只看自己的报文头,处理完就撕掉,交给下一层,整个过程干净利落。
二
船队越来越多,传送带转得飞快。
林小源注意到一个问题——MAC 层、IP 层、传输层那三个身影,动作都非常快,几乎没有停顿。但真正耗时的工作,发生在传送带旁边的一间暗室里。
"那是什么?"
管理员看了一眼:"软中断处理室。。"
他带林小源走到暗室门口。里面很暗,但能听到密密麻麻的声响——校验和计算、防火墙规则匹配、连接跟踪、协议栈回调……这些工作都在软中断上下文里完成。
"硬中断只做最少的工作——标记数据包到达,然后触发软中断。" 管理员说,"软中断在稍后的某个时机运行——可能是硬中断返回的时候,也可能是内核调度的时候。这样做的好处是:硬中断不会被长时间占用,其他设备的中断也能得到响应。"
"如果软中断处理得太慢呢?"
"那就丢包。网卡的接收队列满了,新来的包就被丢掉。所以软中断的处理速度很关键——NAPI 的轮询就是在这里工作的。一次取一批包,处理完再取下一批。如果还有没处理完的,就调度 内核线程继续处理。"
林小源望向暗室深处,看到一盏微弱的灯——那是 的工位。它的优先级很低,只有在正常流程忙不过来的时候才会被唤醒。
"硬中断是门铃,软中断是管家。" 管理员总结道,"门铃响一声就够了,管家负责把事情做完。"
林小源琢磨了一下——硬中断是门铃,软中断是管家,门铃响一声就够了,管家负责把活干完。
三
船队快收完了,海面上只剩几条零星的船影。
林小源坐在栈桥边,看着传送带上的 sk_buff 一个一个滑过。每个 sk_buff 都要经历同样的过程——DMA 到内存、分配结构体、层层剥离协议头、最终到达 socket 的接收队列。
"这个过程……有没有更快的办法?"
管理员坐到他旁边:"你问的是零拷贝。"
他捡起一块石头扔进海里。"传统的接收路径是这样的:网卡通过 DMA 把数据写到内核缓冲区——这是第一次。然后内核把数据从内核缓冲区复制到用户空间——这是第二次。两次拷贝。"
"如果能跳过中间那步呢?"
"DPDK 就是这么干的。" 管理员说,"用户态驱动直接从网卡的环形缓冲区取数据,绕过了整个内核协议栈。没有中断、没有软中断、没有层层剥离。数据从网卡直接到用户空间。"
"那协议栈呢?"
"没有协议栈。用户自己处理一切——协议解析、路由查找、连接管理,全在用户态完成。" 管理员的语气变得严肃,"好处是性能极高——百万级包每秒。坏处是你得自己写所有的东西,而且独占了网卡,其他进程用不了。"
林小源看着最后一条船靠岸。"所以零拷贝不是没有代价。"
"什么都没有代价。" 管理员站起来,拍了拍裤子上的灰,"你想要极致性能,就放弃通用性。你想要通用性,就接受内核协议栈的开销。Linux 的美妙之处在于——它两个都给你选。"
管理员拍了拍裤子站起来:"你想要极致性能,就放弃通用性。你想要通用性,就接受内核协议栈的开销。两个都给你选——这就是 Linux 的美妙之处。"
/*
* 数据包接收的路径:
*
* 网卡接收数据包
* ↓
* DMA 到内存
* ↓
* 触发中断 (或 NAPI 轮询)
* ↓
* 分配 sk_buff
* ↓
* netif_receive_skb()
* ↓
* 协议栈处理
* ↓
* 传递给 socket
*
* NAPI 接收流程:
* 1. 网卡中断
* 2. 禁用网卡中断
* 3. 调度 NAPI 轮询
* 4. 批量处理数据包
* 5. 处理完后启用中断
*/
printf("=== 数据包接收 — 收帆入港 ===\n\n");
printf("数据包接收路径:\n\n");
printf(" 网卡接收数据包\n");
printf(" ↓\n");
printf(" DMA 到内存\n");
printf(" ↓\n");
printf(" 触发中断\n");
printf(" ↓\n");
printf(" 分配 sk_buff\n");
printf(" ↓\n");
printf(" netif_receive_skb()\n");
printf(" ↓\n");
printf(" 协议栈处理\n");
printf(" ↓\n");
printf(" 传递给 socket\n\n");
printf("--- NAPI 接收流程 ---\n");
printf("1. 网卡中断\n");
printf("2. 禁用网卡中断\n");
printf("3. 调度 NAPI 轮询\n");
printf("4. 批量处理数据包\n");
printf("5. 处理完后启用中断\n\n");
printf("--- 协议栈处理 ---\n");
printf("MAC 层:\n");
printf(" 解析 MAC 头\n");
printf(" 检查目标 MAC\n\n");
printf("IP 层:\n");
printf(" 解析 IP 头\n");
printf(" 路由查找\n");
printf(" 传递给传输层\n\n");
printf("传输层:\n");
printf(" TCP/UDP 处理\n");
printf(" 传递给 socket\n\n");
printf("--- 软中断 ---\n");
printf("NET_RX_SOFTIRQ:\n");
printf(" 网络接收软中断\n");
printf(" 处理延迟的接收工作\n\n");
printf("NET_TX_SOFTIRQ:\n");
printf(" 网络发送软中断\n");
printf(" 处理延迟的发送工作\n");#include <stdio.h>
/*
* 数据包接收的路径:
*
* 网卡接收数据包
* ↓
* DMA 到内存
* ↓
* 触发中断 (或 NAPI 轮询)
* ↓
* 分配 sk_buff
* ↓
* netif_receive_skb()
* ↓
* 协议栈处理
* ↓
* 传递给 socket
*
* NAPI 接收流程:
* 1. 网卡中断
* 2. 禁用网卡中断
* 3. 调度 NAPI 轮询
* 4. 批量处理数据包
* 5. 处理完后启用中断
*/
int main() {
printf("=== 数据包接收 — 收帆入港 ===\n\n");
printf("数据包接收路径:\n\n");
printf(" 网卡接收数据包\n");
printf(" ↓\n");
printf(" DMA 到内存\n");
printf(" ↓\n");
printf(" 触发中断\n");
printf(" ↓\n");
printf(" 分配 sk_buff\n");
printf(" ↓\n");
printf(" netif_receive_skb()\n");
printf(" ↓\n");
printf(" 协议栈处理\n");
printf(" ↓\n");
printf(" 传递给 socket\n\n");
printf("--- NAPI 接收流程 ---\n");
printf("1. 网卡中断\n");
printf("2. 禁用网卡中断\n");
printf("3. 调度 NAPI 轮询\n");
printf("4. 批量处理数据包\n");
printf("5. 处理完后启用中断\n\n");
printf("--- 协议栈处理 ---\n");
printf("MAC 层:\n");
printf(" 解析 MAC 头\n");
printf(" 检查目标 MAC\n\n");
printf("IP 层:\n");
printf(" 解析 IP 头\n");
printf(" 路由查找\n");
printf(" 传递给传输层\n\n");
printf("传输层:\n");
printf(" TCP/UDP 处理\n");
printf(" 传递给 socket\n\n");
printf("--- 软中断 ---\n");
printf("NET_RX_SOFTIRQ:\n");
printf(" 网络接收软中断\n");
printf(" 处理延迟的接收工作\n\n");
printf("NET_TX_SOFTIRQ:\n");
printf(" 网络发送软中断\n");
printf(" 处理延迟的发送工作\n");
return 0;
}道藏笔记
内核启示
数据包接收的路径说起来不复杂:网卡通过 DMA 把数据写到内存,触发中断,分配 sk_buff,提交给 ,然后层层剥离协议头——MAC 层检查目标地址、IP 层查路由表、传输层找到对应的 socket 放进接收队列。
硬中断只做最少的工作——标记数据包到达,然后触发软中断 。真正耗时的活儿在软中断里干:校验和计算、防火墙规则匹配、连接跟踪、协议栈回调。硬中断是门铃,软中断是管家,门铃响一声就够了。如果软中断忙不过来,就调度 内核线程继续处理。
零拷贝是个有趣的优化:传统路径要两次拷贝——DMA 到内核缓冲区,再复制到用户空间。DPDK 绕过整个内核协议栈,用户态驱动直接从网卡取数据,性能极高,但代价是独占网卡、自己处理一切。Linux 的美妙之处在于两个都给你选。
数据包接收之试
数据包从网卡收上来进入协议栈时,本章提到的接收入口函数是什么?