内核模块的卸载是加载的逆过程,核心目标是安全回收模块占用的资源、解除内核依赖,并将模块从内核中移除。卸载过程需严格校验模块的 “可卸载性”(无依赖、无资源占用),再执行资源清理与内存释放,以下结合内核源码(5.x/6.x 版本)拆解完整流程。

一、卸载流程总览

卸载操作由用户空间命令(rmmod/modprobe -r)触发,内核通过 sys_delete_module 系统调用完成核心逻辑,流程需确保 “无引用、无依赖、资源全回收”,具体链路:

用户空间(rmmod)→ 调用 sys_delete_module() → 内核空间处理:
  1. 权限/状态校验(模块是否可卸载)
  2. 检查引用计数(是否被其他模块/进程占用)
  3. 执行模块卸载函数(module_exit 注册的逻辑)
  4. 解除符号依赖(从全局符号表移除导出符号)
  5. 回收内存资源(core 段、模块控制结构体)
  6. 从内核模块列表移除 → 卸载完成

二、核心环节拆解(结合内核源码)

1. 用户空间触发:rmmod 的 “前奏操作”

卸载的入口是用户空间的 rmmod 命令(或 modprobe -r),其核心工作:

  • 解析目标模块名,在 /sys/module/ 目录(内核暴露的模块信息接口)中确认模块状态;
  • 调用 sys_delete_module 系统调用,传递模块名及卸载标志(如是否强制卸载);
  • 注:modprobe -r 会先检查依赖,若目标模块是其他模块的依赖,会先卸载依赖它的模块(需无其他占用),再卸载目标模块。

2. 内核空间核心入口:sys_delete_module 系统调用

卸载的内核入口是 SYSCALL_DEFINE2(delete_module, ...),它是卸载流程的 “总控制”,简化源码如下:

#include <linux/module.h>
#include <linux/syscalls.h>

SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
                unsigned int, flags) {
    struct module *mod;
    char name[MODULE_NAME_LEN];
    int err;

    // 1. 权限校验:仅 root 或拥有 CAP_SYS_MODULE 权限的用户可卸载
    err = may_delete_module();
    if (err)
        return err;

    // 2. 从用户空间拷贝模块名到内核
    if (strncpy_from_user(name, name_user, MODULE_NAME_LEN-1) < 0)
        return -EFAULT;
    name[MODULE_NAME_LEN-1] = '\0';

    // 3. 查找目标模块(从内核全局模块列表中匹配模块名)
    mod = find_module(name);
    if (!mod)
        return -ENOENT;

    // 4. 核心卸载逻辑:检查可卸载性 + 执行卸载操作
    err = do_unload_module(mod, flags);
    if (err)
        return err;

    return 0;
}

3. 核心子流程:do_unload_module —— 卸载的 “安全闸门”

do_unload_module 是卸载的核心函数,负责校验模块是否可卸载,并触发后续资源回收,简化源码如下:

static int do_unload_module(struct module *mod, unsigned int flags) {
    int err;

    // 步骤1:检查模块状态(仅“活动态”MODULE_STATE_LIVE 可卸载)
    if (mod->state != MODULE_STATE_LIVE) {
        pr_err("模块 %s 处于不可卸载状态(当前状态:%d)\n", mod->name, mod->state);
        return -EBUSY;
    }

    // 步骤2:检查引用计数(是否被其他模块/进程占用)
    if (!try_module_get(mod)) {
        pr_err("模块 %s 引用计数非零(当前计数:%d)\n", mod->name, module_refcount(mod));
        return -EBUSY;
    }

    // 步骤3:检查依赖(是否有其他模块依赖当前模块)
    if (!list_empty(&mod->source_list)) {
        pr_err("模块 %s 被其他模块依赖,无法卸载\n", mod->name);
        module_put(mod);
        return -EWOULDBLOCK;
    }

    // 步骤4:执行模块卸载函数(module_exit 注册的逻辑)
    err = do_exit_module(mod);
    if (err) {
        module_put(mod);
        return err;
    }

    // 步骤5:回收模块资源 + 从内核移除
    free_module(mod);
    return 0;
}

