【Linux内核模块】模块卸载函数详解
代码语言:javascriptAI代码解释static void __exit 函数名(void) {// 释放资源的操作module_exit(函数名);// 告诉内核这是卸载函数static:和加载函数一样,限制函数仅在当前模块可见__exit:内核会把带这个标记的函数存放在.exit.text段,只有模块支持卸载时才保留:注册卸载函数,让内核知道模块卸载时该调用谁。
一、卸载函数的角色:模块的 "善后专员"
模块卸载函数是当你执行rmmod命令时,内核自动调用的 "收尾函数"。它的核心任务就一个:清理加载函数申请的所有资源,把系统恢复到模块加载前的状态。
举个生活化的例子:加载函数像你出差入住酒店时的 "登记 + 领房卡",卸载函数就是 "退房 + 交钥匙 + 结清费用"。如果退房时没交钥匙(没释放资源),下次就没人能再用这个房间了(设备无法再次加载)。
和加载函数相比,卸载函数有三个明显不同:
- 没有返回值(
void类型),因为卸载失败也无法挽回 - 用
__exit宏标记,告诉内核这是卸载专用函数 - 必须严格按 "反向顺序" 释放资源
二、卸载函数的 "身份证":标准结构解析
2.1 基础定义格式
代码语言:javascript
AI代码解释
static void __exit 函数名(void) {
// 释放资源的操作
}
module_exit(函数名); // 告诉内核这是卸载函数
这个结构里的每个部分都有特殊意义:
static:和加载函数一样,限制函数仅在当前模块可见__exit:内核会把带这个标记的函数存放在.exit.text段,只有模块支持卸载时才保留module_exit:注册卸载函数,让内核知道模块卸载时该调用谁
2.2 __exit宏的特殊作用
__exit宏不仅仅是个标记,它会影响内核对函数的处理:
- 如果模块编译时加了
CONFIG_MODULE_UNLOAD=n(禁用卸载),带__exit的函数会被直接丢弃 - 运行时,
__exit函数在执行后会被从内存中移除 - 这就是为什么不能在其他地方调用卸载函数 —— 它可能已经被释放了
三、卸载函数的工作流程:从触发到完成
当执行rmmod mymod时,整个过程像一场 "有序撤离":

关键步骤解析:
- 依赖检查:内核通过
module->refcnt引用计数判断模块是否被使用(比如有进程打开了模块创建的设备文件)。如果refcnt > 0,会提示 "资源正忙"(-EBUSY),卸载失败。 - 资源释放:这是卸载函数的核心工作,必须严格按 "加载时的反向顺序" 释放。比如加载时先申请内存(A)再注册设备(B),卸载时就要先注销设备(B)再释放内存(A)。
- 清理痕迹:卸载完成后,内核会从
/proc/modules列表中移除该模块,lsmod再也查不到它的信息,就像 "从未存在过"。
四、卸载函数必须做的事:资源释放清单
卸载函数的核心使命是 "清理战场",以下这些资源必须逐个释放:
1. 设备驱动相关资源
- 字符设备:
unregister_chrdev或cdev_del - 块设备:
unregister_blkdev - 网络设备:
unregister_netdev
2. 内存资源
kfree释放kmalloc申请的内存vfree释放vmalloc申请的内存kfree_slab释放 slab 缓存
3. 中断与定时器
free_irq释放申请的中断号del_timer_sync删除定时器并等待其结束
4. 其他资源
- 注销 proc 文件:
remove_proc_entry - 释放信号量 / 自旋锁(如果是动态创建的)
- 取消内核线程(
kthread_stop)
实例:正确的释放顺序
代码语言:javascript
AI代码解释
// 加载函数(简化)
static int __init demo_init(void) {
buf = kmalloc(1024, GFP_KERNEL); // 步骤1:申请内存
register_chrdev(0, "demo", &fops); // 步骤2:注册设备
return 0;
}
// 卸载函数(反向释放)
static void __exit demo_exit(void) {
unregister_chrdev(dev_num, "demo"); // 先注销设备(步骤2的反向)
kfree(buf); // 再释放内存(步骤1的反向)
}
五、常见卸载失败场景及解决方案
5.1 卸载时提示 "Resource temporarily unavailable"
这是最常见的错误,原因通常有两个:
- 模块被其他模块依赖(
lsmod查看Used by字段) - 有进程正在使用模块创建的资源(比如打开了设备文件)
解决步骤:
- 用
lsof /dev/demo查看谁在使用设备 - 关闭相关进程或先卸载依赖模块
- 重新执行
rmmod
5.2 卸载后资源未释放(内存泄漏)
表现为:多次加载卸载后,free命令显示可用内存越来越少。
排查技巧:
- 用
slabtop查看内核内存使用,找是否有持续增长的 slab 缓存 - 在卸载函数中加
printk日志,确认每个释放函数都被执行 - 检查是否漏释放资源(比如动态创建的自旋锁没销毁)
5.3 卸载时内核崩溃(Oops)
通常是因为:
- 重复释放资源(比如
kfree同一个指针两次) - 释放了未初始化的资源(比如
buf为NULL时调用kfree) - 释放顺序错误(比如先释放内存再注销设备,而注销设备还需要访问内存)
解决办法:
- 用
BUG_ON检查指针有效性:BUG_ON(!buf); kfree(buf); - 逐步注释释放代码,定位到导致崩溃的具体语句
- 严格按 "反向顺序" 释放资源
六、卸载函数的 "最佳实践"
6.1 释放前检查资源有效性
代码语言:javascript
AI代码解释
static void __exit demo_exit(void) {
if (dev_registered) { // 用标志位判断是否需要注销
unregister_chrdev(dev_num, "demo");
}
if (buf) { // 检查指针非空再释放
kfree(buf);
buf = NULL; // 避免重复释放
}
}
6.2 用标志位跟踪资源状态
在模块中定义标志位,记录哪些资源已成功初始化:
代码语言:javascript
AI代码解释
static bool dev_registered = false;
static bool buf_allocated = false;
// 加载函数中
buf = kmalloc(...);
if (buf) buf_allocated = true;
// 卸载函数中
if (dev_registered) {
unregister_chrdev(...);
dev_registered = false;
}
6.3 避免在卸载函数中做 "新操作"
卸载函数只做 "释放",不做 "创建" 或复杂逻辑:
- 不要调用
kmalloc(可能失败) - 不要启动新的内核线程
- 不要修改其他模块依赖的全局变量
模块卸载函数看似简单,实则是模块稳定性的 "最后一道防线"。记住这三个核心原则:
- 反向释放:严格按加载的相反顺序释放资源(像剥洋葱一样从外到内)
- 检查有效性:释放前确认资源存在(避免空指针或重复释放)
- 只做清理:卸载函数的唯一任务是 "拆摊子",不做任何新的资源申请
一个好的卸载函数,应该让模块像 "从未存在过" 一样干净地离开系统。下次写模块时,不妨多花点时间打磨卸载逻辑 —— 毕竟,能优雅退场的模块才是真正可靠的模块。
更多推荐



所有评论(0)