Linux内核通知链(notifier chain)原理、实践与避坑指南

发布时间:2026/9/20 23:47:22

Linux内核通知链(notifier chain)原理、实践与避坑指南 在内核里写东西最绕不开的一件事就是“模块之间怎么互相打招呼”。网卡插拔了协议栈要知道IP地址变了路由模块要重新算文件系统挂载成功感知模块要立刻更新状态。如果你在每个事件点上都硬编码调用关系那内核就成了一个巨大的蜘蛛网牵一发动全身。Linux内核给出的标准答案就是通知链notifier chain——一套轻量、同步、基于回调的事件分发机制。这篇文章我从原理讲到写代码再讲到我在真实项目里踩过的坑给准备碰通知链的同学一份能直接抄作业的参考。通知链本身不复杂核心就是一个链表加一个遍历调用但用不好很容易翻车回调里睡眠导致内核崩溃、注册和注销顺序反了导致悬空指针、优先级理解错导致回调不执行——这些坑我在项目里都遇到过。所以这篇文章不只讲它是什么更会告诉你什么时候选哪种通知链、回调里哪些事绝对不能做、调试时从哪里下手。1. 通知链到底在解决什么问题1.1 内核事件分发的困境想象一下这个场景你写了一个安全内核模块需要感知系统里所有网络设备的状态变化设备up了你要启动策略设备down了你要清理状态。如果没有通知链你只能在驱动代码里到处改在以太网驱动、无线驱动、虚拟网卡驱动里分别加你的调用——这显然不现实而且你根本没法维护。再想想文件系统这头。你写了一个透明加密模块正好最近很多人问需要知道某个文件系统什么时候挂载上来、什么时候卸载这样你才能决定对哪个挂载点启用策略。文件系统挂载路径在VFS层你的加密逻辑在文件操作层两者之间怎么通信直接改VFS代码肯定不行。注册一个回调让VFS在挂载/卸载时主动通知你这就对了。这就是内核里典型的“一对多事件分发”问题。一个事件源多个关心该事件的订阅者事件源不应该知道订阅者的存在订阅者也不应该影响事件源的主流程。这个需求用一句话概括就是事件源只负责把事件广播出去谁关心谁自己来登记。1.2 通知链的本质是同步回调订阅表通知链的设计非常朴素本质上就是一张“回调函数注册表”。事件源维护一个链表头订阅者把包含回调函数指针的节点挂到链表上事件发生时事件源遍历这张链表挨个调用注册进来的回调函数。关键点在于“同步”。也就是说事件源调用通知函数后会一直阻塞到所有回调函数返回才能继续干自己的事。这和异步的消息队列完全不同没有缓冲、没有队列、没有并发投递完全是一个紧挨着一个的同步函数调用。这种设计有它的好处实现极其简单事件处理是确定性的回调返回时事件源就知道所有订阅者已经处理完毕。代价就是回调函数不能耗时太长否则会拖慢事件源路径回调函数里不能做任何可能睡眠的事否则在原子上下文里直接崩给你看。用生活中的例子类比通知链就像是业主群里的公告。物业事件源在群里发一条消息调用通知函数所有业主订阅者看到后各自处理回调函数。物业发完消息并不会等所有人回复才去干下一件事——不对其实内核通知链是等的物业发完消息必须等所有业主都回“收到”才能去做下一件事。这个“等待”就是内核里同步通知链和异步机制的底层区别。2. 通知链的底层原理与四种形态2.1 核心数据结构先搞清楚通知链有两个核心结构体一个是回调节点一个是链表头。回调节点是struct notifier_block定义在include/linux/notifier.h里struct notifier_block { notifier_fn_t notifier_call; // 回调函数事件发生时被调用 struct notifier_block __rcu *next; // 链表下一个节点 int priority; // 优先级数值越大越先被调用 };notifier_call是这个结构体的灵魂它的函数签名是int (*notifier_fn_t)(struct notifier_block *nb, unsigned long action, void *data);三个参数分别代表当前回调节点、事件类型、事件附带的数据。action通常是一组枚举值比如NETDEV_UP、NETDEV_DOWNdata则是指向事件相关结构体的指针比如网络设备通知链里的struct net_device *。链表头则是不同类型的结构体常见四种struct atomic_notifier_head { // 原子通知链 spinlock_t lock; struct notifier_block __rcu *head; }; struct blocking_notifier_head { // 阻塞通知链 struct rw_semaphore rwsem; struct notifier_block __rcu *head; }; struct raw_notifier_head { // 原始通知链 struct notifier_block __rcu *head; }; struct srcu_notifier_head { // SRCU通知链 struct mutex mutex; struct srcu_usage srcu; struct srcu_struct ss; struct notifier_block __rcu *head; };注意这几个结构体里都有一个__rcu标记的head指针这意味着遍历链表时使用的是RCU读锁机制。即使链表在并发修改读者也能安全遍历——这是通知链能在高性能路径上存活的关键设计。2.2 注册、通知、注销的完整流程通知链的API分成三组对应三种操作注册、通知、注销。这两组API在链路底层的实现是共用的但会根据通知链类型调用不同的锁机制。注册操作的核心流程是把一个新的notifier_block节点按优先级插入到链表中。内核在kernel/notifier.c里的notifier_chain_register函数干这件事逻辑很直白从头节点开始遍历找到第一个比当前节点优先级低的节点插到它前面。所以链表始终是按优先级从高到低排列的。这里有个初学者很容易理解反的点priority 的值越大越先被调用。priority 1的回调排在priority -1的前面。如果你希望回调最先执行就设一个大的正数希望回调最后执行就设负数或者0。通知操作的核心是notifier_call_chain伪代码如下static int notifier_call_chain(struct notifier_block **nl, unsigned long action, void *data, int nr_to_call, int *nr_calls) { int ret NOTIFY_DONE; struct notifier_block *nb, *next_nb; nb rcu_dereference_raw(*nl); while (nb nr_to_call) { next_nb rcu_dereference_raw(nb-next); ret nb-notifier_call(nb, action, data); if (nr_calls) (*nr_calls); if (ret NOTIFY_STOP_MASK) break; nb next_nb; } return ret; }注意两个细节。第一遍历时用的是rcu_dereference_raw读者不需要加锁就能安全遍历。第二每次调用完一个回调都要检查返回值ret NOTIFY_STOP_MASK。如果回调返回了带停止标志的值就立刻终止遍历后面的回调不再执行。返回值常量语义如下返回值值含义NOTIFY_DONE0x0000已处理但不影响后续回调NOTIFY_OK0x0001已成功处理继续传递NOTIFY_BAD0x0002处理失败终止传递NOTIFY_STOP0x8001已处理停止传递注销操作就是遍历链表找到要注销的节点把它从链表中摘除。这个操作本身也有对应接口。2.3 四种通知链怎么选内核提供四种通知链不是闲得慌而是因为调用时的上下文环境差别巨大。选错了轻则性能受损重则直接死锁或崩溃。通知链类型保护机制回调能否睡眠适用场景原子通知链自旋锁不能中断上下文、软中断、原子上下文阻塞通知链读写信号量能进程上下文、系统调用路径原始通知链无锁由调用者保护取决于调用者调用者自己保证并发安全的高性能路径SRCU通知链SRCU睡眠RCU能受SRCU读锁约束读多写少、注册频繁、回调较慢的场景原子通知链用自旋锁保护回调函数运行在原子上下文里绝对不能睡眠。在中断处理函数、软中断、自旋锁持有期间必须用这种类型。初始化宏是ATOMIC_NOTIFIER_HEAD(name)注册用atomic_notifier_chain_register通知用atomic_notifier_call_chain。阻塞通知链用读写信号量rwsem保护。读锁保护遍历与回调执行写锁保护注册与注销。因为持有的是读锁多个回调可以并发执行而且回调函数里可以睡眠——但要注意如果你注册或注销通知链时持有读锁这时候会获取写锁同一个链上注册和通知会形成互斥。初始化宏是BLOCKING_NOTIFIER_HEAD(name)对应接口是blocking_notifier_chain_register和blocking_notifier_call_chain。原始通知链不提供任何锁保护纯粹是一个链表加遍历操作。调用者必须自己保证并发安全适合对性能极其敏感、且调用路径已经保证串行的场景。比如某些驱动在设备模型里已经加了锁就不需要通知链重复加锁。SRCU通知链用的睡眠RCU读者在通知时会占一个SRCU读锁允许回调睡眠。相比阻塞通知链它的优势是注册和注销不会被通知操作阻塞太久适合读多写少且写操作可能被调用的场景。但使用起来更麻烦需要先srcu_init_notifier_head初始化通知时还要传 SRCU 下标。**我的建议是拿不准的时候就上阻塞通知链除非你的回调明确运行在中断上下文。**阻塞通知链覆盖了90%的模块间通信需求而且语义最好理解。3. 实操写一个可运行的通知链模块这一节我们动手写代码。我用一个模拟文件访问事件的场景实现一个自定义通知链一个模块作为事件源负责广播文件打开/关闭事件另一个模块作为订阅者接收事件并打印日志。这个模型和透明加密模块的联动架构很像后续扩展思路我会在第4节细说。3.1 事件源模块事件源模块要做三件事定义一个通知链表头、提供一个广播函数并导出、提供注册接口给其他模块调用。// file_event_source.c #include linux/module.h #include linux/init.h #include linux/notifier.h #include linux/fs.h // 定义事件类型从某个起始值开始避免和内核已有枚举冲突 #define FILE_OP_OPEN 0x0001 #define FILE_OP_CLOSE 0x0002 // 定义一个阻塞通知链表头 static BLOCKING_NOTIFIER_HEAD(file_op_chain); // 广播函数文件打开/关闭时调用 void file_op_notify(unsigned long action, struct file *filp) { struct file_op_event evt; evt.filp filp; evt.pid current-pid; evt.ts_nsec ktime_get_real_ns(); // 遍历链表逐个调用订阅者的回调 blocking_notifier_call_chain(file_op_chain, action, evt); } EXPORT_SYMBOL(file_op_notify); // 注册接口供其他模块挂载回调 int file_op_register(struct notifier_block *nb) { return blocking_notifier_chain_register(file_op_chain, nb); } EXPORT_SYMBOL(file_op_register); // 注销接口 int file_op_unregister(struct notifier_block *nb) { return blocking_notifier_chain_unregister(file_op_chain, nb); } EXPORT_SYMBOL(file_op_unregister); static int __init file_event_source_init(void) { pr_info(file_event_source: 事件源模块已加载\n); return 0; } static void __exit file_event_source_exit(void) { pr_info(file_event_source: 事件源模块已卸载\n); } module_init(file_event_source_init); module_exit(file_event_source_exit); MODULE_LICENSE(GPL);这个模块本身不创造事件它只是提供了广播渠道。真正触发事件的场景是某个被hook的文件操作路径里调用file_op_notify()。比如你用kprobe或者替换file_operations的方式截获了read/write在进入点调一下file_op_notify(FILE_OP_OPEN, filp)事件就分发下去了。这就是通知链和热词里拦截read/write的天然组合方式。3.2 订阅者模块订阅者模块负责注册一个notifier_block等到事件发生时做自己的处理。// file_op_subscriber.c #include linux/module.h #include linux/init.h #include linux/notifier.h #include linux/fs.h struct file_op_event { struct file *filp; pid_t pid; u64 ts_nsec; }; static int file_op_callback(struct notifier_block *nb, unsigned long action, void *data) { struct file_op_event *evt data; if (action FILE_OP_OPEN) pr_info(subscriber: 进程%d 打开文件 0x%px\n, evt-pid, evt-filp); else if (action FILE_OP_CLOSE) pr_info(subscriber: 进程%d 关闭文件 0x%px\n, evt-pid, evt-filp); return NOTIFY_DONE; } static struct notifier_block file_op_nb { .notifier_call file_op_callback, .priority 0, }; static int __init file_op_subscriber_init(void) { int ret file_op_register(file_op_nb); if (ret) { pr_err(subscriber: 注册通知链失败, ret%d\n, ret); return ret; } pr_info(subscriber: 已注册到文件事件通知链\n); return 0; } static void __exit file_op_subscriber_exit(void) { int ret file_op_unregister(file_op_nb); if (ret) pr_err(subscriber: 注销通知链失败, ret%d\n, ret); pr_info(subscriber: 已从文件事件通知链注销\n); } module_init(file_op_subscriber_init); module_exit(file_op_subscriber_exit); MODULE_LICENSE(GPL);注意几个细节。第一struct notifier_block可以静态初始化并且priority在这里设置为0。如果两个订阅者都想设优先级注册时会按数值降序排列。第二回调函数的data参数是指向struct file_op_event的指针这是事件源和订阅者之间的隐式协议。事件源负责构造并填充数据订阅者负责按约定解析两边必须保持一致否则类型强转后就是乱读内存。第三回调返回NOTIFY_DONE不影响同一链表上的其他订阅者。如果你的逻辑是我处理完这件事你们不用管了就返回NOTIFY_STOP。但除非你能保证后续订阅者确实不需要被通知否则慎用。3.3 Makefile与编译验证写Makefile之前你要有一份对应版本的内核头文件。最稳妥的方式是直接用当前运行内核的头文件编译成外部模块obj-m file_event_source.o obj-m file_op_subscriber.o KERNELDIR ? /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) clean编译并加载make sudo insmod file_event_source.ko sudo insmod file_op_subscriber.ko dmesg | tail -20正常情况你会看到两行日志file_event_source: 事件源模块已加载 subscriber: 已注册到文件事件通知链现在触发一个事件。因为我们的示例里还没有真正挂钩某个读文件路径你可以用一个小技巧手动触发随便cat一个文件然后在dmesg里看看有没有回调日志——正常情况下你什么都看不到因为没有任何代码调用file_op_notify()。这就引出一个关键点通知链只是渠道它本身不产生事件。你要在合适的时机调用file_op_notify()才能让事件流起来。在真实项目里这一步通常和kprobe、fops替换、LSM钩子等配合使用。为了快速验证效果我通常会临时加一个测试触发点比如在模块里用proc_create暴露一个/proc/test_trigger写一次这个文件就调用一次file_op_notify()。这比反复猜为什么没触发要高效很多。3.4 顺手验证一下优先级再看一个我特别强调过的点priority数值越大越先被调用。我们改一下订阅者模块注册两个不同的notifier_block一个priority 10一个priority -10static int file_op_callback_high(struct notifier_block *nb, unsigned long action, void *data) { pr_info(subscriber: 高优先级回调执行\n); return NOTIFY_DONE; } static int file_op_callback_low(struct notifier_block *nb, unsigned long action, void *data) { pr_info(subscriber: 低优先级回调执行\n); return NOTIFY_DONE; } static struct notifier_block high_nb { .notifier_call file_op_callback_high, .priority 10, }; static struct notifier_block low_nb { .notifier_call file_op_callback_low, .priority -10, };注册时先注册低优先级再注册高优先级触发事件后观察日志顺序你会发现高优先级始终先执行。这是因为notifier_chain_register内部会按优先级重新排序和你注册的先后顺序无关。这个排序逻辑在notifier_chain_register里是拿nl-head开始从头遍历找到合适位置插入的所以是稳定的插入排序。4. 内核子系统里那些真实的通知链用法4.1 网络子系统最典型的通知链大户整个网络子系统是通知链用得最频繁的地方。你几乎可以在任何网络相关的内核模块里看到register_netdevice_notifier它注册的链表就是处理网络设备事件的事件类型包括设备注册、注销、up、down、地址变化、MTU改变、特性变更等。内核里有一个全局的netdev_chain所有关心网络设备事件的模块都往这个链上挂回调。比如一个虚拟化网络模块需要感知物理网卡的up/down事件以便同步调整虚拟网卡的链路状态一个防火墙模块需要感知网卡IP地址变化以便更新NAT规则。这些模块互不知晓但通过同一个通知链协同工作。再往下细分网络子系统还有几个独立的链inetaddr_chainIPv4地址变更事件接口是register_inetaddr_notifier。inet6addr_chainIPv6地址变更事件。netdev_chain网络设备生命周期事件接口是register_netdevice_notifier。fib_notifier_chain路由表变更事件用于感知FIB表变化。我在一个项目里用register_netdevice_notifier监控网卡插拔回调里拿到了struct net_device *通过netdev_notifier_info_to_dev从data参数里解析出设备指针再通过dev-name判断是哪个接口。这个接口在较新的内核版本里已经封好了不用你自己用container_of去转换。4.2 文件系统与块设备register_filesystem的对应场景热词里提到了register_filesystem这确实和通知链有很强的关联。文件系统子系统本身有file_systems链表通过register_filesystem把struct file_system_type挂上去。如果你要感知文件系统注册/注销可以直接遍历这个链表但如果想收到变化的通知就得靠事件机制。内核中有一个专门的super_blocks通知链但是更多时候你关心的是文件系统挂载/卸载这种高级事件。由于VFS层没有直接暴露挂载事件的通用通知链很多项目会自己埋点或者使用fsnotify机制。这正是我在第3节里示例的场景来源——自己定义一个链在自己需要感知事件的位置调用广播函数。块设备子系统也有自己的通知链register_blkdev相关的块设备事件、memory_notifier内存热插拔事件。在透明加密场景里你通常需要感知块设备的插入和移除因为加密盘挂载和卸载时密钥管理和策略启用都要联动。4.3 重启、内存、键管理等其他应用除了网络和文件系统通知链在内核里可以说是无处不在重启通知链register_reboot_notifier内核重启或关机时遍历回调模块可以在里面做最后的清理工作比如落盘关键数据、通知用户态守护进程。内存热插拔register_memory_notifier物理内存插入或移除时通知内存热管理模块用它调整水位线。键管理register_key_type_notifier内核密钥类型注册和注销时通知安全模块用它感知密钥类型变化。电源管理PM子系统也有自己的通知机制用于休眠/唤醒时的事件分发。回顾一下热词里提到的linux内核hook write加密这类项目通常涉及多个内核机制的组合用kprobe或fops替换来拦截read/write用通知链来感知挂载点变化和设备插拔用内核同步机制互斥锁、RCU来保护关键数据结构。通知链在这些方案里扮演的是事件总线的角色它不直接参与数据加解密但负责协调整个模块的事件联动。4.4 和透明加密/文件拦截类模块怎么配合很多人在做透明加密或者文件内容过滤时第一反应是只要替换file_operations里的read/write就够了。但实际上线后你会发现光有read/write拦截远远不够文件系统挂载时你需要知道挂载点、文件系统类型、挂载选项才能决定是否对该挂载点启用加密策略。设备插拔时加密盘的块设备节点发生变化需要重新绑定策略。进程启动或退出时你维护的进程-密钥映射表需要增删。这几类需求天然适合用通知链来解决。你可以这样设计用register_netdevice_notifier感知网络设备变化如果加密模块还涉及网络流量。自建一个挂载点通知链在VFS挂载路径上埋点广播挂载/卸载事件。用register_reboot_notifier做退出前的策略清理。我实际做过的架构里通知链模块负责事件采集fops hook负责数据路径两者之间通过一个全局状态表交互。事件到达时通知链回调里只做状态更新不做耗时操作read/write路径里查状态表决定是否加解密。这样的分层让每个模块都足够简单也方便单独调试。5. 避坑指南与调试技巧5.1 我踩过的几个大坑第一坑在原子通知链回调里睡眠。这是我见过最多的问题也是内核崩溃最常见的原因之一。原子通知链的回调运行在自旋锁保护区域内你一旦调用msleep、mutex_lock、kmalloc(GFP_KERNEL)这种可能睡眠的操作内核会直接报 scheduling while atomic 然后崩溃。解决方法是要么改用阻塞通知链要么在回调里只做轻量操作把耗时工作推迟到工作队列里。第二坑注销顺序反了导致悬空指针。模块B注册到模块A的通知链上rmmod的时候必须先卸载B再卸载A。如果先把A卸载了A的链表头被释放B的回调节点还留在那块内存里后续任何事件触发都会访问已释放的内存。更隐蔽的情况是B卸载时调用了blocking_notifier_chain_unregister但由于优先级排序问题它以为注销成功了实际上节点还挂在链上。这类问题很难复现因为内存被释放后可能不会立刻报错。我建议在module_exit里第一行就注销通知链并且校验返回值注销失败时打印日志排查。第三坑注册前回调数据未初始化。事件源在注册完成后可能立刻广播事件尤其是priority较高的回调可能在blocking_notifier_chain_register返回之前就被触发了如果事件恰好发生在注册过程中。所以你的回调使用到的所有数据结构必须在注册之前就初始化好注册后再初始化就是竞态。第四坑在阻塞通知链的回调里再次注册/注销同一个链。阻塞通知链调用回调时持有读锁如果你在回调里调用了注册接口它需要申请写锁而写锁等待读锁释放读锁等着回调返回形成自死锁。表现为内核卡死dmesg没有任何输出就像死机一样。解决方法是把注册/注销操作推迟到工作队列或者用srcu_notifier_chain_register它的注册和通知互斥性较弱但也别在回调里做。第五坑混淆NOTIFY_STOP和NOTIFY_BAD。NOTIFY_STOP的语义是这个事件我处理完了你们不用再处理了常用于最后一个处理者NOTIFY_BAD的语义是我处理失败了这是一个错误请停止整个链。在代码里看起来都是终止遍历但语义完全不同。事件源会检查返回值来决定是否需要回滚你返回错误的值可能导致事件源误判。第六坑对通知链加锁理解错误。很多人以为通知链内部完全线程安全其实不是。原子通知链保证并发安全阻塞通知链的注册/注销/通知之间互斥但同一个事件源多次调用blocking_notifier_call_chain是并发执行的因为每次调用只持有读锁。如果你在blocking链的回调里操作了共享数据结构还是需要自己的锁。5.2 调试工具与日志技巧日志先行。最简单有效的调试方式是在回调函数入口和出口加pr_info打印action和data关键字段static int my_callback(struct notifier_block *nb, unsigned long action, void *data) { pr_info(my_notifier: action0x%lx, data%px\n, action, data); int ret do_real_work(action, data); pr_info(my_notifier: ret%d\n, ret); return ret; }但注意日志本身是有开销的在原子上下文里频繁打印会让系统变慢甚至丢中断。生产环境建议用trace_printk或者编译内核时开启CONFIG_DYNAMIC_DEBUG。ftrace查看调用路径。把notifier_call_chain放在ftrace的跟踪列表里可以看到每次触发通知链的完整调用栈这对定位谁在什么上下文里触发了事件非常有用echo notifier_call_chain /sys/kernel/tracing/set_ftrace_filter echo function /sys/kernel/tracing/current_tracer echo 1 /sys/kernel/tracing/tracing_on cat /sys/kernel/tracing/trace这样你能清楚地看到通知链上每个回调函数的执行顺序和耗时。用crash工具遍历链表。如果手上有crash工具可以直接查看某个通知链表头上的所有节点crash struct blocking_notifier_head 0xffffffff820a4a00 crash struct notifier_block 0xffff888012345678这样能确认某个订阅者是否真的注册上去了以及它的priority和notifier_call指针是否正常。5.3 性能与设计建议通知链是同步调用所以性能问题本质上是回调函数有多快的问题。一个几十微秒的回调在事件较少时没什么感觉但如果事件频率高比如网络设备每秒钟收包都触发一次事件回调里的每一条指令都会被放大。我建议在设计通知链回调时遵守几个原则回调里只做必要的事。能放到工作队列的就放到工作队列能延迟处理的信息先记到缓存里再异步落盘。优先使用阻塞通知链而不是原子通知链除非明确需要原子上下文。阻塞链允许回调里有更多操作也更容易维护。不要在回调里调用其他模块的导出函数时假设对方一定存在。模块加载顺序不确定回调里调用一个不存在的符号会导致kernel panic。正确做法是模块加载时获取函数指针比如用symbol_get并且检查返回值。尽量避免在一个事件里注册太多高优先级回调。每个高优先级回调都会增加该事件路径的延迟影响所有订阅者。另外如果你要为一个高频事件设计自己的通知链可以考虑使用srcu_notifier_head它允许读操作并行写操作的延迟也更可控。代价是使用复杂度上升要维护一个SRCU下标回调里如果睡眠要小心与注销操作竞态。6. 一些顺手的小技巧最后分享几个我在项目里积累的小经验不一定写进文档但很实用。第一写通知链回调时尽量用NOTIFY_DONE作为默认返回值只有你确定这个事件我来收尾时才返回NOTIFY_STOP。很多隐蔽的问题都是返回值语义理解错了导致链路后面的模块收不到事件。第二注册回调时给notifier_block起一个有辨识度的变量名或者用container_of把它嵌到你的模块私有结构体里。这样在crash工具里看到notifier_block节点时能快速定位到你的模块和上下文。我就是用一个私有结构体包裹notifier_block把模块状态、锁、统计计数都放一起回调里一个container_of全拿回来。第三如果你同时在多个通知链上注册了回调建议在回调函数入口统一做一个事件路由按action分发给不同处理函数而不是每个回调各写一套。这样日志、统计、错误处理都可以集中管理排查问题时不用在多个文件之间跳来跳去。第四模块卸载时注销通知链后最好加一个synchronize_rcu()或者至少synchronize_rcu()等待确保正在执行的回调都退出了再释放模块里被回调引用的资源。虽然大多数通知链框架已经做了这层保障但在自定义链上调用者通常需要自己处理这个语义。写通知链代码这几年我最大的体会是它真的不难但你真的要尊重它的执行上下文。每个看似不起眼的约束——回调里能不能睡眠、返回值应该是什么、注册顺序必须怎样——背后都是内核开发者踩过无数次坑之后总结出来的规则。理解了这些规则通知链就能成为你手里非常顺手的一件工具帮你在模块之间织出一张干净、解耦的事件网络。
延伸阅读

更多相关文章

2026/9/20 23:42:22

使用 ncnn CMake 选项裁剪出最小化推理库:完整瘦身指南

使用 ncnn CMake 选项裁剪出最小化推理库:完整瘦身指南 【免费下载链接】ncnn ncnn is a high-performance neural network inference framework optimized for the mobile platform 项目地址: https://gitcode.com/gh_mirrors/nc/ncnn 如果你的目标是让 ncnn…

2026/9/20 23:42:22

校园二手交易系统概要设计:从架构到数据库的完整实践指南

简介:面向软件工程专业学生、毕业设计开发者及需要规范化文档支撑的校园项目团队,这份校园二手交易系统概要设计说明书以真实场景为依托,系统解决从需求确认到详细设计之间的蓝图缺失问题。文档遵循标准概要设计规范,完整涵盖编写…

2026/9/21 0:47:25

RAG技术优化:检索增强生成系统的关键策略与实践

1. RAG技术体系概述检索增强生成(Retrieval-Augmented Generation)作为当前NLP领域的前沿技术,通过将信息检索与文本生成相结合,有效解决了传统大语言模型的知识固化问题。我在实际项目中发现,标准的RAG流程通常包含四…

2026/9/21 0:47:25

Claude Code 桌面版接入 DeepSeek 与离线 Skills 安装全攻略

1. 为什么我要折腾这套组合:Claude Code 桌面版 DeepSeek 离线 Skills先说清楚这套东西到底是什么。Claude Code 是 Anthropic 推出的一个命令行 AI 编程助手,它跟普通聊天式 AI 最大的区别在于:它能直接读写你本地的项目文件、执行终端命令…

2026/9/21 0:47:25

QGIS等时圈分析实战:ORS插件Key申请与参数设置避坑指南

1. 等时圈分析与ORS插件到底在做什么等时圈分析这件事,说白了就是回答一个很朴素的问题:从某个点出发,在给定时间内,我到底能走到哪些地方。做城市规划的要拿它评估公共服务覆盖范围,做商业选址的要拿它算门店辐射半径…

2026/9/21 0:47:25

普通人用AI变现,第一个工具到底该怎么选?

我见过太多人,一听说AI能变现,第一反应就是到处问:现在哪个AI工具最强?哪个能不限次数白嫖?哪个生成的内容最像真人?然后就开始了一场漫长的工具测评之旅。各种官网、教程、对比帖收藏了上百篇,…

2026/9/21 0:47:25

JDK 17.0.8免安装版Windows配置指南:从下载到环境变量

简介:JDK 17.0.8 Windows免安装版为Java开发者提供开箱即用的开发环境,无需经过复杂安装流程,解压配置环境变量即可使用。作为长期支持(LTS)版本,它包含javac编译器、Java运行环境、javadoc文档生成器、jdb…

2026/9/21 0:42:24

Xilinx 7系列FPGA入门:从选型架构到时序约束实战要点

简介:面向FPGA初学者与嵌入式开发者的Xilinx 7系列FPGA入门介绍文档,以简明方式梳理系列整体定位与核心技术要点。内容涵盖Spartan-7、Artix-7、Kintex-7、Virtex-7四个子系列的适用场景、性能参数与功耗优势,详细对比单位功耗性价比、成本削…

2026/9/20 0:04:49

GAMP 5 基于风险的计算机化系统验证:软件分类与审计追踪实践

简介:《A Risk-Based Approach to Compliant GxP Computerized Systems》即业内熟知的GAMP 5指南,面向制药企业质量与IT合规人员、验证工程师及计算机化系统管理者,用于解决GxP法规环境下系统合规性难以科学落地的问题。文档以风险管理为主线…

2026/9/20 0:04:49

安全托管MSSP实战:从静态防御到人机协同的攻防运营与应急响应

简介:这份PPT围绕互联网业务安全托管服务展开,面向企业安全负责人、IT运维人员及关注MSSP/MSS选型的读者,重点回应传统安全过度依赖人工、碎片化静态防御难以对抗产业化攻击等痛点。资源共1个pptx文件,包体约30.63MB,以…

2026/9/21 0:02:23

OpenResearch:构建可复现的开放式研究工作流

第一次看到“OpenResearch”这个名字,我脑子里冒出的不是某个具体软件,而更像一种研究方式的宣言:开放、可复现、可验证。这三件事放在一起,其实比大多数人想象中难得多。过去几年我一直在折腾自己的研究工作流,从纯纸…

2026/9/20 4:54:47

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/20 5:01:23

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/20 5:09:33

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码