ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Linux内核通知链机制深度解析:事件驱动与模块解耦实战

Linux内核通知链机制深度解析:事件驱动与模块解耦实战 1. 项目概述与核心设计思路1.1 为什么需要通知链Linux内核里的“广播机制”很多人第一次看到“通知链”这三个字脑子里蹦出来的是“链表”。确实从数据结构上看它就是一个链表但在内核里它承担的是一套完整的事件订阅与分发机制。我先抛个具体场景。你写了一个网卡驱动用户拿ip link set eth0 up把网口拉起来此时内核网络协议栈里那帮家伙——路由表、邻居子系统、防火墙过滤规则——全都想知道“网卡起来了”这件事然后做各自的处理路由表要刷新一下邻居缓存要清掉失效条目netfilter可能要根据新的链路状态调整策略。问题是网卡驱动总不能挨个去调用路由模块、邻居模块、netfilter模块的函数吧它压根不知道这些模块存不存在、注册了没有如果硬编码依赖关系驱动代码会变成一团乱麻。通知链就是来解决这个“我发生了某件事但我不想管谁关心这件事”的痛点。它的本质很朴素内核维护一张回调函数链表事件发生时事件源把链路上每个节点的回调函数依次调一遍通知它们“事情发生了”。关心这个事件的模块自己往链上挂节点不关心的什么都不用做。这就是Linux内核里标准的生产者-消费者解耦模型。我在实际驱动开发中凡是遇到“某个状态变化需要通知多个子系统”这种需求第一反应就是去找现成的通知链而不是自己造一套回调管理逻辑。原因不只是懒而是内核里已经沉淀了大量成熟的通知链实例——网络子系统的netdev_chain、内存热插拔的memory_chain、电源管理的reboot_chain——直接复用它们的事件类型和调用约定比自己另起炉灶要稳妥得多也更容易被上游社区接受。1.2 通知链解决什么问题它适合谁通知链解决的是一类非常典型的问题单点事件、多点响应、且响应方动态可变。举几个内核里实实在在的例子网络设备状态变化up/down、地址变更、MTU调整要通知所有关心网络拓扑的子系统内存热插拔事件要通知页面迁移、内存cgroup、设备驱动等模块系统关机或重启时要通知所有注册了重启回调的驱动做收尾工作文件系统注册/注销时要通知所有使用该文件系统的模块如果你是做Linux内核开发、驱动开发、嵌入式系统移植或者正在啃内核源码看书通知链几乎是绕不开的一块内容。面试里被问“内核里模块之间如何通信”“网卡状态怎么通知协议栈”答案的核心就是通知链。尤其是做嵌入式方向的朋友经常要裁剪内核、移植驱动搞懂通知链能帮你少走很多弯路。这篇文章不打算只讲理论。我会从数据结构拆到源码执行路径再给一份可以拿来即用的完整示例代码最后把我在实际项目中踩过、见过的坑全部倒出来——有些坑光看内核文档和源码注释是根本发现不了的。2. 通知链核心机制深度拆解2.1 数据结构先把那根“链”看清楚通知链最底层的节点定义是这个struct notifier_block { int (*notifier_call)(struct notifier_block *nb, unsigned long action, void *data); struct notifier_block *next; int priority; };三个成员各司其职。notifier_call是回调函数指针事件发生时被调用next把多个通知块串成链表priority是优先级数值大的节点排在链的前面优先被调用。但内核的思考显然比这个更细。细心的读者可能发现上面这个结构体里没有锁。因为通知链在不同场景下的并发模型差别太大了有的在原子上下文被调用连信号量都不能用有的在进程上下文被调用可以舒舒服服地睡一觉等待锁。把锁写死在结构体里反而限制了使用场景。于是内核定义了四种通知链类型对应四种并发处理策略类型头结构加锁方式回调上下文典型场景原子通知链atomic_notifier_head自旋锁原子上下文中断/软中断内核崩溃、panic通知可阻塞通知链blocking_notifier_head信号量rwsem进程上下文网络设备通知、内存热插拔SRCU通知链srcu_notifier_headSRCU锁进程上下文但读端开销低需要频繁读取通知链的场景原始通知链raw_notifier_head无锁由调用者自己保证取决于调用者网络收包路径等极热路径抢锁逻辑用一个生活化的类比来理解原子通知链就像在急诊室里喊医生大家都不敢熄灯睡觉怕错过什么所以用最短的锁自旋锁快速处理可阻塞通知链像开部门会议大家都能坐下来好好谈但开会前需要先约时间拿会议室信号量SRCU通知链更像电话会议旁听的人很多但旁听几乎不占用资源。大部分驱动开发场景碰到的都是blocking_notifier因为设备状态通知几乎都在进程上下文完成。真正的原子通知链用得非常谨慎毕竟在里面能干的活太有限了。2.2 注册机制把自己“挂”到链上先看注册入口。内核对外提供了封装好的注册函数针对每种通知链类型都有对应的一套int atomic_notifier_chain_register(struct atomic_notifier_head *nh, struct notifier_block *nb); int blocking_notifier_chain_register(struct blocking_notifier_head *nh, struct notifier_block *nb); int srcu_notifier_chain_register(struct srcu_notifier_head *nh, struct notifier_block *nb); int raw_notifier_chain_register(struct raw_notifier_head *nh, struct notifier_block *nb);虽然类型不同但底层最终都汇聚到notifier_chain_register()这个核心函数。它的逻辑其实非常简单按照priority从大到小找到插入位置把节点串进去。如果优先级相同新节点插到链表尾部也就是说尽量让后注册的节点保持相对靠后的位置。我为什么不厌其烦地强调优先级因为很多新手都会忽略priority这个字段默认填0。问题来了如果在同一条链上有两个模块都填了默认优先级0那么它们的执行顺序就交给了“插入时链表尾部”这个规则来保证看起来还行。但一旦你负责的模块必须接收通知后立刻处理不能等其他模块先跑比如你要先更新一块共享内存其他模块的回调要基于新值做计算就必须把priority调高。实战中我给驱动的通知回调设置优先级一般会写一个正数确保它排在常规模块前面。解注册函数是*_notifier_chain_unregister()对应每种类型都有。必须强调一句模块卸载路径里忘了注销通知链是内核开发最经典的失误之一内核不会因为你忘了就自动帮你清理留下的悬空指针会在下一次事件广播时把整个系统带崩。2.3 通知机制事件触发的完整链路当事件发生时事件源调用“通知函数”沿链表一个个执行回调。核心函数是static int notifier_call_chain(struct notifier_block **nl, unsigned long action, void *data, int *nr_to_call, int *nr_calls);各类型通知链再基于它做自己的封装int atomic_notifier_call_chain(struct atomic_notifier_head *nh, unsigned long action, void *data); int blocking_notifier_call_chain(struct blocking_notifier_head *nh, unsigned long action, void *data);执行逻辑大致是从头节点开始依次调用每个notifier_block的回调函数把action事件类型和data事件附带的数据指针传下去。回调函数返回一个整数内核定义了这样几个标准返回值返回值含义后续行为NOTIFY_DONE已处理但不关心结果继续通知下一个NOTIFY_OK处理成功继续通知下一个NOTIFY_BAD处理失败停止通知并向调用者返回负的错误码NOTIFY_STOP不要再继续通知了停止通知但返回NOTIFY_OK给调用者NOTIFY_DISABLE屏蔽该通知链停止通知并记录该节点被禁用这里有个知识点容易被忽略NOTIFY_BAD和NOTIFY_STOP都会中断通知链的遍历但它们对调用者的反馈不同。前者会把“某个模块处理失败了”这件事通过返回值告诉事件源事件源可能需要回滚或记录错误后者只是“我不想再听了”并不代表出错。在写回调的时候如果只是不想让后面的模块参与返回NOTIFY_STOP是合适的如果需要让调用者感知到错误就必须返回NOTIFY_BAD。我还想多说一句NOTIFY_DISABLE这个状态和卸载不一样它只是让该节点不再参与后续通知但节点仍然驻留在链表里。如果哪天需要重新启用需要手动把该节点的priority等字段初始化再重新注册。实际开发中用得少但看到内核日志里出现notifier callback ... already registered这种提示时多半就和节点的注册状态管理有关。3. 实操过程与核心环节实现3.1 实战场景用通知链监控网络设备的插拔与状态变化理论说多了容易飘直接上实战。我这边要做一个虚拟网卡驱动需要感知物理网卡的上线、下线事件以便在物理网卡上来时自动创建虚拟接口在物理网卡下去时清理虚拟接口。这正好是netdev_chain通知链的典型应用场景。选择netdev_chain而不是自己定义一个新的通知链是因为网络子系统已经帮我把“什么样的网络事件会产生”定义清楚了而且事件参数规范、文档齐全。如果自己造一套链还要自己定义事件类型、自己找触发点、自己保证并发安全工作量完全不在一个量级。事件类型方面内核在include/linux/netdevice.h里定义了一批NETDEV_*宏常用的有NETDEV_UP网络设备被激活NETDEV_DOWN网络设备被停止NETDEV_REGISTER网络设备被注册到内核NETDEV_UNREGISTER网络设备被注销NETDEV_CHANGEMTUMTU值改变NETDEV_CHANGEADDRMAC地址改变这些宏本质上就是unsigned long类型的整数每个代表一类事件。事件触发时调用方会同时把dev结构体指针作为data参数往下传。监听NETDEV_REGISTER和NETDEV_UNREGISTER两个事件再加上NETDEV_UP/NETDEV_DOWN的基本状态监控就构成了我这次需求的最小闭环。3.2 完整示例注册回调、构建处理逻辑下面是一段可以编译进内核模块的示例代码基于内核 5.10 的接口#include linux/module.h #include linux/netdevice.h #include linux/notifier.h #include linux/netlink.h static int vnic_netdev_event(struct notifier_block *nb, unsigned long event, void *ptr) { struct net_device *dev netdev_notifier_info_to_dev(ptr); if (!dev) { pr_err(vnic: netdev event with NULL dev\n); return NOTIFY_DONE; } switch (event) { case NETDEV_REGISTER: pr_info(vnic: dev %s registered\n, dev-name); /* 在这里创建对应的虚拟接口 */ break; case NETDEV_UNREGISTER: pr_info(vnic: dev %s unregistered\n, dev-name); /* 在这里清理虚拟接口 */ break; case NETDEV_UP: pr_info(vnic: dev %s is up\n, dev-name); break; case NETDEV_DOWN: pr_info(vnic: dev %s is down\n, dev-name); break; default: break; } return NOTIFY_DONE; } static struct notifier_block vnic_nb { .notifier_call vnic_netdev_event, .priority 10, }; static int __init vnic_init(void) { int ret; ret register_netdevice_notifier(vnic_nb); if (ret) { pr_err(vnic: failed to register notifier, err%d\n, ret); return ret; } pr_info(vnic: netdevice notifier registered\n); return 0; } static void __exit vnic_exit(void) { int ret; ret unregister_netdevice_notifier(vnic_nb); if (ret) pr_warn(vnic: unregister notifier failed, err%d\n, ret); else pr_info(vnic: netdevice notifier unregistered\n); } module_init(vnic_init); module_exit(vnic_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(Virtual NIC netdevice notifier example);这段代码看起来简单但里面每一步都是权衡过的。回调里通过netdev_notifier_info_to_dev(ptr)而不是直接把ptr强转成net_device *这是因为不同版本内核的ptr参数类型有变化早期是void *后来改成了netdev_notifier_info封装直接用封装接口可以在内核版本间平滑迁移。priority 10是我故意设置的比默认优先级高一些保证我在所有默认优先级模块之前拿到事件。回调里只做打印和轻量级资源准备没有做慢速操作因为NETDEV_REGISTER等事件的通知路径虽然不是原子上下文但依然要照顾整体性能。所有的注册/解注册返回值都检查了并且用pr_err/pr_warn留下了日志这套习惯在真实项目中能帮你少掉很多头发。3.3 模块加载与事件触发验证写完代码编译成.ko文件后建议的验证流程是# 加载模块 insmod vnic_notifier.ko # 查看内核日志确认注册成功 dmesg | grep vnic # 创建一个 dummy 网卡来触发 NETDEV_REGISTER 事件 ip link add dummy0 type dummy ip link set dummy0 up # 查看日志应该能看到 vnic 监听到的回调输出 dmesg | tail -n 20 # 删除网卡触发 NETDEV_UNREGISTER ip link del dummy0 # 卸载模块 rmmod vnic_notifier.ko执行ip link add dummy0 type dummy后内核网络子系统会走完整的设备注册流程NETDEV_REGISTER事件会被广播到netdev_chain上的所有监听者。此时 dmesg 里如果出现了vnic: dev dummy0 registered说明通知链注册成功、回调被正确触发了。这一步验证看起来简单但它验证的是整条链路的正确性模块注册通知链、事件源产生事件、通知链遍历节点、回调函数被执行。任何一环有问题日志都不会如预期出现。3.4 用自定义通知链实现模块间解耦进阶实操netdev_chain是内核已经定义好的链条属于“顺水推舟”。但更常见、也更能体现通知链价值的是你自己写一个子系统然后开放给别人注册监听。我这里再演示一个最小可用的自定义通知链。假设你写了一个传感器数据采集驱动每次采集到新数据时希望让其他模块比如显示驱动、日志模块能感知到。可以这样做第一定义一个头结构static BLOCKING_NOTIFIER_HEAD(sensor_chain);这个宏会在编译期静态初始化一个blocking_notifier_head连锁都不用你手动初始化。第二导出注册接口给其他模块使用int sensor_register_notifier(struct notifier_block *nb) { return blocking_notifier_chain_register(sensor_chain, nb); } EXPORT_SYMBOL(sensor_register_notifier); int sensor_unregister_notifier(struct notifier_block *nb) { return blocking_notifier_chain_unregister(sensor_chain, nb); } EXPORT_SYMBOL(sensor_unregister_notifier);第三在采集到数据的地方触发事件#define SENSOR_EVENT_DATA_READY 0x01 #define SENSOR_EVENT_ERROR 0x02 void sensor_data_ready(int value) { /* 把采集到的值通过 data 指针传出去 */ blocking_notifier_call_chain(sensor_chain, SENSOR_EVENT_DATA_READY, value); }第四其他模块想监听只需要注册自己的notifier_block即可static int display_notifier_call(struct notifier_block *nb, unsigned long event, void *data) { if (event SENSOR_EVENT_DATA_READY) { int *val (int *)data; /* 更新显示缓存 */ } return NOTIFY_OK; } static struct notifier_block display_nb { .notifier_call display_notifier_call, .priority 0, }; /* 在模块初始化时 */ sensor_register_notifier(display_nb);就这样采集驱动完全不知道谁在监听显示驱动也完全不需要和采集驱动在代码层面产生耦合。模块可以独立加载、独立卸载只要在卸载时把通知节点注销干净就行。这种解耦方式比写一堆函数指针注册表要规范得多也更贴近内核社区的主流组织方式。4. 常见问题与排查技巧实录4.1 回调里的“睡眠”禁区通知链回调里能不能睡眠取决于链的类型。这个问题我见过太多人栽跟头在一个atomic_notifier回调里调用kmalloc(GFP_KERNEL)——在原子上下文里试图睡眠分配内存——直接触发调度器报错系统可能 oops。最让人防不胜防的是你以为自己在安全的上下文实际上不是。举个例子某些网络驱动在硬中断上下文里会触发NETDEV_CHANGEADDR等事件如果你在这个回调里用了GFP_KERNEL分配内存后果大概率是系统卡死或者内核崩溃。排查时日志里可能会出现类似BUG: sleeping function called from invalid context的信息。总结一下不同链型该遵守的纪律atomic_notifier回调里绝对不允许睡眠连spin_lock都尽量少用blocking_notifier回调里可以睡眠但不要在持锁状态下长时间等待不管哪种链回调里都不应该做重活。如果需要做大量处理正确做法是把工作扔给工作队列schedule_work或内核线程4.2 注册顺序和 priorities一场看不见的竞速默认priority为0的情况下新节点插到链尾事件通知按注册顺序执行。这个顺序带来的问题很微妙如果你的模块和另一个模块都监听NETDEV_UP两个模块之间本没有依赖关系那么谁先谁后都可以接受。但如果存在隐含依赖——A模块必须先于B模块处理网络状态否则B模块会基于过期的状态决策——就必须显式调整优先级。在 A 的notifier_block里把 priority 设为比B更大的数值。在实际调试中我推荐的习惯是监控链上的注册节点。可以在模块加载后遍历通知链头结构把每个节点的priority和回调地址、符号名打印出来确认顺序符合预期。例如struct notifier_block *nb netdev_chain.head; while (nb) { pr_info(notifier: cb%pS priority%d\n, nb-notifier_call, nb-priority); nb nb-next; }注意不同内核版本里对struct notifier_head的成员名可能不同head字段有的是head有的是first需要视具体版本微调。这样打印出来的信息能帮你快速定位“为什么我的回调在超时后才执行”。4.3 模块卸载竞态通知链悬空指针的世纪难题模块卸载时如果还有事件正在通知链上传送而你刚好把模块代码卸载了回调函数变成悬空指针下一个事件到来时内核就会跳到不可描述的内存地址去执行。这个问题在真实的项目里发生概率不低特别是那些“不按固定频率触发”的事件——你可能测试几小时都没问题但某次恰好赶上模块卸载的窗口就炸了。内核社区是通过blocking_notifier使用的 rwsem 锁机制来规避这个问题的注册和通知都要拿锁所以在通知过程中无法完成解注册这就保证了“只要通知还在链上跑回调代码一定还在”。但前提是解注册函数确实在模块卸载路径里被调用了并且模块引用计数管理正确。实操建议如下第一在模块的exit函数里第一行就注销通知链不要等到清理完所有资源之后再做。你先注销再释放回调依赖的资源悬空窗口可以缩短到0。第二如果通知链类型是atomic_notifier这类链没有 rwsem 保护解注册只能保证“未来事件不再找我”并不能保证“正在执行的回调已经结束”。此时就要借助synchronize_rcu()等机制等待可能正在执行的回调结束再释放资源。第三写模块时最好加一个引用计数器每个进入通知回调的路径都计数解注册时等待计数归零。虽然会增加一点复杂度但在生产级驱动里是值得的。4.4 死锁与重入回调里再触发同一个通知链这个坑说小不小。如果回调函数内部直接或间接触发了同一条通知链的事件分发就可能在遍历链表的同时修改链表轻则跳过某些节点重则死锁。拿blocking_notifier举例通知过程已经持有了 rwsem 的读锁或写锁取决于实现版本此时你在回调里又触发了一次事件分发分发函数尝试再次获取同一把锁。rwsem 在同一个上下文里重复获取写锁是会直接自锁的。解决方案是回调函数里想要触发新的事件不要同步调用分发函数而是推迟到上下文中再执行比如塞进工作队列。如果非要同步触发至少要保证新触发的是另一条通知链的事件不会和当前链共用锁。这块的经验我一个词概括分层。事件处理的触发源、传递层、执行层要分开不要让回调变成一个“什么都往里塞的大筐”。4.5 调试工具与实战经验补充通知链的调试最核心的工具其实是dmesg和/proc/kallsyms。前者看日志后者帮你定位回调函数符号。当系统崩溃且崩溃栈指向某个notifier_call时用gdb加载 vmlinux 和 System.map查看崩溃地址对应的符号通常就能锁定是哪个模块的回调出了问题。有些较新版本的内核里还带了 tracepoint 或者 ftrace 的过滤器可以用trace-cmd跟踪某一类事件。不过最实用的还是几个土办法在回调入口出口分别打pr_info记录进入/退出时间和返回码用WARN_ON(1)在可疑路径上留下栈回溯信息如果怀疑通知链链表本身被破坏遍历链表并检查每个节点的合法性日志打的越多越影响性能所以真机上我会用一个变量记录统计值只在崩溃或主动触发时通过/sys节点导出这个做法既能保留现场又不会干扰实时路径。4.6 常见问题速查表现象可能原因排查方向回调从未被调用通知链注册失败或事件类型不匹配检查注册返回值对比事件类型常量回调执行时系统卡死回调里睡眠/阻塞在原子上下文看日志中是否有 “sleeping function called from invalid context”模块卸载后系统崩溃通知链未注销或资源释放顺序错误卸载前先确认解注册成功加上资源计数回调执行顺序错乱priority 设置不当或未设置打印链上各节点优先级确认插入位置死锁回调中再次触发同一通知链检查锁获取方式避免同步重入5. 从理论到悟到的几点实战心得5.1 先找现成的再决定是否自造我见过很多工程师一上来就自己定义通知链觉得“我要做一个 XXX 事件总线让所有模块都能订阅”。在大部分内核子系统的场景里其实是不必要的。网络子系统有netdev_chain块设备有blocking_notifier电源有power_supply通知内存有memory_chain——先去看看你的事件类型有没有现成链条可用有就复用它的事件分类和参数约定。只有当事件承载的信息和现有链条差异太大或者安全语义不同比如必须原子执行才需要自建。自己造通知链要思考的事情比想象中多一套回调返回值的语义约定沿用内核标准、并发模型选择、HEAD初始化的正确姿势、消息data的引用计数生命周期。光是这些设计问题就足以吞掉好几天开发时间。复用现成链条等于顺手继承了内核社区验证过的那套约定省下的时间用来打磨你自己的业务逻辑这才是正解。5.2 回调越短越好必要时延迟处理一个常见的反面教材是驱动在NETDEV_UP回调里做了大量设备探测、状态存储、调用用户态通知导致网卡 up 的路径明显变慢甚至出现超时。通知链的设计初衷是“广播事件”不是“同步执行繁重任务”。回调函数比较合适的定位是记录状态、快速决策、把耗时操作调度到工作队列或专用内核线程。判断标准很简单——如果你的回调执行时间超过了几百微秒就要警惕了这个时间已经可能影响其他节点的执行和其他模块的响应。我自己写过最长的回调不过几十行而且这几十行逻辑里没有 I/O、没有互斥等待、没有网络操作全是内存里的状态处理和队列调度这个边界感是长期调试熬出来的。5.3 事件数据生命周期必须明确通知链的data参数是个void *它指向的对象谁分配、谁释放、生命周期到哪一刻结束必须在设计时写得明明白白。比如NETDEV_REGISTER事件里的net_device *dev它是由事件源即网络核心代码持有的回调里可以安全地去访问但你不能在回调里把dev保存下来等几个小时后你的工作队列再去用它——那时候dev可能已经注销释放了。处理这种问题的常规做法是在回调里拿到需要保存的信息后立刻复制一份自己的私有副本比如dev-name拷贝到字符数组里或者用引用计数机制在回调里临时增加对象引用确保后续使用者安全。5.4 注册与注销的对称性最后一个小经验把注册和解注册写在同一对 init/exit 函数里保持对称。不要在 init A 注册却在 clean B 处注销也不要让解注册分散在多个路径上。维护一个“我注册过什么、在哪个函数里、以什么顺序”的清单对于代码审查和调试都有极大帮助。我见过最隐蔽的一个事故就是模块里同时注册了两条通知链错误的退出路径只注销了一条导致另一条回调悬空。而这种问题往往不是一次操作就暴露的要等对应的网络设备动作触发时系统才会在野指针上崩掉。排查起来远比正常工作流里那些有日志的错误要费劲。如果你正在开发一个长期维护的模块建议在注销函数里增加一个防御性检查比如注销后再次遍历对应链头确认自己的notifier_block已经不在链上能在问题爆发之前抓住接口误用。6. 再补几个压箱底的排查细节6.1 通过内核日志和符号定位回调节点前面提到用遍历链表的方式打印每个节点信息具体落地时注意有一些链头结构在不同内核版本里字段名有差异。例如有的版struct blocking_notifier_head里面是rwsemhead有的版本内部字段更隐蔽。需要以你本机/usr/src/linux-headers-$(uname -r)下的头文件为准。在实践中我还常用/proc/kallsyms确认notifier_call符号对应的模块是否还在内存里。假如一个回调地址落在某个已卸载模块的地址范围中这个节点的解释基本上就是悬空指针必须立刻处理。6.2 内核崩溃时如何从栈回溯定位通知链问题系统崩溃时日志里的栈回溯往往长得像天书。但只要抓住几个关键信号就能快速定位栈里有没有notifier_call_chain的调用帧有没有你注册的回调函数名回溯列表里同时出现这两个名字说明崩溃发生在通知链广播过程中。下一步就是检查你回调附近访问的指针。大多数情况是data参数指向的内存已经释放了生命周期管理不当或者是回调里拿了某个锁而锁的另一端正试图对你的回调持有者做解注册操作锁顺序死锁。定位到这一步之后修复方案就清晰多了。6.3 与 ftrace 联动如果问题不是必现而是偶发靠加打印大概率会疲于奔命。这时可以把 ftrace 的 function tracer 打开跟踪notifier_call_chain及特定模块回调的调用序列配合trace_printk布点把事件触发前后的调用关系完整记录下来。虽然 ftrace 的配置有点繁琐但对于偶发的通知链异步问题它能直接给出“什么顺序调用、在哪个位置中断”的确切答案比纯靠猜高效太多。内核文档和源码注释里对通知链的说明已经不少了但真正能缩短你排查时间的往往是在真实设备上被硬生生“试”出来的这些经验。在碰到具体问题时把上面这些检查步骤逐一过一遍基本能在半个小时内锁定问题方向。
返回列表