2. 第一个内核模块:编写并加载一个简单的“Hello World”内核模块,理解 module_init 和 module_exit 机制
2.1 本章目标
- 掌握内核模块的基本代码结构
- 理解
module_init与module_exit宏的注册与回调机制 - 学会使用 Makefile 编译内核模块
- 掌握
insmod、rmmod、dmesg等基本操作
2.2 内核模块的骨架
一个最简的内核模块必须包含两个核心宏:module_init 和 module_exit。它们分别指定模块加载到内核时执行的初始化函数,以及模块卸载时执行的清理函数。
关键概念: 内核模块运行在 内核空间,不能直接调用标准 C 库函数(如 printf),必须使用内核提供的
printk 函数输出日志。
2.2.1 代码结构
#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: 模块已加载\n");
return 0; // 返回 0 表示加载成功
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Hello World: 模块已卸载\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL"); // 许可证声明(必须)
MODULE_AUTHOR("Your Name"); // 作者信息
MODULE_DESCRIPTION("A simple Hello World module"); // 模块描述
2.3 module_init 与 module_exit 机制
这两个宏本质上是编译器属性标记,它们将函数指针放入特定的 ELF 段(.init.text 和 .exit.text)。内核在加载模块时,会从模块文件中提取 module_init 注册的函数地址并执行;卸载时则调用 module_exit 注册的函数。
提示:
__init 标记告诉内核:该函数仅在初始化阶段使用,初始化完成后可以释放其占用的内存。__exit 标记则用于卸载路径,如果模块被编译进内核(而非动态加载),该函数会被忽略。
2.3.1 加载与卸载流程
2.4 编译内核模块: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
| 变量/目标 | 说明 |
|---|---|
obj-m := hello.o |
告诉 Kbuild 将 hello.c 编译成可动态加载的内核模块 hello.ko |
KDIR |
指向当前运行内核的构建目录(包含 Makefile 和头文件) |
-C $(KDIR) |
切换到内核源码目录执行 make |
M=$(PWD) |
指定模块源码所在路径,make 会返回该目录编译模块 |
modules |
内核 Makefile 的目标,用于编译外部模块 |
常见错误: 如果系统没有安装内核头文件(
linux-headers-*),KDIR 路径不存在,编译会失败。Ubuntu/Debian 下执行 sudo apt install linux-headers-$(uname -r) 安装。
2.5 编译与测试步骤
- 编写代码: 将 2.2.1 节的代码保存为
hello.c,Makefile 保存为Makefile。 - 编译: 在终端执行
make,生成hello.ko。 - 加载模块:
sudo insmod hello.ko(无输出表示成功)。 - 查看日志:
dmesg | tail,应看到Hello World: 模块已加载。 - 卸载模块:
sudo rmmod hello。 - 再次查看日志:
dmesg | tail,应看到Hello World: 模块已卸载。 - 清理:
make clean删除编译产生的中间文件。
2.6 模块信息与工具
| 命令 | 功能 | 示例 |
|---|---|---|
insmod |
加载模块(需指定 .ko 文件路径) | sudo insmod hello.ko |
rmmod |
卸载模块(只需模块名,不含 .ko) | sudo rmmod hello |
modprobe |
智能加载/卸载(自动处理依赖) | sudo modprobe hello |
lsmod |
列出当前已加载的所有模块 | lsmod | grep hello |
modinfo |
查看模块的详细信息(作者、描述、参数等) | modinfo hello.ko |
dmesg |
查看内核环形缓冲区日志 | dmesg -H(人类可读格式) |
2.7 常见问题与避坑
- 权限问题: 加载和卸载模块需要 root 权限,必须使用
sudo。 - 版本不匹配: 模块必须与当前运行的内核版本完全一致,否则
insmod会报Invalid module format错误。 - printk 看不到输出: 检查日志级别,使用
dmesg -H或journalctl -k查看;某些系统可能将KERN_INFO级别的日志过滤掉。 - 模块名冲突: 如果内核已存在同名模块,
insmod会失败,先用lsmod确认。 - 忘记 MODULE_LICENSE: 不声明许可证会导致内核报
kernel tainted警告,且某些内核功能不可用。
2.8 本章小结
通过本章,你完成了第一个内核模块的编写、编译、加载和卸载全流程。核心要点是:
module_init和module_exit是内核模块的入口和出口,由内核在特定时机回调。- 内核模块使用
printk输出日志,通过dmesg查看。 - Makefile 依赖内核构建系统
Kbuild,必须指定正确的KDIR。 - 模块的加载/卸载操作需要 root 权限,且版本必须与内核匹配。