一、卸载函数的角色:模块的 "善后专员"

模块卸载函数是当你执行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时,整个过程像一场 "有序撤离":

关键步骤解析:

  1. 依赖检查:内核通过module->refcnt引用计数判断模块是否被使用(比如有进程打开了模块创建的设备文件)。如果refcnt > 0,会提示 "资源正忙"(-EBUSY),卸载失败。
  2. 资源释放:这是卸载函数的核心工作,必须严格按 "加载时的反向顺序" 释放。比如加载时先申请内存(A)再注册设备(B),卸载时就要先注销设备(B)再释放内存(A)。
  3. 清理痕迹:卸载完成后,内核会从/proc/modules列表中移除该模块,lsmod再也查不到它的信息,就像 "从未存在过"。

四、卸载函数必须做的事:资源释放清单

卸载函数的核心使命是 "清理战场",以下这些资源必须逐个释放:

1. 设备驱动相关资源

  • 字符设备:unregister_chrdevcdev_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字段)
  • 有进程正在使用模块创建的资源(比如打开了设备文件)

解决步骤

  1. lsof /dev/demo查看谁在使用设备
  2. 关闭相关进程或先卸载依赖模块
  3. 重新执行rmmod
5.2 卸载后资源未释放(内存泄漏)

表现为:多次加载卸载后,free命令显示可用内存越来越少。

排查技巧

  • slabtop查看内核内存使用,找是否有持续增长的 slab 缓存
  • 在卸载函数中加printk日志,确认每个释放函数都被执行
  • 检查是否漏释放资源(比如动态创建的自旋锁没销毁)
5.3 卸载时内核崩溃(Oops)

通常是因为:

  • 重复释放资源(比如kfree同一个指针两次)
  • 释放了未初始化的资源(比如bufNULL时调用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(可能失败)
  • 不要启动新的内核线程
  • 不要修改其他模块依赖的全局变量

模块卸载函数看似简单,实则是模块稳定性的 "最后一道防线"。记住这三个核心原则:

  • 反向释放:严格按加载的相反顺序释放资源(像剥洋葱一样从外到内)
  • 检查有效性:释放前确认资源存在(避免空指针或重复释放)
  • 只做清理:卸载函数的唯一任务是 "拆摊子",不做任何新的资源申请

一个好的卸载函数,应该让模块像 "从未存在过" 一样干净地离开系统。下次写模块时,不妨多花点时间打磨卸载逻辑 —— 毕竟,能优雅退场的模块才是真正可靠的模块。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