4. 模块加载与卸载命令:insmod、rmmod、modprobe 的底层原理与日志查看

模块的加载与卸载是内核模块开发中最基础的操作。理解 insmod、rmmod 和 modprobe 的底层原理,能够帮助开发者在模块加载失败时快速定位问题根源。本节将深入剖析这三个命令的系统调用路径、内核处理流程,并讲解如何通过日志查看模块的 init 与 exit 函数执行状态。

4.1 模块加载命令:insmod 与 modprobe

insmod 和 modprobe 都可以加载内核模块,但它们的定位和底层机制有显著差异。

特性 insmod modprobe
依赖处理 不处理依赖,只加载指定文件 自动解析并加载所有依赖模块
参数传递 通过命令行直接传递参数 从配置文件 /etc/modprobe.d/ 读取参数
模块路径 必须指定完整路径或相对路径 从 /lib/modules/$(uname -r)/ 自动查找
底层调用 直接调用 init_module() 系统调用 先解析依赖,再逐个调用 init_module()
适用场景 调试阶段、加载自定义模块 生产环境、加载标准内核模块

4.1.1 insmod 的底层原理

insmod 的工作流程可以分为用户态和内核态两个阶段。用户态负责读取 ELF 文件、解析符号、重定位;内核态负责执行模块初始化。

关键系统调用: insmod 最终通过 init_module() 或 finit_module() 系统调用将模块二进制数据传递给内核。
insmod 加载模块完整流程 用户态阶段 1. 读取 .ko ELF 文件 解析节区、符号表 2. 符号重定位 解析未定义符号 3. 调用 init_module() 系统调用陷入内核 内核态阶段 4. 内核模块加载器 分配 module 结构体 5. 复制模块代码 到内核内存空间 6. 执行 module_init 调用 init 函数 7. 返回结果给用户态 成功返回 0,失败返回负错误码 ⚠ 调试要点 • 如果 init 函数返回非 0 值,内核会回滚模块加载并释放已分配资源 • 模块加载失败时,错误码通过 errno 返回,可使用 perror() 查看

4.1.2 modprobe 的依赖解析机制

modprobe 的强大之处在于它能够自动处理模块依赖。它通过读取 modules.dep 文件(由 depmod 生成)来获取模块之间的依赖关系。

依赖解析流程: modprobe 加载模块 A 时,会先递归查找 A 的所有依赖模块,按照依赖顺序从底层到上层依次加载,最后才加载目标模块 A。
modprobe 依赖解析与加载顺序 模块 A 模块 B 模块 C 模块 D 模块 E 模块 F 加载顺序(深度优先) 1. 先加载 D → 2. 加载 E → 3. 加载 B(依赖 D、E 已就绪) 4. 加载 F → 5. 加载 C(依赖 F 已就绪)→ 6. 最后加载 A(依赖 B、C 已就绪) 如果任意依赖模块加载失败,整个加载过程立即终止并回滚

4.2 模块卸载命令:rmmod

rmmod 用于卸载已加载的内核模块。其底层通过 delete_module() 系统调用实现,内核会检查模块的引用计数,只有当引用计数为 0 时才能成功卸载。

4.2.1 rmmod 的底层流程

  1. 检查引用计数: 内核检查模块的 refcnt 是否为 0,如果其他模块或进程正在使用该模块,卸载将被拒绝。
  2. 调用 module_exit: 执行模块的 exit 函数,释放模块占用的资源(如内存、设备号、文件系统等)。
  3. 释放模块内存: 内核回收模块代码段和数据段占用的内存空间。
  4. 更新模块链表: 从内核模块链表中移除该模块记录。
常见卸载失败原因:
  • 模块被其他模块依赖(如 A 依赖 B,卸载 B 会失败)
  • 模块被用户态进程占用(如打开了模块创建的设备文件)
  • exit 函数执行时发生错误或死锁

4.3 模块加载/卸载的日志查看

