Skip to content

第一百一十二章:港口

问道期

涉及内核源码:

林小源沿着海岸线走了很远,看到了一座真正的港口。

不是码头边那种简陋的泊位,而是一座巨大的、钢铁铸成的港口。港口的大门上挂着一块铜牌:。旁边还有几座稍小的港口——wlan0 是无线港,信号塔上天线林立;lo 是一座环形的内湖港口,船驶出去转一圈就回来了。

一个身穿工装的港口管理员站在 的大门前,手里拿着一块平板,上面显示着各种参数。

"你来看船?" 管理员问,"我是 ,每个网络设备都是我这个结构体的实例。"

他指了指大门上方的铭牌:"设备名 ,MTU 1500,MAC 地址 00:11:22:33:44:55。这些是最基本的属性。"

"MTU 是什么?"

"Maximum Transmission Unit——我这个港口一次能装的最大货物量。以太网默认 1500 字节。超过这个大小的包,要么在 IP 层分片,要么发不出去。" 管理员敲了敲铭牌,"你以后会经常遇到 MTU 的问题——尤其是遇到隧道设备的时候,外层封装会吃掉一部分 MTU。"

林小源望向港口内部。一排排巨大的传送带从港口深处延伸到海边,传送带上放着一个个 sk_buff。港口外面是大海,港口里面是协议栈。传送带就是数据包进出的通道。

林小源望着港口里那些传送带——进进出出的 sk_buff 全靠这些通道,堵了就得丢包。

管理员带林小源走进了港口的控制室。

墙上挂着两排指示灯,左边标着"发送队列",右边标着"接收队列"。每盏灯代表一个 sk_buff 正在排队等待处理。

"每个网络设备都有发送队列和接收队列。" 管理员指着左边的灯,"发送的时候,sk_buff 从协议栈下来,进入发送队列。网卡从队列里取数据包,一个一个发出去。如果队列满了——"

他按下一个按钮,所有红灯亮起。"这就是丢包。。应用层调了 send(),数据到了设备层,但队列排不下了,包就丢了。"

"那接收呢?"

"接收队列类似。网卡收到数据包,放进接收队列。如果内核处理不过来——比如软中断忙不过来——队列也会满,也会丢包。"

管理员走到另一面墙前,墙上画着两幅流程图。

"传统的接收模式:每个数据包到达都触发一次硬件中断。中断处理函数运行,分配 sk_buff,提交给协议栈。这在低流量时没问题——但高流量时,每秒几万个包,每个包一个中断,CPU 光处理中断就忙不过来了。这叫中断风暴。"

他翻到第二幅图:"NAPI 模式不一样。第一个包到达时触发中断,中断处理函数禁用网卡中断,然后调度一个轮询任务。这个任务在软中断里批量处理数据包——一次取几十个、几百个,处理完了再启用中断。"

"所以 NAPI 是用轮询代替中断?"

"准确地说,是低流量用中断、高流量用轮询。中断保证低延迟,轮询保证高吞吐。NAPI 把两者结合起来了。" 管理员关掉了墙上的灯,"这才是正道——不是非此即彼,而是各取所长。"

他忽然明白了管理员的意思——低流量用中断保延迟,高流量用轮询保吞吐,NAPI 两者兼得。

傍晚,林小源坐在港口的屋顶上,看着夕阳下的海面。

管理员递给他一杯茶。"你知道 吗?"

"设备的发送函数?"

"对。协议栈把 sk_buff 准备好了,调用 进入设备层。设备层做一些处理——QoS 排队、流量整形——然后调用驱动注册的 回调。驱动把 sk_buff 交给网卡硬件,网卡把数据发到线路上。"

他指着远处的海面:"整条链路就是:协议栈 → 设备队列 → 驱动 → 网卡硬件 → 线路。每一层都只关心自己的事——协议栈不关心用的是以太网还是 WiFi,设备层不关心上面跑的是 TCP 还是 UDP。这就是分层的好处。"

