1. 内核模块基础概念

1.1 什么是内核模块

内核模块(Kernel Module)是一段可以在 Linux 内核运行时动态加载和卸载的代码,用于扩展内核功能而不需要重新编译整个内核或重启系统。模块通常用于实现设备驱动、文件系统、网络协议、系统调用等内核级功能。

核心特征: 内核模块是可动态插拔的内核扩展单元,通过 insmod / modprobe 加载,通过 rmmod 卸载。

内核模块在 Linux 系统中的位置 用户空间(User Space) 系统调用接口(System Call Interface) 内核空间(Kernel Space) 模块 A(驱动) 模块 B(文件系统) 模块 C(网络协议) 模块 D(安全) 模块运行在内核空间,拥有最高特权级(Ring 0),可直接访问硬件和内核数据结构

1.2 内核模块与用户态程序的区别

内核模块与普通用户态程序在运行环境、权限、开发方式等方面存在本质差异。理解这些区别是正确编写和调试模块的前提。

对比维度 内核模块 用户态程序
运行空间 内核空间(Ring 0) 用户空间(Ring 3)
入口函数 module_init() / module_exit() main()
内存访问 直接访问物理内存、内核地址空间 只能访问虚拟地址空间,受 MMU 保护
系统调用 不能直接调用系统调用,使用内核 API 通过系统调用接口请求内核服务
标准库 不能链接 glibc,使用内核提供的 printk、kmalloc 等 可链接标准 C 库,使用 printf、malloc 等
调试方式 通过 dmesg、kgdb、ftrace 等内核调试工具 通过 gdb、strace、valgrind 等用户态调试工具
崩溃影响 模块崩溃可能导致整个系统 panic 或死机 进程崩溃通常只影响自身,系统继续运行
加载方式 insmod / modprobe 动态加载 通过 shell 或 exec 启动进程
许可证要求 通常需要 GPL 兼容许可证(MODULE_LICENSE("GPL")) 无特殊许可证限制

⚠️ 关键风险: 内核模块运行在最高特权级,一个空指针解引用或缓冲区溢出就可能导致整个系统崩溃。开发内核模块时必须格外谨慎,做好边界检查和错误处理。

内核模块 vs 用户态程序:运行环境对比 用户态程序 glibc / 标准 C 库 系统调用(syscall) 虚拟地址空间(MMU 隔离) 崩溃 → 仅进程退出 内核模块 内核 API(printk, kmalloc 等) 直接访问内核地址空间 物理内存 / 硬件寄存器 崩溃 → 内核 panic / 系统死机

1.3 模块生命周期

内核模块从编写完成到最终卸载,经历一系列明确的阶段。理解生命周期有助于定位加载失败、运行异常等问题。

生命周期阶段总览

阶段 触发动作 关键函数 / 事件 典型状态
1. 编译 make Kbuild 系统编译生成 .ko 文件 ELF 目标文件
2. 加载 insmod / modprobe module_init() 被调用 Live(活动)
3. 初始化 内核模块加载器 执行 init 函数,注册设备/文件系统等 Live(活动)
4. 运行 系统正常运行 模块提供服务(驱动读写、协议处理等) Live(活动)
5. 卸载准备 rmmod 检查模块引用计数是否为 0 Going-away(正在移除)
6. 卸载 内核模块卸载器 module_exit() 被调用,释放资源 Deleted(已删除)
内核模块生命周期流程图 ① 编译(.ko) ② 加载(insmod) ③ 初始化(init) ④ 运行(Live) ⑤ 卸载准备(引用计数检查) ⑥ 卸载(exit) 关键检查点 • 模块许可证(GPL) • 符号依赖(symbol dependency) • 版本校验(vermagic) • 引用计数(refcount)

模块加载与卸载的详细流程

当执行 insmod mymodule.ko 时,内核模块加载器执行以下步骤:

  1. 文件读取: 将 .ko ELF 文件读入内核内存。
  2. 符号解析: 解析模块中引用的外部符号(函数、变量),在内核符号表中查找匹配项。
  3. 版本校验: 检查模块的 vermagic 字符串是否与当前内核版本一致。
  4. 内存分配: 为模块代码段、数据段分配内核内存空间。
  5. 重定位: 根据加载地址修正模块内的绝对地址引用。
  6. 调用 init 函数: 执行 module_init() 指定的初始化函数。
  7. 注册模块: 将模块加入内核模块链表,更新引用计数。

💡 实用技巧: 使用 lsmod 查看当前已加载的模块列表;使用 modinfo mymodule.ko 查看模块的详细信息(作者、描述、许可证、依赖等)。

一个完整的内核模块示例

// hello.c — 最简单的内核模块
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Kernel Module loaded!\n");
    return 0;  // 返回 0 表示初始化成功
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, Kernel Module unloaded!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Kernel Expert");
MODULE_DESCRIPTION("A simple Hello World kernel module");
MODULE_VERSION("1.0");

编译该模块的 Makefile:

obj-m += hello.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

关键点总结:

  • __init 标记初始化函数,加载完成后该函数占用的内存会被释放。
  • __exit 标记卸载函数,仅在模块编译为内置(built-in)时有效。
  • MODULE_LICENSE("GPL") 是必需的,否则内核会标记模块为“污染”(tainted)。
  • printk 是内核空间的“printf”,输出可通过 dmesg 查看。

模块引用计数机制

内核使用引用计数(reference count)来跟踪模块是否正在被使用。当引用计数大于 0 时,rmmod 会失败并返回 EBUSY。

操作 引用计数变化 说明
insmod 0 → 1 模块加载成功,初始引用计数为 1
打开模块设备文件 +1 用户态程序打开模块对应的设备节点
关闭模块设备文件 -1 用户态程序关闭设备节点
其他模块依赖本模块 +1 模块 B 使用了模块 A 导出的符号
rmmod 1 → 0 引用计数为 0 时才能卸载

⚠️ 常见故障: 模块卸载失败最常见的原因是引用计数不为 0。使用 lsmod 查看 Used by 列,确认哪些进程或模块还在使用当前模块。使用 fuser -v /dev/your_device 查找占用设备文件的进程。

模块引用计数变化示例 insmod → ref=1 open → ref=2 close → ref=1 rmmod → ref=0 引用计数为 0 是卸载的必要条件 • 如果 close 后仍有其他模块依赖,ref 可能仍 > 1,此时 rmmod 会返回 EBUSY • 使用 lsmod 查看 Used by 列,确认依赖关系后再卸载

模块状态查看命令

命令 功能 示例输出
lsmod 列出所有已加载模块 hello 16384 0 - Live 0x0000000000000000
modinfo <module> 显示模块详细信息 显示作者、描述、许可证、依赖等
dmesg 查看内核日志(含 printk 输出) [12345.678] Hello, Kernel Module loaded!
cat /proc/modules 查看模块内存占用和引用计数 hello 16384 0 - Live 0xffffffffc0000000
cat /proc/kallsyms 查看内核符号表(含模块导出符号) ffffffffc0000000 t hello_init

💡 调试建议: 在模块开发阶段,建议在 init 和 exit 函数中增加 printk 日志输出,配合 dmesg -w 实时查看加载/卸载过程中的内核消息,快速定位问题。