4. 关键子环节拆解

(1)状态与引用计数校验:卸载的 “安全前提”
  • 状态校验:模块状态必须是 MODULE_STATE_LIVE(活动态),若处于加载中(MODULE_STATE_COMING)或卸载中(MODULE_STATE_GOING),直接拒绝卸载;
  • 引用计数校验:内核为每个模块维护 kref 类型的引用计数器(mod->refcnt),若计数 > 0,说明模块被其他模块 / 进程占用(如进程打开了模块注册的设备文件),无法卸载;
  • 依赖校验:模块的 source_list 链表记录了 “依赖当前模块的其他模块”,若链表非空,说明当前模块是其他模块的依赖,必须先卸载依赖它的模块,否则拒绝卸载。
(2)执行卸载函数:do_exit_module —— 资源清理的核心

do_exit_module 会调用模块通过 module_exit(exit_func) 注册的卸载函数,这是模块自定义资源回收的入口,逻辑如下:

static int do_exit_module(struct module *mod) {
    // 1. 将模块状态置为“卸载中”(MODULE_STATE_GOING),防止并发操作
    mod->state = MODULE_STATE_GOING;

    // 2. 调用模块的卸载函数(module_exit 注册的函数)
    if (mod->exit != NULL) {
        mod->exit();  // 执行自定义卸载逻辑:注销设备、释放中断、删除/proc节点等
    }

    // 3. 清理内核自动管理的资源:
    // - 移除模块导出的符号(从全局符号表 kallsyms 中删除)
    module_unexport_symbols(mod);
    // - 清理模块参数、sysfs 节点等
    module_cleanup_params(mod);

    return 0;
}

模块卸载函数(exit_func)的典型工作:

  • 注销设备驱动(如 unregister_chrdev);
  • 释放申请的中断(free_irq);
  • 删除 /proc//sys 中创建的节点;
  • 释放模块加载时申请的内存(如 kfree/vfree);
  • 解除与其他内核子系统的绑定(如从文件系统链表中移除)。
(3)资源回收:free_module —— 彻底移除模块

free_module 是卸载的最后一步,负责回收模块占用的所有内核资源,核心逻辑:

static void free_module(struct module *mod) {
    // 1. 释放模块的 core 段内存(加载时通过 vmalloc 分配的核心内存)
    vfree(mod->module_core);
    // 2. 释放模块控制结构体(struct module)本身的内存
    kfree(mod);
    // 3. 从内核全局模块列表(modules 链表)中移除该模块
    list_del(&mod->list);
    // 4. 更新内核模块统计信息
    module_update_counts();
}

这里的 mod->module_core 指向加载时为 core 段分配的虚拟连续内存(包含 .text、.data 等核心节区),通过 vfree 释放(vmalloc 的对应释放函数);而 struct module 结构体是通过 kmalloc 分配的,因此用 kfree 释放。

三、卸载流程的关键约束

  1. 原子性与安全性:卸载过程中任何一步失败(如引用计数非零、依赖未解除),都会终止流程并保留模块状态,避免内核资源泄漏;
  2. init 段无需单独释放:加载时的 init 段在初始化完成后已被释放,卸载仅需处理 core 段;
  3. 强制卸载的风险:rmmod -f 可跳过引用计数校验强制卸载,但可能导致内核崩溃(如模块资源被占用时强行释放),仅用于调试场景。

四、卸载流程总结

内核模块卸载是 “校验 - 清理 - 回收” 的闭环:
4. 校验层:确保模块无依赖、无引用、状态合法;
5. 清理层:执行模块自定义卸载逻辑 + 内核自动资源清理;
6. 回收层:释放内存、移除模块控制结构,彻底脱离内核管理。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