内核模块的 init 和 exit 函数执行状态会通过内核日志系统输出。开发者可以通过以下命令查看模块加载/卸载的详细日志。

4.3.1 日志查看命令

命令 说明 示例
dmesg 查看内核环形缓冲区日志 dmesg | tail -20
dmesg -w 实时监控内核日志(类似 tail -f) dmesg -w
dmesg -l info 只查看 info 级别日志 dmesg -l info | grep my_module
journalctl -k 使用 systemd-journald 查看内核日志 journalctl -k -f
cat /var/log/kern.log 查看持久化的内核日志文件 grep 'my_module' /var/log/kern.log

4.3.2 典型日志输出分析

以下是一个模块加载成功和失败的日志对比:

# 加载成功示例
[ 1234.567890] my_module: loading out-of-tree module taints kernel.
[ 1234.567895] my_module: module verification failed: signature and/or required key missing - tainting kernel
[ 1234.567912] my_module: Hello from init function!
[ 1234.567915] my_module: Module loaded successfully

# 加载失败示例(init 返回非 0)
[ 1235.123456] my_module: loading out-of-tree module taints kernel.
[ 1235.123460] my_module: Initialization failed: resource not available
[ 1235.123463] my_module: module my_module: init function returned error -16
[ 1235.123465] my_module: module my_module: loading module failed

# 卸载成功示例
[ 1236.789012] my_module: Goodbye from exit function!
[ 1236.789015] my_module: Module unloaded successfully

# 卸载失败示例(引用计数不为 0)
[ 1237.345678] my_module: Module is in use, cannot unload
[ 1237.345680] my_module: rmmod: ERROR: Module my_module is in use
日志级别与 printk: 模块中的 printk() 函数可以指定日志级别,例如 printk(KERN_INFO "Hello\n")。常用级别包括 KERN_EMERG (0)、KERN_ALERT (1)、KERN_CRIT (2)、KERN_ERR (3)、KERN_WARNING (4)、KERN_NOTICE (5)、KERN_INFO (6)、KERN_DEBUG (7)。级别数字越小,优先级越高。

4.4 实战:使用 strace 追踪模块加载系统调用

为了深入理解 insmod 和 rmmod 的底层行为,可以使用 strace 工具追踪它们的系统调用。

# 追踪 insmod 加载模块
$ strace -e trace=init_module,finit_module,open,read,write insmod ./my_module.ko 2>&1 | tail -20

# 输出示例
openat(AT_FDCWD, "./my_module.ko", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\1\0\3\0\1\0\0\0\0\0\0\0\0\0\0\0"..., 4096) = 4096
finit_module(3, "", 0)                  = 0

# 追踪 rmmod 卸载模块
$ strace -e trace=delete_module rmmod my_module 2>&1 | tail -10

# 输出示例
delete_module("my_module", O_NONBLOCK)  = 0
调试技巧: 当模块加载失败时,结合 dmesg 查看内核日志和 strace 查看系统调用返回值,可以快速定位问题是在用户态(如 ELF 解析失败)还是内核态(如 init 函数执行失败)。

4.5 常见加载/卸载故障排查清单

故障现象 可能原因 排查方法
insmod: ERROR: could not insert module 模块与内核版本不匹配 检查 uname -r 与模块编译时的内核版本
Unknown symbol 依赖模块未加载或符号未导出 使用 modprobe 自动加载依赖,或检查 /proc/kallsyms
Module is in use 引用计数不为 0 使用 lsmod 查看模块引用计数,lsof 查找占用进程
init function returned error init 函数执行失败 查看 dmesg 获取具体错误信息
rmmod: Module not found 模块名称错误或已卸载 使用 lsmod | grep 确认模块名称
Permission denied 非 root 用户操作 使用 sudo 或以 root 身份执行

掌握这些命令的底层原理和日志查看方法,是高效调试内核模块加载/卸载故障的基础。在实际开发中,建议始终在加载模块前开启 dmesg -w 实时监控日志,以便第一时间捕获 init 和 exit 函数的执行状态。