3. 第一个内核模块:Hello World 模块编写、编译、加载与卸载

本章是内核模块编程的“零号里程碑”。你将亲手编写一个最简单的内核模块,完成从源代码到加载运行、再到卸载清理的完整生命周期。通过这个最小可工作示例,你将掌握内核模块的基本骨架、编译系统的配置方法以及核心管理命令的使用。

3.1 内核模块的基本结构

一个标准的内核模块至少包含两个关键函数:模块初始化入口和模块卸载出口。它们分别对应模块被加载和卸载时内核自动调用的回调。

核心要点:内核模块运行在内核空间,不能直接调用标准C库(如 printf),必须使用内核提供的专用 API(如 printk)。模块的入口和出口通过 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:内核构建系统的目标,用于编译外部模块。
注意:编译内核模块需要安装内核头文件或内核源码包。在 Ubuntu/Debian 上执行 sudo apt install linux-headers-$(uname -r),在 CentOS/RHEL 上执行 sudo yum install kernel-devel。如果缺少头文件,编译会报错。

编译过程示意图如下:

内核模块编译流程 hello.c 模块源代码 Makefile 构建规则 Kernel Build System /lib/modules/.../build hello.ko 可加载模块 执行 make 命令后,内核构建系统自动处理预处理、编译、链接,最终生成 .ko 文件

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 图展示了内核模块从编写到卸载的完整生命周期:

内核模块生命周期 编写源码 hello.c + Makefile 编译 make → hello.ko 加载 insmod hello.ko 运行 内核空间执行 卸载 rmmod hello 关键命令总结 编译:make | 加载:sudo insmod hello.ko | 查看:lsmod | grep hello | 卸载:sudo rmmod hello 日志查看:dmesg | tail | 清理:make clean

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 查看内核日志,定位模块加载/卸载过程中的问题。