3. 第一个内核模块:Hello World 模块编写、编译、加载与卸载
本章是内核模块编程的“零号里程碑”。你将亲手编写一个最简单的内核模块,完成从源代码到加载运行、再到卸载清理的完整生命周期。通过这个最小可工作示例,你将掌握内核模块的基本骨架、编译系统的配置方法以及核心管理命令的使用。
3.1 内核模块的基本结构
一个标准的内核模块至少包含两个关键函数:模块初始化入口和模块卸载出口。它们分别对应模块被加载和卸载时内核自动调用的回调。
module_init() 和 module_exit() 宏向内核注册。
下表列出了 Hello World 模块中每个代码元素的作用:
| 代码元素 | 作用 | 所属类别 |
|---|---|---|
#include <linux/module.h> |
包含模块核心宏(module_init、module_exit) | 头文件 |
#include <linux/kernel.h> |
包含内核打印函数 printk | 头文件 |
#include <linux/init.h> |
包含 __init 和 __exit 标记宏 | 头文件 |
static int __init hello_init(void) |
模块加载时自动执行的初始化函数 | 入口函数 |
static void __exit hello_exit(void) |
模块卸载时自动执行的清理函数 | 出口函数 |
module_init(hello_init) |
将 hello_init 注册为模块入口 | 注册宏 |
module_exit(hello_exit) |
将 hello_exit 注册为模块出口 | 注册宏 |
MODULE_LICENSE("GPL") |
声明模块许可证,必须与内核兼容 | 模块信息 |
MODULE_AUTHOR("Your Name") |
声明模块作者 | 模块信息 |
MODULE_DESCRIPTION("A simple Hello World module") |
声明模块描述 | 模块信息 |
3.2 编写 Hello World 源代码
创建一个名为 hello.c 的文件,输入以下代码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello World: module loaded.\n");
return 0; // 返回0表示初始化成功
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye World: module unloaded.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Expert");
MODULE_DESCRIPTION("A simple Hello World kernel module");
printk 的 KERN_INFO 是日志级别宏,表示信息性消息。你可以通过 dmesg 命令查看内核日志输出。与用户空间的 printf 不同,printk 不会直接输出到终端,而是写入内核环形缓冲区。
3.3 编写 Makefile 编译模块
内核模块的编译不能使用普通的 gcc 命令,必须借助内核的构建系统(Kbuild)。你需要编写一个 Makefile,利用内核源码树中的编译规则来生成 .ko 文件。
在 hello.c 同目录下创建 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
Makefile 关键点解释:
obj-m += hello.o:告诉内核构建系统将hello.c编译成一个可加载模块(.ko)。-C /lib/modules/$(shell uname -r)/build:切换到当前运行内核的构建目录。M=$(PWD):指定模块源码所在的当前目录。modules:内核构建系统的目标,用于编译外部模块。
sudo apt install linux-headers-$(uname -r),在 CentOS/RHEL 上执行 sudo yum install kernel-devel。如果缺少头文件,编译会报错。
编译过程示意图如下:
3.4 编译模块
在终端中进入模块源码目录,执行 make 命令:
$ make
make -C /lib/modules/5.15.0-91-generic/build M=/home/user/hello modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-91-generic'
CC [M] /home/user/hello/hello.o
MODPOST /home/user/hello/Module.symvers
LD [M] /home/user/hello/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-91-generic'
编译成功后,目录下会生成以下关键文件:
hello.ko:最终的内核模块文件(可加载对象)。hello.o:编译后的目标文件。Module.symvers:符号版本信息文件。.hello.ko.cmd/.hello.o.cmd:编译命令记录文件(可忽略)。
3.5 加载模块:insmod
insmod 命令用于将编译好的 .ko 文件插入到内核中。加载时需要 root 权限。
$ sudo insmod hello.ko
如果命令没有输出任何错误信息,说明模块加载成功。此时内核会调用 hello_init 函数,执行 printk 输出日志。你可以通过 dmesg 查看最后几行内核日志:
$ dmesg | tail -5
[ 1234.567890] Hello World: module loaded.
.ko 文件与当前内核版本不匹配、缺少依赖模块、内核配置禁止加载未签名模块(Secure Boot 或 CONFIG_MODULE_SIG_FORCE)。使用 dmesg 查看错误详情是首要排查手段。
3.6 查看已加载模块:lsmod
lsmod 命令列出当前内核中所有已加载的模块及其信息。执行后你会看到类似如下的输出:
$ lsmod | grep hello
hello 16384 0
输出字段含义:
| 字段 | 含义 | 示例值 |
|---|---|---|
| Module | 模块名称 | hello |
| Size | 模块占用的内存大小(字节) | 16384 |
| Used by | 被其他模块或内核组件引用的次数 | 0 |
lsmod 实际上读取的是 /proc/modules 虚拟文件的内容,你也可以直接查看该文件:
$ cat /proc/modules | grep hello
hello 16384 0 - Live 0x0000000000000000
3.7 卸载模块:rmmod
当不再需要模块时,使用 rmmod 命令将其从内核中移除。卸载同样需要 root 权限。
$ sudo rmmod hello
卸载成功后,内核会调用 hello_exit 函数。再次查看内核日志:
$ dmesg | tail -5
[ 1234.567890] Hello World: module loaded.
[ 1235.123456] Goodbye World: module unloaded.
lsmod 中 Used by 不为 0),rmmod 会失败并提示 Module is in use。此时需要先卸载依赖它的模块,或使用 rmmod -f 强制卸载(不推荐,可能导致内核不稳定)。
3.8 模块生命周期管理流程
下面的 SVG 图展示了内核模块从编写到卸载的完整生命周期:
3.9 常见错误与排查
初学者在编写和加载第一个内核模块时,常遇到以下问题:
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
make: *** /lib/modules/.../build: No such file or directory |
未安装内核头文件 | 安装 linux-headers-$(uname -r) |
insmod: ERROR: could not insert module hello.ko: Invalid module format |
.ko 与当前内核版本不匹配 | 确认使用当前内核的头文件重新编译 |
insmod: ERROR: could not insert module hello.ko: Operation not permitted |
未使用 root 权限,或 Secure Boot 阻止 | 使用 sudo,或在 BIOS 中关闭 Secure Boot |
insmod: ERROR: could not insert module hello.ko: Unknown symbol in module |
模块引用了未导出的内核符号 | 检查 Module.symvers,确认符号可用 |
rmmod: ERROR: Module hello is in use |
有其他模块或进程依赖 hello | 先卸载依赖模块,或重启后重试 |
make clean 再重新 make,避免残留的目标文件导致编译错误。加载新模块前,先用 rmmod 卸载旧版本(如果已加载)。
3.10 本章小结
通过本章的学习,你已经完成了第一个内核模块的完整实践:
- 掌握了内核模块的标准代码骨架:
module_init、module_exit、许可证声明。 - 学会了使用内核构建系统编译模块,理解了
obj-m和make -C的作用。 - 熟练运用
insmod、lsmod、rmmod三个核心管理命令。 - 能够通过
dmesg查看内核日志,定位模块加载/卸载过程中的问题。