第一百一十二章:港口
问道期涉及内核源码:
一
林小源沿着海岸线走了很远,看到了一座真正的港口。
不是码头边那种简陋的泊位,而是一座巨大的、钢铁铸成的港口。港口的大门上挂着一块铜牌:。旁边还有几座稍小的港口——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 的灯安静地发着柔和的光。
"每个设备都不一样,但接口是一样的。" 管理员说,"、、——所有驱动都实现这些回调。内核不需要知道你的硬件长什么样,只需要你提供这些接口。这就是 的设计哲学。"
"不管硬件长什么样,接口都一样——这才是 的设计精髓。" 管理员最后补了一句。
/*
* 网络设备 (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");#include <stdio.h>
/*
* 网络设备 (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 — 虚拟设备
*/
int main() {
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");
return 0;
}道藏笔记
内核启示
每个网络设备都是一个 结构体的实例——eth0、wlan0、lo,不管物理的还是虚拟的,都实现同样的接口。
设备的关键属性就那么几个:设备名、MTU(一次能装的最大货物量,以太网默认 1500 字节)、MAC 地址。操作接口也统一: 打开设备、 关闭设备、 发送数据包。内核不需要知道你的硬件长什么样,只要实现了这些接口就是网络设备。
接收路径上 NAPI 是关键优化:第一个包到达时触发硬件中断,然后禁用网卡中断、调度轮询任务,批量处理数据包。低流量用中断保延迟,高流量用轮询保吞吐——不是非此即彼,而是各取所长。发送路径从协议栈到设备队列到驱动到网卡硬件,每一层只关心自己的事,这就是分层的好处。
网络设备之试
网络设备驱动真正把 skb 交给网卡发送时,本章提到的驱动回调是什么?