这篇文章来讲讲内核模块的卸载过程机制。
本文引用的内核代码参考来自版本 linux-5.15.4 。
在用户空间,通过指令 rmmod 可以将一个内核模块从系统中卸载,使用方法如下:
rmmod xx /* 卸载已经加载的内核模块 xx */
注意,卸载内核模块需要具有 CAP_SYS_MODULE 权限(root用户或者其他具有这个权限的用户),否则会加载失败。
rmmod 指令通过系统调用 sys_module_module()
完成卸载工作。
系统调用 sys_delete_module
sys_delete_module() 函数原型如下:
long sys_delete_module(const char __user *name_user, unsigned int flags);
参数 name_user 是模块名称。参数 flags 为卸载标志。
函数的具体代码如下(已经将函数名称替换为实际展开后的形式),关键函数添加了注释:
/* <kernel/module.c> */
long sys_delete_module(const char __user *name_user, unsigned int flags)
{
struct module *mod;
char name[MODULE_NAME_LEN];
int ret, forced = 0;
/* 判断是否有卸载模块的权限 */
if (!capable(CAP_SYS_MODULE) || modules_disabled)
return -EPERM;
/* 从用户空间复制模块名称到内核空间 */
if (strncpy_from_user(name, name_user, MODULE_NAME_LEN-1) < 0)
return -EFAULT;
name[MODULE_NAME_LEN-1] = '\0';
audit_log_kern_module(name);
/* 互斥锁 */
if (mutex_lock_interruptible(&module_mutex) != 0)
return -EINTR;
/* 在内核链表中查找要卸载的内核模块 */
mod = find_module(name);
if (!mod) {
ret = -ENOENT;
goto out;
}
/* 检查模块的依赖关系 */
if (!list_empty(&mod->source_list)) {
/* Other modules depend on us: get rid of them first. */
ret = -EWOULDBLOCK;
goto out;
}
/* 判断模块是否已经加载成功 */
if (mod->state != MODULE_STATE_LIVE) {
/* FIXME: if (force), slam module count damn the torpedoes */
pr_debug("%s already dying\n", mod->name);
ret = -EBUSY;
goto out;
}
/* If it has an init func, it must have an exit func to unload */
if (mod->init && !mod->exit) {
forced = try_force_unload(flags);
if (!forced) {
/* This module can't be removed */
ret = -EBUSY;
goto out;
}
}
/* Stop the machine so refcounts can't move and disable module. */
ret = try_stop_module(mod, flags, &forced);
if (ret != 0)
goto out;
mutex_unlock(&module_mutex);
/* Final destruction now no one is using it. */
if (mod->exit != NULL)
mod->exit();
blocking_notifier_call_chain(&module_notify_list,
MODULE_STATE_GOING, mod);
klp_module_going(mod);
ftrace_release_mod(mod);
async_synchronize_full();
/* 记录最近卸载的模块的名字,便于诊断问题 */
strlcpy(last_unloaded_module, mod->name, sizeof(last_unloaded_module));
/* 释放模块占用的资源 */
free_module(mod);
/* someone could wait for the module in add_unformed_module() */
wake_up_all(&module_wq);
return 0;
out:
mutex_unlock(&module_mutex);
return ret;
}
下边结合代码来分析模块的卸载过程。
模块的卸载过程
模块卸载的过程由一系列执行动作组合而成,此处只介绍其中关键的执行步骤。其他的函数调用,有兴趣的话可以自己研究一下。
- 判断是否可以卸载模块
通过以下代码
if (strncpy_from_user(name, name_user, MODULE_NAME_LEN-1) < 0)
return -EFAULT;
判断用户是否具备卸载模块的权限,或者内核是否允许卸载模块。
如果不能卸载此内核模块,则退出并返回错误码 -EFAULT。否则,继续执行卸载动作。
- 查找模块
首先,将要卸载的模块名称从用户空间复制到内核空间,调用函数 strncpy_from_user()
:
if (strncpy_from_user(name, name_user, MODULE_NAME_LEN-1) < 0)
return -EFAULT;
然后,通过函数 find_module()
在内核模块链表 modules 中查找要卸载的模块,函数的入参为模块的名字。其函数的实现代码如下:
/* <kernel/module.c> */
struct module *find_module(const char *name)
{
return find_module_all(name, strlen(name), false);
}
/* 具体的执行函数 */
static struct module *find_module_all(const char *name, size_t len,
bool even_unformed)
{
struct module *mod;
module_assert_mutex_or_preempt();
/* 遍历内核模块链表 */
list_for_each_entry_rcu(mod, &modules, list,
lockdep_is_held(&module_mutex)) {
if (!even_unformed && mod->state == MODULE_STATE_UNFORMED)
continue;
if (strlen(mod->name) == len && !memcmp(mod->name, name, len))
return mod;
}
return NULL;
}
由代码可知,通过 list_for_each_entry_rcu()
实现遍历 modules 链表中每一个模块。如果查找到指定的模块,那么 find_module()
返回该模块的 mod 结构指针,否则返回 NULL。
- 检查模块依赖关系
为了系统的稳定,一个有依赖关系的模块不应该从系统中卸载,即有其他模块依赖将要删除的模块。模块间的依赖关系通过结构体 struct module 中的成员变量 source_list 和 target_list 来实现。
其中,source_list 用于将对此模块有依赖的模块链接起来。因此,检查卸载模块的 source_list 链表是否为空,即可判断模块是否被其他模块依赖,相关代码段为:
if (!list_empty(&mod->source_list)) {
/* Other modules depend on us: get rid of them first. */
ret = -EWOULDBLOCK;
goto out;
}
如果存在依赖关系,则结束卸载,并返回错误码。否则,继续执行卸载动作。
- 释放模块占用的资源
如果前边步骤一切正常,sys_delete_module()
会调用 free_module()
函数来做模块卸载末期的清理工作。包括:更新模块状态为 MODULE_STATE_UNFORMED,将卸载的模块从 modules 链表中移除,将模块占用的 CORE section 空间释放,释放模块接收的参数所占空间等。
小结
在此,对内核模块的基本内容进行简单的总结:
- 内核模块可以在系统运行期间动态加载。
- 用户空间,加载和卸载模块使用 mod utils 的工具包,包括最基本的 insmod 和 rmmod 工具。
- 内核模块的文件格式是一种可重定位的 ELF 文件。
- 模块可以调用内核源码或者其他模块实现的函数。
- 系统中所有加载成功的模块都被链接到 modules 链表中。
- 一个 .ko 文件是基于某一特定内核源码树所构成的,版本不一致可能会引起潜在的问题。