"如果发送失败呢?"

"驱动返回 ,设备层会把 sk_buff 留在队列里,稍后重试。如果网卡彻底坏了—— 关闭设备,队列里的包全部丢弃。"

林小源喝了一口茶,看着港口的灯一盏一盏亮起来。 的灯最亮,wlan0 的灯微微闪烁,lo 的灯安静地发着柔和的光。

"每个设备都不一样,但接口是一样的。" 管理员说,"——所有驱动都实现这些回调。内核不需要知道你的硬件长什么样,只需要你提供这些接口。这就是 的设计哲学。"

"不管硬件长什么样,接口都一样——这才是 的设计精髓。" 管理员最后补了一句。

c
/*
 * 网络设备 (struct net_device) 的关键字段:
 *
 *   name — 设备名(如 eth0)
 *   mtu — 最大传输单元
 *   mac_addr — MAC 地址
 *   tx_queue — 发送队列
 *   rx_queue — 接收队列
 *
 * 网络设备的操作:
 *   ndo_open — 打开设备
 *   ndo_stop — 关闭设备
 *   ndo_start_xmit — 发送数据包
 *   ndo_set_rx_mode — 设置接收模式
 *
 * 设备类型:
 *   eth0 — 以太网
 *   wlan0 — 无线网
 *   lo — 回环
 *   tun/tap — 虚拟设备
 */

printf("=== 网络设备 — 数据包的港口 ===\n\n");

printf("网络设备:\n");
printf("  eth0: 以太网\n");
printf("  wlan0: 无线网\n");
printf("  lo: 回环(127.0.0.1)\n");
printf("  tun/tap: 虚拟设备\n\n");

printf("--- 设备属性 ---\n");
printf("name: eth0\n");
printf("mtu: 1500\n");
printf("mac: 00:11:22:33:44:55\n");
printf("ip: 192.168.1.10\n\n");

printf("--- 设备操作 ---\n");
printf("ndo_open: 打开设备\n");
printf("  启用中断、分配资源\n\n");
printf("ndo_stop: 关闭设备\n");
printf("  禁用中断、释放资源\n\n");
printf("ndo_start_xmit: 发送数据包\n");
printf("  把 sk_buff 发送到网卡\n\n");

printf("--- 发送队列 ---\n");
printf("每个设备有发送队列:\n");
printf("  [skb1] → [skb2] → [skb3]\n");
printf("  网卡从队列取数据包发送\n\n");

printf("--- 中断与轮询 ---\n");
printf("传统模式:\n");
printf("  每个数据包触发中断\n");
printf("  高流量时中断风暴\n\n");
printf("NAPI 模式:\n");
printf("  中断触发后切换到轮询\n");
printf("  批量处理数据包\n");
printf("  减少中断开销\n\n");

printf("--- 查看设备 ---\n");
printf("ip link show\n");
printf("  eth0: <BROADCAST,MULTICAST,UP>\n");
printf("    link/ether 00:11:22:33:44:55\n");

道藏笔记

内核启示

每个网络设备都是一个 结构体的实例——eth0、wlan0、lo,不管物理的还是虚拟的,都实现同样的接口。

设备的关键属性就那么几个:设备名、MTU(一次能装的最大货物量,以太网默认 1500 字节)、MAC 地址。操作接口也统一: 打开设备、 关闭设备、 发送数据包。内核不需要知道你的硬件长什么样,只要实现了这些接口就是网络设备。

接收路径上 NAPI 是关键优化:第一个包到达时触发硬件中断,然后禁用网卡中断、调度轮询任务,批量处理数据包。低流量用中断保延迟,高流量用轮询保吞吐——不是非此即彼,而是各取所长。发送路径从协议栈到设备队列到驱动到网卡硬件,每一层只关心自己的事,这就是分层的好处。


破关试炼

网络设备之试

网络设备驱动真正把 skb 交给网卡发送时,本章提到的驱动回调是什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道