这篇文章来讲讲内核模块的卸载过程机制。

本文引用的内核代码参考来自版本 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 文件是基于某一特定内核源码树所构成的,版本不一致可能会引起潜在的问题。