In continuation of the previous text:第二章:模块的编译与运行-12 Initialization and Shutdown, let's GO ahead.
The Cleanup Function
Every nontrivial module also requires a cleanup function, which unregisters interfaces and returns all resources to the system before the module is removed. This function is defined as:
所有非简单功能的模块都还需要一个清理函数。在模块被移除前,该函数会注销模块注册的所有接口,并将所有资源归还给系统。清理函数的定义格式如下:
static void __exit cleanup_function(void)
{
/* Cleanup code here */
}
module_exit(cleanup_function);
The cleanup function has no value to return, so it is declared void. The __exit modifier marks the code as being for module unload only (by causing the compiler to place it in a special ELF section). If your module is built directly into the kernel, or if your kernel is configured to disallow the unloading of modules, functions marked __exit are simply discarded. For this reason, a function marked __exit can be called only at module unload or system shutdown time; any other use is an error. Once again, the module_exit declaration is necessary to enable to kernel to find your cleanup function.
If your module does not define a cleanup function, the kernel does not allow it to be unloaded.
清理函数无需返回值,因此声明为 void 类型。__exit 修饰符会将该函数标记为 “仅用于模块卸载”—— 其实现原理是让编译器将函数放入一个特殊的 ELF 段中。若你的模块被直接编译到内核里,或是内核配置为禁止卸载模块,那么被 __exit 标记的函数会被直接丢弃。正因如此,被 __exit 标记的函数只能在模块卸载或系统关机时调用,任何其他场景下的调用都会引发错误。
同样,module_exit 声明也是必需的,它能让内核找到你的清理函数。
若你的模块没有定义清理函数,内核会禁止该模块被卸载。
补充说明:
-
__exit修饰符的 “场景限定” 逻辑-
对可加载模块(
.ko文件):__exit函数会被保留在模块的 ELF 段中,仅在rmmod卸载模块时调用; -
对内置模块(编译进内核镜像):由于内置模块无法卸载,
__exit函数会被编译器从最终代码中剔除,避免占用不必要的内存; -
错误使用后果:若在初始化函数或其他非卸载场景调用
__exit函数,编译器会报 “函数未定义” 错误,或内核运行时触发崩溃(因函数代码已被丢弃)。
-
-
清理函数的 “反向操作” 原则
清理函数必须严格撤销初始化函数的所有操作,顺序通常与初始化相反,常见任务包括:
-
恢复状态:如将硬件寄存器重置为默认值、取消对其他模块的依赖引用(避免其他模块被误卸载)。
-
释放资源:如用
kfree()释放动态分配的内存、iounmap()解除硬件地址映射、class_destroy()销毁设备类; -
注销接口:如调用
unregister_chrdev()注销字符设备、proc_remove()删除/proc文件、free_irq()释放中断号;
-
-
无清理函数的模块限制
若模块未通过
module_exit声明清理函数,内核会将模块标记为MOD_UNLOAD禁用状态。此时执行rmmod会失败,系统日志会输出 “Operation not permitted”,这是内核为防止资源泄漏设置的保护机制 —— 没有清理函数,模块占用的内存、设备号等资源无法回收,强行卸载会导致系统不稳定。 -
module_exit与module_init的对称关系两者均为内核模块的核心入口宏,功能完全对称:
-
module_exit:指定模块卸载时执行的清理逻辑,解决 “如何让模块干净退出内核”;除仅用于调试、无需卸载的临时模块外,所有可加载模块都需成对使用这两个宏,确保模块的 “加载 - 卸载” 流程完整。 -
module_init:指定模块加载时执行的初始化逻辑,解决 “如何让模块接入内核”。
-
Module-Loading Races
Thus far, our discussion has skated over an important aspect of module loading: race conditions. If you are not careful in how you write your initialization function, you can create situations that can compromise the stability of the system as a whole. We will discuss race conditions later in this book; for now, a couple of quick points will have to suffice.
到目前为止,我们的讨论忽略了模块加载中的一个重要问题:竞争条件(race conditions)。如果初始化函数的编写不够谨慎,可能会导致影响整个系统稳定性的问题。本书后续会详细讨论竞争条件,目前先简要说明两个核心要点。
The first is that you should always remember that some other part of the kernel can make use of any facility you register immediately after that registration has completed. It is entirely possible, in other words, that the kernel will make calls into your module while your initialization function is still running. So your code must be prepared to be called as soon as it completes its first registration. Do not register any facility until all of your internal initialization needed to support that facility has been completed.
第一个要点是:你必须始终记住,在完成某个功能实体的注册后,内核的其他部分可能会立即使用该实体。换句话说,即使你的初始化函数仍在运行,内核也完全有可能调用你的模块。因此,代码必须做好准备 —— 一旦完成首次注册,就可能被内核调用。在完成支持该功能实体所需的所有内部初始化之前,不要注册任何功能实体。
You must also consider what happens if your initialization function decides to fail, but some part of the kernel is already making use of a facility your module has registered. If this situation is possible for your module, you should seriously consider not failing the initialization at all. After all, the module has clearly succeeded in exporting something useful. If initialization must fail, it must carefully step around any possible operations going on elsewhere in the kernel until those operations have completed.
你还必须考虑另一种情况:如果初始化函数决定终止(即初始化失败),但内核的某些部分已经在使用模块已注册的功能实体,该如何处理?如果你的模块可能出现这种情况,应认真考虑是否要避免初始化失败。毕竟,模块显然已经成功导出了一些有用的功能。如果初始化必须终止,那么在其他内核部分的相关操作完成前,模块必须小心避开这些可能正在进行的操作。
补充说明:
-
竞争条件的典型场景
模块初始化阶段的竞争条件多源于 “注册提前,准备滞后”,例如:
-
场景 2:模块注册
/proc文件后,未初始化/proc读写函数依赖的缓冲区;若内核线程或用户程序立即读取该文件,会触发空指针访问错误。核心解决原则是 “先内部准备,后外部注册”—— 确保所有依赖的资源(内存、数据结构、硬件状态)都初始化完成后,再向内核注册功能实体。 -
场景 1:模块先调用
register_chrdev()注册字符设备(让内核可见),但未完成设备数据结构(如struct scull_dev)的初始化;此时用户态程序若立即open该设备,内核调用模块的open函数时,会访问未初始化的数据,导致崩溃。
-
-
初始化失败的 “资源回滚” 逻辑
若初始化过程中某一步失败(如内存分配失败、硬件初始化出错),模块必须 “回滚” 已完成的注册操作,避免资源泄漏或无效功能残留,例如:
-
若模块先创建了 sysfs 条目,后初始化硬件失败,需调用
sysfs_remove_file()删除条目后再退出。回滚操作的顺序应与注册顺序相反(如先注册 A 再注册 B,失败时先注销 B 再注销 A),确保不会遗漏任何已注册的资源。 -
若模块先注册了字符设备,后分配内存失败,需在返回错误前调用
unregister_chrdev()注销设备;
-
-
避免初始化失败的实际建议
若模块已注册的功能实体被内核其他部分使用,强制终止初始化可能导致系统不稳定,此时可采用 “降级初始化” 而非 “完全失败”:
-
优势:既能保留部分可用功能,又避免了 “已被使用的实体突然不可用” 引发的竞争问题。
-
示例:某硬件驱动注册中断处理函数后,发现硬件参数异常,可选择禁用非核心功能(如关闭性能优化),而非注销中断并终止初始化;
-
-
内核提供的同步工具
后续章节会介绍的互斥锁(mutex)、自旋锁(spinlock) 等同步机制,可用于解决初始化阶段的竞争条件。例如,在模块内部用互斥锁保护 “初始化状态”,确保注册的功能实体被调用时,初始化已完全完成。
Linux模块清理与加载竞争

被折叠的 条评论
为什么被折叠?



