作者:huangwenbo
环境:Ubuntu 20.04 LTS,内核 5.15.0-139-generic
目标:编写、编译、加载并卸载第一个 Linux 内核模块
1. 本课学习目标
完成本课后,你应该能够:
- 理解用户空间程序与内核模块的区别
- 编写一个最简单的内核模块
hello.c - 使用 Kbuild 系统编译出
hello.ko - 用
insmod/rmmod加载和卸载模块 - 用
dmesg查看内核日志输出 - 独立排查常见编译与加载错误
2. 核心概念
2.1 用户程序 vs 内核模块
| 对比项 | 用户空间程序 | 内核模块 |
|---|---|---|
| 入口函数 | main() |
module_init() |
| 退出方式 | return / exit() |
module_exit() |
| 输出方式 | printf → 终端 |
printk → 内核日志 |
| 编译产物 | 可执行文件 | .ko(Kernel Object) |
| 运行位置 | 用户空间 | 内核空间 |
2.2 什么是内核模块
内核模块是可以动态加载到内核中的代码,不需要重新编译整个内核。
典型流程:
hello.c → make → hello.ko → insmod → 内核执行 hello_init()
→ dmesg 看到输出
rmmod → 内核执行 hello_exit()
2.3 out-of-tree 模块
我们这种在 kernel 源码树外部单独编译的模块,叫做 out-of-tree module(树外模块)。
加载时可能出现如下警告(开发阶段正常,可忽略):
hello: loading out-of-tree module taints kernel.
hello: module verification failed: signature and/or required key missing - tainting kernel
含义:
- out-of-tree:模块不在官方内核源码树内
- taints kernel:内核被标记为"不干净",便于后续 bug 排查
- signature missing:模块未签名,Ubuntu 默认要求签名验证
查看内核污点状态:
cat /proc/sys/kernel/tainted
3. 安装环境
3.1 获取当前 Ubuntu 内核版本
uname -r
示例输出:5.15.0-139-generic。后续安装内核头文件、编译模块都要用到这个版本号。
3.2 安装编译工具
sudo apt update
sudo apt install build-essential linux-headers-$(uname -r)
build-essential 是一个开发者工具包,包含 GCC、MAKE 等常用编译工具。
linux-headers-$(uname -r) 是当前内核版本对应的头文件,内核模块编译必须依赖它。
3.3 验证安装是否成功
上一步安装成功后,可以验证:
gcc -v
make -v
也可以查看内核头文件目录是否存在:
ls /lib/modules/$(uname -r)/build
最后一行应能看到内核头文件目录,例如 /usr/src/linux-headers-5.15.0-139-generic。
4. 项目结构
hello/
├── hello.c # 内核模块源码
└── Makefile # 编译脚本(注意大写 M)
5. 源码讲解:hello.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
// 模块加载时执行
static int __init hello_init(void)
{
printk("Hello, World! 内核模块加载成功\n");
return 0;
}
// 模块卸载时执行
static void __exit hello_exit(void)
{
printk("Hello, World! 内核模块卸载成功\n");
}
module_init(hello_init); // 绑定初始化函数
module_exit(hello_exit); // 绑定卸载函数
MODULE_LICENSE("GPL");
MODULE_AUTHOR("huangwenbo");
MODULE_DESCRIPTION("第一个内核模块");
MODULE_VERSION("0.1");
5.1 关键语法说明
| 语法 | 作用 |
|---|---|
#include <linux/...> |
内核头文件,不能用 <stdio.h> |
static int __init |
初始化函数,加载后内存可被释放 |
static void __exit |
卸载函数 |
printk() |
内核打印,输出到环形缓冲区,不是终端 |
module_init() |
注册模块入口 |
module_exit() |
注册模块出口 |
MODULE_LICENSE("GPL") |
声明 GPL 许可证,才能使用 GPL 内核符号 |
return 0 |
返回 0 表示加载成功;非 0 表示失败 |
5.2 关于 printk 日志级别
printk 支持不同日志级别,常用宏:
| 宏 | 含义 |
|---|---|
KERN_EMERG |
紧急 |
KERN_ERR |
错误 |
KERN_WARNING |
警告 |
KERN_INFO |
信息(常用) |
KERN_DEBUG |
调试 |
更简洁的写法:pr_info("...")、pr_err("...") 等。
6. Makefile 讲解
.PHONY: all clean
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
6.1 各行含义
| 行 | 含义 |
|---|---|
obj-m += hello.o |
将 hello.c 编译为可加载模块(m = module) |
obj-y |
静态编入内核(本课不用) |
make -C .../build |
进入内核编译系统目录 |
M=$(PWD) |
指定外部模块源码所在目录 |
modules |
编译模块目标 |
clean |
清理编译产物 |
6.2 文件名必须是 Makefile
Linux 下文件名区分大小写,这一点很容易踩坑:
普通 make
makefile或Makefile都行
内核模块编译(Kbuild)
- 必须是
Makefile(大写 M)
| 场景 | 认哪个文件名 |
|---|---|
普通 make |
makefile 或 Makefile 均可 |
| 内核 Kbuild | 必须是 Makefile(大写 M) |
错误示例:文件名为 makefile(小写),内核编译时会报:
Makefile: 没有那个文件或目录
修复方法:
mv makefile Makefile
7. 编译与运行
7.1 完整操作流程
# 1. 进入项目目录
cd ~/Desktop/workspace/hello
# 2. 编译模块
make
# 3. 确认产物
ls hello.ko
# 4. 加载模块(注意是 .ko,不是 .ok)
sudo insmod hello.ko
# 5. 查看内核日志
dmesg | tail
# 6. 确认模块已加载
lsmod | grep hello
# 7. 查看模块信息
modinfo hello.ko
# 8. 卸载模块(不带 .ko 后缀)
sudo rmmod hello
# 9. 再次查看日志
dmesg | tail
7.2 成功时的 dmesg 输出示例
[ 6474.545115] hello: loading out-of-tree module taints kernel.
[ 6474.545328] hello: module verification failed: signature and/or required key missing - tainting kernel
[ 6474.546871] Hello, World! 内核模块加载成功
[ 6570.448375] Hello, World! 内核模块卸载成功
前两行是警告,后两行是我们的模块输出,说明加载/卸载成功。
7.3 模块加载 / 卸载 / 日志命令
sudo insmod hello.ko # 加载 hello 模块
lsmod | grep hello # 查看已经加载的模块,并过滤出 hello
sudo rmmod hello # 卸载 hello 模块
dmesg | tail # 看内核日志最后 10 行
sudo dmesg -c # 清空内核日志,需要权限
注意:加载时用
hello.ko(带后缀),卸载时用hello(不带后缀)。
7.4 常用命令速查
| 命令 | 作用 |
|---|---|
make |
编译,生成 hello.ko |
make clean |
删除 .ko、.o 等编译产物 |
sudo insmod hello.ko |
加载 hello 模块 |
lsmod \| grep hello |
查看已经加载的模块,并过滤出 hello |
sudo rmmod hello |
卸载 hello 模块 |
dmesg \| tail |
看内核日志最后 10 行 |
sudo dmesg -c |
清空内核日志,需要权限 |
modinfo hello.ko |
查看模块元信息 |
dmesg -w |
实时跟踪内核日志 |
cat /proc/modules |
另一种查看已加载模块的方式 |
8. 常见错误与排查
8.1 Makefile 找不到
现象:
/home/.../hello/Makefile: 没有那个文件或目录
原因: 文件名是 makefile(小写),内核 Kbuild 只认 Makefile。
解决:
mv makefile Makefile
8.2 insmod 报 No such file or directory
现象:
insmod: ERROR: could not load module hello.ko: No such file or directory
常见原因:
- 文件名打错:
hello.ok应为hello.ko - 未编译或已清理:执行过
make clean,.ko已被删除 - 不在正确目录:当前目录没有
hello.ko
解决:
ls hello.ko # 确认文件存在
make # 若不存在则重新编译
sudo insmod hello.ko
8.3 make clean 后无法 insmod
原因: make clean 会删除 hello.ko,必须先重新 make。
正确流程:
改代码 → make → insmod → dmesg → rmmod
若执行了 make clean,需要重新 make 再 insmod。
8.4 模块已加载,再次 insmod 失败
现象:
insmod: ERROR: could not insert module hello.ko: File exists
原因: 模块已经在内核中。
解决:
sudo rmmod hello
sudo insmod hello.ko
8.5 dmesg 看不到输出
原因: printk 输出在内核环形缓冲区,不会显示在普通终端。
解决: 使用 dmesg | tail 或 dmesg -w 查看。
9. 本课知识小结
┌─────────────────────────────────────────────────┐
│ 用户空间 │
│ main.c → gcc → 可执行文件 → ./a.out │
│ printf 输出到终端 │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 内核空间 │
│ hello.c → make → hello.ko → insmod │
│ printk 输出到 dmesg │
└─────────────────────────────────────────────────┘
记住三个关键点:
- 不能用
printf、malloc等用户空间函数 - Makefile 必须大写 M
- 加载用
.ko,卸载不带后缀
10. 下一课预告
建议学习顺序:
- 模块参数(
module_param)—— 加载时传入变量 - 字符设备驱动 —— 实现
/dev/mydev,用户程序通过 read/write 与内核通信 - proc / sysfs —— 通过
/proc或/sys与内核交互
11. 参考资源
- The Linux Kernel Module Programming Guide (LKMPG) — 经典内核模块入门教程
- 内核源码文档:
/usr/src/linux-headers-$(uname -r)/Documentation/
12. 练习
- 修改
hello_init中的打印内容,重新编译并加载,确认dmesg输出已更新 - 执行
modinfo hello.ko,找到author、description、version字段 - 故意把
insmod hello.ok打错,观察报错,再改回正确命令 - 执行
make clean后直接insmod,观察报错,再make后重新加载 - 查资料了解
__init和__exit宏的作用
第一课完成。恭喜迈出 Linux 内核开发的第一步!