2. 第一个内核模块 Hello World:编写最简单的模块代码、Makefile 基础结构、编译与加载测试
本章是驱动开发实战的起点。我们将从零编写一个最简单的内核模块,它不操作硬件、不注册设备,只在加载和卸载时打印一条消息。通过这个最小实例,你将掌握内核模块的代码骨架、Makefile 的编写规则,以及编译、加载、卸载、查看日志的完整流程。
学习目标:完成本章后,你能够独立编写一个可编译、可加载、可卸载的内核模块,并理解模块的入口/出口函数、许可证声明、Makefile 中 obj-m 与 KERNELDIR 的作用。
2.1 内核模块的最小代码结构
一个 Linux 内核模块至少需要两个函数:模块初始化函数(加载时调用)和模块退出函数(卸载时调用)。此外,还需要使用 module_init() 和 module_exit() 宏将这两个函数注册到内核模块框架中。
下面是最简的 hello.c 代码:
#include <linux/module.h> // 包含模块相关的宏和函数
#include <linux/kernel.h> // 包含 printk 等内核打印函数
#include <linux/init.h> // 包含 __init 和 __exit 标记宏
static int __init hello_init(void)
{
printk(KERN_INFO "Hello World from kernel module!\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("Your Name"); // 作者信息(可选)
MODULE_DESCRIPTION("A simple Hello World module"); // 描述(可选)
MODULE_VERSION("1.0"); // 版本号(可选)
关键点解析:
__init和__exit是内核标记宏,告诉内核该函数在初始化/退出后可以释放内存,减少运行时开销。printk是内核空间的打印函数,KERN_INFO指定日志级别,消息会写入内核日志缓冲区,可通过dmesg查看。MODULE_LICENSE("GPL")是必须的,缺少此声明会导致模块加载时内核报“tainted kernel”警告,且某些内核符号无法使用。
2.2 Makefile 基础结构
内核模块的编译不能使用普通的 gcc 命令,必须借助内核的构建系统(Kbuild)。Makefile 的核心任务是告诉 Kbuild 要编译哪个源文件,以及去哪里找内核头文件和编译规则。
下面是与 hello.c 配套的 Makefile:
# 指定要编译的目标模块名称(不带 .ko 后缀)
obj-m := hello.o
# 指定当前内核源码树的路径(根据实际环境修改)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
# 指定当前源码目录
PWD := $(shell pwd)
# 默认目标:编译模块
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
# 清理目标:清除编译产物
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
注意:Makefile 中的缩进必须使用 Tab 键,不能使用空格。这是 Make 语法的硬性要求,否则会报“missing separator”错误。
Makefile 各部分的含义如下表所示:
| 变量/目标 | 作用 | 说明 |
|---|---|---|
obj-m := hello.o |
指定要编译成模块的目标文件 | Kbuild 会根据此变量将 hello.c 编译为 hello.ko |
KERNELDIR |
内核源码树路径 | /lib/modules/$(uname -r)/build 是发行版安装内核头文件后的标准路径 |
PWD |
当前工作目录 | 传递给 Kbuild,告诉它源文件在哪里 |
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules |
调用内核构建系统编译模块 | -C 进入内核目录,M= 指定模块源码路径 |
2.3 编译与加载测试流程
将 hello.c 和 Makefile 放在同一个目录下,执行以下步骤:
- 编译模块:在终端执行
make,成功后会生成hello.ko、hello.o、hello.mod.c等文件。 - 加载模块:使用
sudo insmod hello.ko将模块插入内核。 - 查看模块信息:
lsmod | grep hello确认模块已加载;modinfo hello.ko查看模块的许可证、作者、描述等信息。 - 查看内核日志:执行
dmesg | tail -5,应看到Hello World from kernel module!消息。 - 卸载模块:
sudo rmmod hello,再次查看dmesg应看到Goodbye, kernel module unloaded.消息。 - 清理编译产物:
make clean删除.o、.ko、.mod.c等临时文件。
常见问题:如果
dmesg 没有输出,可能是日志级别被过滤。尝试 dmesg -l info 或 dmesg -l 6 查看 info 级别的消息。如果模块加载失败,使用 dmesg 查看内核给出的具体错误原因。
2.4 模块编译与加载流程 SVG 图
下图展示了从源代码到内核模块加载的完整流程:
2.5 模块信息查看与常见错误
加载模块后,可以使用以下命令获取模块的详细信息:
lsmod:列出当前系统中所有已加载的模块,包括模块大小、被其他模块引用的次数。modinfo hello.ko:显示模块的 license、author、description、依赖关系、参数等信息。cat /proc/modules | grep hello:查看模块在内核中的内存地址和状态。
初学者常遇到的错误及解决方法:
| 错误现象 | 可能原因 | 解决方法 |
|---|---|---|
make: *** /lib/modules/.../build: No such file or directory |
未安装内核头文件或内核开发包 | 执行 sudo apt install linux-headers-$(uname -r)(Ubuntu/Debian)或 sudo yum install kernel-devel(RHEL/CentOS) |
insmod: ERROR: could not insert module hello.ko: Operation not permitted |
未使用 root 权限 | 使用 sudo insmod hello.ko |
insmod: ERROR: could not insert module hello.ko: Invalid module format |
模块编译时的内核版本与当前运行内核版本不一致 | 确保 KERNELDIR 指向当前运行内核的源码树,重新编译 |
dmesg 无输出 |
日志级别被过滤,或 printk 未正确调用 |
尝试 dmesg -l info 或 dmesg -T | tail -10 |
2.6 本章小结
通过本章的学习,你已经完成了第一个内核模块的编写、编译和加载测试。这个最小实例虽然简单,但它包含了内核模块开发的所有核心要素:
- 代码骨架:
module_init/module_exit宏、__init/__exit标记、许可证声明。 - Makefile 结构:
obj-m变量、KERNELDIR路径、make -C调用 Kbuild。 - 编译与测试流程:
make→insmod→dmesg→rmmod→make clean。