3、第一个内核模块:Hello World模块编写、Makefile编写、模块加载与卸载、printk调试
本章是Linux内核模块开发的起点。我们将从零编写一个最简单的“Hello World”内核模块,并配套编写标准的Kbuild Makefile,完成模块的编译、加载、查看内核日志以及卸载的全流程。通过本章,你将掌握内核模块的基本骨架和printk调试方法,为后续网卡驱动开发打下坚实基础。
3.1 内核模块的基本结构
一个标准的内核模块至少包含以下四个要素:
| 要素 | 宏/函数 | 说明 |
|---|---|---|
| 模块许可证声明 | MODULE_LICENSE("GPL") |
必须声明,否则内核会标记模块为“污染” |
| 模块作者描述 | MODULE_AUTHOR("Your Name") |
可选,但推荐填写 |
| 模块初始化入口 | module_init(hello_init) |
模块加载时自动调用 |
| 模块卸载出口 | module_exit(hello_exit) |
模块卸载时自动调用 |
关键点: 内核模块运行在内核空间,不能直接调用C标准库函数(如printf)。内核提供的输出函数是printk,其日志通过dmesg查看。
3.2 Hello World模块代码实现
创建文件 hello.c,内容如下:
#include <linux/module.h> // 包含 MODULE_* 宏
#include <linux/kernel.h> // 包含 printk 和 KERN_INFO
#include <linux/init.h> // 包含 module_init / module_exit
static int __init hello_init(void)
{
printk(KERN_INFO "Hello World: 模块已加载!\n");
printk(KERN_INFO "当前内核版本: %s\n", UTS_RELEASE);
return 0; // 返回0表示初始化成功
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye World: 模块已卸载!\n");
}
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux网卡驱动开发65讲");
MODULE_DESCRIPTION("第一个内核模块 - Hello World");
MODULE_VERSION("1.0");
__init 和 __exit 标记: __init 告诉内核该函数仅在初始化阶段使用,初始化完成后可以释放占用的内存;__exit 标记在模块卸载时使用,如果模块被编译进内核(非动态加载),该函数会被忽略。
3.3 Makefile编写规范
内核模块的编译必须使用内核源码树提供的Kbuild系统。创建 Makefile,内容如下:
# 指定模块目标名称
obj-m := hello.o
# 当前内核源码树路径(通常自动获取)
KDIR := /lib/modules/$(shell uname -r)/build
# 当前目录
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
| Makefile变量 | 含义 |
|---|---|
obj-m |
指定要编译成模块的目标文件(.o) |
KDIR |
内核源码树路径,指向当前运行内核的构建目录 |
M=$(PWD) |
告诉Kbuild在指定目录下构建模块 |
modules |
Kbuild目标,编译所有obj-m指定的模块 |
常见错误: Makefile中$(MAKE)前面的缩进必须是Tab,不能是空格。否则make会报“missing separator”错误。
3.4 编译与加载卸载流程
执行以下命令完成完整流程:
# 1. 编译模块
make
# 2. 查看生成的模块文件
ls -l hello.ko
# 3. 加载模块(需要root权限)
sudo insmod hello.ko
# 4. 查看已加载模块列表
lsmod | grep hello
# 5. 查看内核日志
dmesg | tail -5
# 6. 卸载模块
sudo rmmod hello
# 7. 再次查看卸载日志
dmesg | tail -3
预期输出(dmesg日志):
[ 1234.567890] Hello World: 模块已加载!
[ 1234.567895] 当前内核版本: 5.15.0-91-generic
[ 5678.901234] Goodbye World: 模块已卸载!
3.5 printk日志级别详解
printk 是内核空间的“printf”,它通过日志级别控制消息的显示行为。常用日志级别如下:
| 宏 | 数值 | 含义 | 终端显示 |
|---|---|---|---|
KERN_EMERG |
0 | 紧急事件,系统崩溃前 | 通常显示 |
KERN_ALERT |
1 | 需要立即处理 | 通常显示 |
KERN_CRIT |
2 | 严重条件 | 通常显示 |
KERN_ERR |
3 | 错误条件 | 通常显示 |
KERN_WARNING |
4 | 警告条件 | 可能显示 |
KERN_NOTICE |
5 | 正常但重要 | 可能显示 |
KERN_INFO |
6 | 信息性消息 | 不显示到终端 |
KERN_DEBUG |
7 | 调试消息 | 不显示到终端 |
调试技巧: 在开发网卡驱动时,建议使用 KERN_INFO 或 KERN_DEBUG 级别,避免刷屏。通过 dmesg -w 可以实时跟踪内核日志输出。
3.6 内核模块加载与卸载流程图
下图展示了从编译到卸载的完整生命周期:
3.7 常见问题与避坑指南
- 权限问题:
insmod和rmmod需要 root 权限,使用sudo执行。 - 内核版本不匹配: 编译模块的内核版本必须与运行内核版本一致,否则加载会报
Invalid module format错误。 - 符号冲突: 模块中的全局函数名不要与内核已有符号重名,建议使用
static限制作用域。 - printk 看不到输出: 检查是否使用了
KERN_INFO级别,并确认通过dmesg查看而非终端。 - 模块卸载失败: 如果
module_init返回非0值,模块加载失败;如果模块被其他模块依赖,无法卸载。
生产建议: 在网卡驱动开发中,建议在 module_init 中注册网络设备,在 module_exit 中注销并释放资源。使用 printk(KERN_DEBUG ...) 输出调试信息,避免在生产环境中产生过多日志。
3.8 本章小结
通过本章,你已掌握:
- 内核模块的标准骨架:
module_init/module_exit/MODULE_LICENSE - Kbuild Makefile 的编写规则与编译流程
- insmod / rmmod / lsmod / dmesg 命令的配合使用
- printk 日志级别及其在调试中的应用