3、第一个内核模块:Hello World
说实话,写第一个内核模块的感觉,跟写第一个用户态程序完全不一样。你想想看,用户态程序跑在应用层,出错了大不了崩掉一个进程。内核模块要是写崩了,那就是整个系统挂掉,直接黑屏或者 kernel panic。
我记得我第一次写内核模块时,手都在抖。但别怕,咱们从最简单的 Hello World 开始,一步步来。
3.1 什么是内核模块?
内核模块,说白了就是一段可以动态加载到内核中的代码。它不需要重新编译整个内核,也不需要重启系统。你写好了,用 insmod 命令插进去,它就成了内核的一部分。
我习惯把内核模块比作「乐高积木」。内核是底座,模块就是一块块积木。想加功能了,插一块上去;不想要了,拔下来就行。当然,拔的时候得小心,别把底座带坏了。
| 对比项 | 内核模块 | 用户态程序 |
|---|---|---|
| 运行空间 | 内核空间(Ring 0) | 用户空间(Ring 3) |
| 入口函数 | module_init() | main() |
| 出口函数 | module_exit() | return 0 |
| 标准库支持 | ❌ 不能使用 libc | ✅ 完全支持 |
| 调试方式 | printk() + dmesg | printf() + gdb |
| 崩溃后果 | 系统挂掉 | 进程退出 |
3.2 编写第一个内核模块
先看代码,再解释。这是最经典的 Hello World 内核模块,我在 RK3568 上验证过,直接能用。
// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello World from RK3568!\n");
printk(KERN_INFO "Module loaded successfully.\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye! Module unloaded.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World kernel module for RK3568");
MODULE_VERSION("1.0");
这段代码看着简单,但有几个关键点我得强调一下:
- 头文件:module.h 是必须的,kernel.h 提供 printk,init.h 提供 __init 和 __exit 宏
- __init 和 __exit:这两个宏告诉内核,初始化函数和退出函数在运行完后可以释放掉,节省内存。我刚开始写时没加,也能跑,但内核会给你警告
- printk:内核版的 printf,但它有日志级别。KERN_INFO 是信息级别,还有 KERN_ERR、KERN_WARNING 等
- MODULE_LICENSE:必须写,不写的话内核会报「tainted kernel」。GPL 是最常用的
3.3 Makefile 编写
内核模块的 Makefile 跟普通程序的 Makefile 不太一样。它用的是内核的构建系统,说白了就是让内核帮你编译。
# 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:告诉内核,我要编译一个名为 hello 的模块。hello.o 由 hello.c 生成
- KDIR:指向内核源码树的路径。$(shell uname -r) 获取当前内核版本号,/lib/modules/xxx/build 是个软链接,指向实际的内核源码
- -C $(KDIR):切换到内核源码目录,读取它的 Makefile
- M=$(PWD):告诉内核,我的模块源码在当前目录
3.4 编译与加载
编译很简单,直接在源码目录下执行 make:
$ make
make -C /lib/modules/5.10.160/build M=/home/user/hello modules
make[1]: Entering directory '/home/user/kernel'
CC [M] /home/user/hello/hello.o
MODPOST /home/user/hello/Module.symvers
LD [M] /home/user/hello/hello.ko
make[1]: Leaving directory '/home/user/kernel'
编译成功后,会生成 hello.ko 文件。这就是我们要的内核模块。
加载和卸载的命令:
# 加载模块
$ sudo insmod hello.ko
# 查看已加载的模块
$ lsmod | grep hello
# 卸载模块
$ sudo rmmod hello
# 或者用 modprobe(会自动处理依赖)
$ sudo modprobe hello
$ sudo modprobe -r hello
insmod 和 modprobe 的区别?我简单说一下:
- insmod:直接加载,不处理依赖。适合我们这种简单的模块
- modprobe:会分析模块依赖,自动加载依赖模块。但需要先把模块放到 /lib/modules/xxx/ 下
3.5 查看内核日志
模块加载和卸载时,printk 输出的信息会写到内核日志缓冲区。查看方式有几种:
# 查看所有内核日志
$ dmesg
# 查看最后几行
$ dmesg | tail -10
# 实时监控(类似 tail -f)
$ dmesg -w
# 清空日志
$ sudo dmesg -c
加载模块后,你应该能看到类似这样的输出:
[ 1234.567890] Hello World from RK3568!
[ 1234.567895] Module loaded successfully.
卸载时:
[ 2345.678901] Goodbye! Module unloaded.
3.6 完整流程与知识结构
下面这张图把整个流程串起来了,从源码到加载再到日志查看,一目了然。
3.7 常见问题与避坑
写内核模块,坑不少。我把自己踩过的坑列出来,你们遇到了直接对照:
| 问题 | 现象 | 原因 | 解决方法 |
|---|---|---|---|
| Invalid module format | insmod 报错,模块加载失败 | 内核版本不匹配,或编译配置不同 | 确认 KDIR 指向正确的内核源码 |
| Unknown symbol | insmod 报错,提示符号找不到 | 模块依赖其他模块,或内核配置未开启 | 用 modprobe 替代 insmod,或检查内核配置 |
| Permission denied | insmod 需要 root 权限 | 加载内核模块需要特权 | 加 sudo |
| dmesg 看不到输出 | 模块加载成功,但没日志 | printk 级别太高,被内核过滤了 | 用 KERN_ALERT 或调整 /proc/sys/kernel/printk |
| 模块卸载时卡死 | rmmod 后系统无响应 | exit 函数中有死循环或资源未释放 | 检查 exit 函数,确保能正常返回 |
3.8 本章小结
到这里,你已经完成了第一个内核模块的编写、编译、加载和卸载。虽然只是个 Hello World,但整个流程跟写一个真正的驱动完全一样。说白了,驱动开发就是在这个框架上不断加功能。
记住几个关键点:
- module_init 和 module_exit 是模块的入口和出口
- Makefile 要用内核的构建系统,别自己写链接脚本
- printk 是内核的调试利器,dmesg 是查看日志的必备工具
- 交叉编译时,编译器、内核源码、目标板三者必须匹配
嗯,这一章就到这。代码不多,但每一步都很关键。建议你亲手敲一遍,别复制粘贴。敲的过程中你会注意到很多细节,比如头文件的顺序、宏的写法,这些在以后写复杂驱动时都会用到。