3. 第一个内核模块
本章是驱动开发的“零号里程碑”。你将亲手编写一个最简单的内核模块,并掌握从源码到 .ko 文件的完整编译流程。这个模块不操作硬件、不注册设备,只在内核空间打印一条消息——但它验证了你的开发环境、工具链和内核构建系统是否就绪。
3.1 内核模块的基本结构
一个标准的内核模块至少包含两个宏:模块许可证声明和入口/出口函数。与用户空间程序不同,内核模块没有 main(),而是通过 module_init() 和 module_exit() 指定加载和卸载时的回调。
关键概念:内核模块运行在内核空间,拥有最高特权级。任何崩溃都会导致整个系统挂掉——所以第一个模块务必简单、正确。
3.1.1 模块组成要素
| 要素 | 宏/函数 | 说明 |
|---|---|---|
| 入口函数 | module_init(hello_init) |
insmod 加载时调用,返回 0 表示成功 |
| 出口函数 | module_exit(hello_exit) |
rmmod 卸载时调用,清理资源 |
| 许可证 | MODULE_LICENSE("GPL") |
必须声明,否则内核会标记为“污染” |
| 作者/描述 | MODULE_AUTHOR / MODULE_DESCRIPTION |
可选,但推荐填写以方便维护 |
3.2 编写 Hello World 模块源码
创建一个文件 hello.c,内容如下。注意头文件 <linux/module.h> 和 <linux/init.h> 是所有模块都必须包含的基础头文件。
// hello.c — 第一个内核模块
#include <linux/module.h> // 模块核心宏
#include <linux/init.h> // __init / __exit 宏
#include <linux/kernel.h> // printk 内核打印函数
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 "再见,内核空间!模块已卸载。\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux驱动课程");
MODULE_DESCRIPTION("一个简单的Hello World内核模块示例");
MODULE_VERSION("1.0");
💡 提示:
printk 是内核版的 printf,KERN_INFO 是日志级别。消息会写入内核环形缓冲区,可通过 dmesg 查看。
3.3 模块生命周期 SVG 流程
下图展示了从加载到卸载的完整生命周期,以及内核空间与用户空间的边界。
3.4 Makefile 编写:内核构建系统
内核模块不能直接用 gcc 编译,必须使用内核提供的 Kbuild 构建系统。Makefile 的核心是调用内核源码树的 make modules 目标。
3.4.1 标准 Makefile 模板
在 hello.c 同目录下创建 Makefile,内容如下:
# Makefile — 内核模块编译
# 目标模块名(不带 .ko 后缀)
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
⚠️ 常见错误:
KDIR 必须指向已编译过的内核源码树或内核头文件包。如果系统未安装内核头文件,先执行 sudo apt install linux-headers-$(uname -r)(Ubuntu/Debian)或 sudo yum install kernel-devel(RHEL/CentOS)。
3.4.2 Makefile 关键变量解析
| 变量 | 含义 | 示例值 |
|---|---|---|
obj-m |
指定要编译成模块的目标文件(不含路径) | hello.o |
KDIR |
内核构建目录路径 | /lib/modules/6.8.0-45-generic/build |
M=$(PWD) |
告诉 Kbuild 返回当前目录编译模块 | M=/home/user/driver/hello |
-C |
切换到内核目录执行 Makefile | — |
3.5 编译与测试步骤
按照以下顺序执行,验证你的第一个内核模块:
- 准备源码:确保
hello.c和Makefile在同一目录。 - 执行编译:运行
make,输出应包含Building modules, stage 2.和MODPOST等字样。 - 检查产物:确认生成了
hello.ko文件。 - 加载模块:
sudo insmod hello.ko(无输出表示成功)。 - 查看日志:
dmesg | tail -5,应看到Hello, World! 模块已加载。 - 卸载模块:
sudo rmmod hello。 - 再次查看日志:
dmesg | tail -3,应看到再见,内核空间!模块已卸载。
✅ 验证清单:
lsmod | grep hello在加载后应列出模块。modinfo hello.ko可查看作者、描述、许可证等元信息。- 如果
insmod返回Operation not permitted,检查是否使用sudo。
3.6 编译产物与目录结构
编译成功后,目录下会生成多个中间文件。下表说明各文件的用途:
| 文件 | 类型 | 说明 |
|---|---|---|
hello.ko |
最终模块 | 可动态加载的内核对象文件 |
hello.o |
目标文件 | 编译后的二进制,未链接 |
hello.mod.c |
自动生成 | 模块元信息(许可证、依赖等)的 C 源码 |
hello.mod.o |
目标文件 | hello.mod.c 编译后的对象 |
Module.symvers |
符号表 | 导出的内核符号版本信息(多模块依赖时重要) |
.hello.ko.cmd |
命令记录 | Kbuild 用于增量编译的依赖追踪文件 |
3.7 常见编译错误与解决
- “No rule to make target ‘modules’”:
KDIR路径错误或内核头文件未安装。 - “implicit declaration of function ‘printk’”:缺少
#include <linux/kernel.h>。 - “Module license ‘unspecified’ taints kernel”:未添加
MODULE_LICENSE("GPL")。 - “Invalid module format”:模块编译时的内核版本与运行内核版本不一致。用
uname -r确认版本。
🔧 调试技巧:在 Makefile 中添加
CFLAGS_hello.o := -DDEBUG 可以启用模块内的 pr_debug() 输出,配合 dmesg -n 7 查看所有调试信息。
3.8 本章小结
通过本章,你完成了驱动开发的“Hello World”里程碑:
- 理解了内核模块的
init/exit生命周期。 - 掌握了 Kbuild 构建系统的 Makefile 编写方法。
- 成功编译并动态加载/卸载了第一个
.ko模块。 - 学会了使用
dmesg和modinfo验证模块行为。
这个基础流程将贯穿整个课程——后续所有驱动项目都遵循同样的“编写源码 → 编写 Makefile → make → insmod → 测试 → rmmod”循环。