14.10 通知链
上一节我们讨论了 NFC 这种「握手协议」,内核在其中扮演了精巧的翻译官角色。但内核不仅要在硬件之间翻译,还得时刻监听整个系统的状态变化。
网络世界不是静止的。网线被拔掉、MTU 被修改、MAC 地址被重写、设备被卸载——这些事情每时每刻都在发生。当这些事件发生时,网络协议栈和其他相关子系统必须立刻知情,否则路由表还在发往旧地址,ARP 缓存还在查死掉的端口,整个网络逻辑就会瞬间崩塌。
这需要一个神经系统。
旧方案的沉默
在早期设计中,处理这种状态变化是非常痛苦的。如果子系统 A 想知道子系统 B 里的设备是否存活,通常只有两种办法:轮询,或者强行修改子系统 B 的代码,塞入一堆 A 的逻辑。
轮询太蠢且慢,强行耦合代码则会让架构变成一团乱麻——ARP 模块不应该关心 VLAN 模块的内部实现,防火墙也不应该因为网卡的驱动代码变了而跟着改。
我们需要一种机制,让子系统可以「订阅」它感兴趣的事件,当事件发生时,内核负责把消息广播出去。这就是通知链。
通知链的核心机制
你可以把通知链理解为内核里的「发布-订阅」系统——就像报纸订阅一样。
但「订阅报纸」这个比喻有一个地方是错的:报纸投递员通常不管你有没有读完,也不管投递过程中会不会有别人半路插入,而内核的通知链需要精确控制执行顺序、并发安全和优先级。
回到那个「报纸」:notifier_block
在内核里,每个「订阅者」都需要填写一张订阅单,这张单子就是 notifier_block 结构体:
struct notifier_block {
int (*notifier_call)(struct notifier_block *, unsigned long, void *);
struct notifier_block __rcu *next;
int priority;
};
这张单子上只有三个关键信息:
notifier_call:这是回调函数指针。当事件发生时,内核就会拨打这个电话。next:指向下一个订阅者的指针。所有的订阅单会被串成一条链表。priority:优先级。数字越大,越先被通知。这很重要——有些模块必须先于别人做出反应(比如先关闭硬件,再通知上层断开连接)。
链条的四张面孔
内核并不是只有一种通知链。根据使用场景的不同(是否允许睡眠、是否处于原子上下文、是否有锁保护),通知链被分成了四种「口味」。
虽然底层都依赖 kernel/notifier.c 里的核心逻辑,但我们在使用时会看到不同的封装接口:
atomic_notifier_chain_register():用于原子上下文。它在执行回调时禁用中断(或使用自旋锁),极其严格,回调函数里绝对不能睡眠。blocking_notifier_chain_register():用于进程上下文。它持有互斥锁,允许回调函数睡眠。raw_notifier_chain_register():最原始、最宽松的版本。它没有任何锁保护。这通常用于那些已经自己管理了锁机制的子系统(比如网络子系统)。srcu_notifier_chain_register():使用 SRCU(Sleepable Read-Copy Update)机制,兼顾了高性能和允许睡眠的特性。
网络子系统主要使用的是 raw_notifier_chain。为什么?因为网络代码路径极其复杂,有些情况已经在锁里了,有些情况则不能睡眠,使用 Raw 链可以让网络子系统自己决定怎么加锁,而不是被通用锁机制锁死。
网络事件清单:一张详尽的诊断书
当一个网络设备发生变化时,内核会发出特定的事件代码。表 14-1 列出了所有可能的事件。这不仅仅是一张表,它是网络设备生命周期的一张诊断书。
我们可以把这些事件分成几类来看:
1. 生死存亡
最基础的状态改变。
NETDEV_UP/NETDEV_DOWN:设备被管理员激活或关闭。NETDEV_REGISTER/NETDEV_UNREGISTER:设备内核对象的注册与注销。- 这里有一个微妙的时间差问题:
NETDEV_UNREGISTER发生时,设备还在;而NETDEV_UNREGISTER_FINAL发生时,那是真正的最后一口气,设备彻底要释放内存了。如果你在处理UNREGISTER时还要访问设备结构体,千万别等到FINAL。
- 这里有一个微妙的时间差问题:
NETDEV_POST_INIT:设备注册过程中,但在创建 sysfs 等对象之前。这是一个很早期的初始化钩子。
2. 属性变更
设备还在,但它的「样子」变了。
NETDEV_CHANGEMTU:最大传输单元变了。这会导致路由表重新计算,因为分片策略可能全变了。NETDEV_CHANGEADDR:MAC 地址变了。NETDEV_CHANGENAME:设备名变了(比如从eth0改成了lan0)。NETDEV_FEAT_CHANGE:硬件特性变了(比如通过 ethtool 关闭了 TSO/GSO)。
3. 特殊场景的开关
这些事件通常是为了在动作发生前做最后一次检查,或者为了处理特殊的虚拟设备逻辑。
NETDEV_PRE_UP:设备即将 UP,但还没 UP。这是一票否决的机会。- 这一点非常关键。比如 WiFi 驱动 (
cfg80211) 会检查:如果硬件开关被物理关闭了,或者被 rfkill 杀掉了,它就会在这里返回错误,拒绝设备启动。如果等到NETDEV_UP再拒绝,状态机就乱了。
- 这一点非常关键。比如 WiFi 驱动 (
NETDEV_GOING_DOWN:设备即将关闭。这是个「清理现场」的最后机会。NETDEV_PRE_TYPE_CHANGE/NETDEV_POST_TYPE_CHANGE:设备类型即将改变/已经改变。这通常用于 Bonding 或 Team 驱动,当一个设备从普通口变成 Bond 的从口时。
4. 聚合与迁移
这部分是虚拟化和高可用的战场。
NETDEV_BONDING_FAILOVER:Bond 驱动发生了主备切换。数据流瞬间从一条线切到了另一条线。NETDEV_NOTIFY_PEERS:通知邻居。通常用于虚拟机迁移或故障切换后,设备主动向网络喊话:「我不在那个老地方了,我现在在这里,ARP 表请更新!」NETDEV_JOIN:有新设备加入(成为从设备)。
实战演练:接入 netdev_chain
光看事件表很枯燥,我们来看一个真实的案例。Linux 的网桥模块是如何监听网络设备变化的?
当一块网卡被加入到网桥时,如果有人改了这块网卡的 MTU,网桥的 MTU 也必须跟着变(取所有从设备的最小值)。怎么做到的?靠 netdev_chain。
第一步:定义订阅单
网桥模块在 net/bridge/br_notify.c 里定义了自己的 notifier_block:
struct notifier_block br_device_notifier = {
.notifier_call = br_device_event
};
没有复杂的初始化,直接填上回调函数地址。
第二步:去前台登记
在模块初始化时,把它注册到内核的通知链上。这里用的是网络子系统专用的封装接口 register_netdevice_notifier()(它本质上是 raw_notifier_chain_register() 的包装):
static int __init br_init(void)
{
...
register_netdevice_notifier(&br_device_notifier);
...
}
一旦这行代码执行成功,网桥模块的耳朵就竖起来了。
第三步:处理来电
当网络设备发生任何事件时,内核都会调用 call_netdevice_notifiers(),进而触发 br_device_event()。
注意这个函数签名,它是所有通知回调的标准模板:
static int br_device_event(struct notifier_block *unused, unsigned long event, void *ptr)
{
struct net_device *dev = ptr;
struct net_bridge_port *p;
struct net_bridge *br;
bool changed_addr;
int err;
. . .
unused:就是我们刚才定义的notifier_block本身(有时候一个 block 管多个回调,这里虽然叫 unused,但在复杂场景下可能有用)。event:就是表 14-1 里的那些宏(比如NETDEV_CHANGEMTU)。ptr:指向产生事件的设备(struct net_device)。
回调函数内部通常是一个巨大的 switch 语句,用来筛选它关心的事件:
switch (event) {
case NETDEV_CHANGEMTU:
dev_set_mtu(br->dev, br_min_mtu(br));
break;
. . .
}
这就是处理逻辑的核心:我不关心是谁发的通知,我只关心发生了什么事。 如果是 NETDEV_CHANGEMTU,我就重新计算一下整个桥的最小 MTU,并设置主桥设备;如果是 NETDEV_DOWN,我可能要把对应的端口从桥里移除。
还有别的链吗?
除了网络设备链 (netdev_chain),网络子系统里还有几条重要的支线:
inet6addr_chain:专门监听 IPv6 地址的变化。如果添加或删除了一个 IPv6 地址,这条链会通知所有订阅者。netevent_notif_chain:处理更底层的网络事件,比如邻居表项的变化(FIB 更新)。inetaddr_chain:对应 IPv4 地址的通知链。
甚至网络之外,时钟事件子系统 (clockevents_chain)、甚至系统崩溃时的告警子系统 (panic_notifier_list) 都在使用这套机制。这证明了通知链设计的高度通用性。
总结
本节我们看完了内核的「通知链」机制。
从技术角度看,它是一套基于回调链表的分发器;但从架构角度看,它是解耦的基石。它让内核的开发者可以放心地编写模块化代码,而不用担心「改了 A 却忘了通知 B」这种低级错误。
通过 notifier_block,我们将事件的产生(网络设备状态变化)和事件的处理(路由更新、防火墙刷新、桥接逻辑重组)彻底分离开来。
下一节,我们将视线从软件逻辑拉回到硬件总线,去看看支撑网卡(尤其是 PCIe 网卡)工作的底座——PCI 子系统。我们会看到内核是如何枚举设备、分配资源,以及实现像 Wake-on-LAN 这种神奇功能的。