4. 第一个内核模块:Hello World模块
说实话,学驱动开发最兴奋的时刻,就是看到自己写的代码跑在内核空间里。虽然只是个打印"Hello World"的小模块,但这一步跨过去,你就真正进入了内核开发的大门。
我记得自己第一次写内核模块时,连Makefile都编译不过去,折腾了整整一个下午。后来才发现是交叉编译链的路径写错了。嗯,这些坑我今天都会帮你避开。
4.1 模块代码框架:init/exit
内核模块和用户态程序最大的区别是什么?它没有main函数。内核模块的入口和出口,是通过两个宏来声明的:module_init() 和 module_exit()。
来看一个最基础的Hello World模块代码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello World: 模块已加载\n");
printk(KERN_INFO "当前内核版本: %s\n", UTS_RELEASE);
return 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("一个简单的Hello World内核模块");
MODULE_VERSION("1.0");
这里有几个关键点,我重点说一下:
- __init 和 __exit:这两个宏告诉内核,初始化函数在模块加载后可以释放掉,退出函数在模块卸载后也可以释放。说白了就是省内存。
- printk:内核空间的打印函数,和printf类似,但多了个日志级别。KERN_INFO是信息级别,还有KERN_DEBUG、KERN_ERR等。
- MODULE_xxx:这些宏用来描述模块信息,不是必须的,但我建议你每次都写上。为什么?因为modinfo命令会用到它们。
4.2 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
这个Makefile看起来简单,但背后的逻辑你得搞清楚:
| 变量/目标 | 作用 | 说明 |
|---|---|---|
| obj-m | 指定要编译成模块的目标文件 | hello.o 对应 hello.c |
| KDIR | 内核源码树的路径 | build是软链接,指向实际源码 |
| -C | 切换到内核源码目录 | 使用内核的构建系统 |
| M=$(PWD) | 指定模块源码所在目录 | 编译完成后回到当前目录 |
4.3 编译与加载
编译过程很简单,直接执行make命令:
$ make
make -C /lib/modules/5.15.0-91-generic/build M=/home/user/hello modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-91-generic'
CC [M] /home/user/hello/hello.o
MODPOST /home/user/hello/Module.symvers
LD [M] /home/user/hello/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-91-generic'
编译成功后,你会看到生成了 hello.ko 文件。这个.ko就是内核模块文件,相当于用户态的.exe或.elf。
加载和卸载模块的命令:
# 加载模块
sudo insmod hello.ko
# 查看内核日志
dmesg | tail -5
[ 1234.567890] Hello World: 模块已加载
[ 1234.567895] 当前内核版本: 5.15.0-91-generic
# 卸载模块
sudo rmmod hello
# 再次查看日志
dmesg | tail -3
[ 1234.567890] Hello World: 模块已加载
[ 1234.567895] 当前内核版本: 5.15.0-91-generic
[ 1240.123456] Hello World: 模块已卸载
关键区别:
- insmod:加载模块,需要指定模块文件路径
- rmmod:卸载模块,只需要模块名(不带.ko)
- modprobe:更智能的加载方式,会自动处理依赖
4.4 查看模块信息
模块加载后,怎么确认它真的在运行?两个命令搞定:
lsmod:列出已加载的模块
$ lsmod | grep hello
hello 16384 0
输出信息解读:
- 第一列:模块名称
- 第二列:模块占用的内存大小(单位:字节)
- 第三列:被其他模块引用的次数
- 第四列:依赖该模块的其他模块列表
modinfo:查看模块详细信息
$ modinfo hello.ko
filename: /home/user/hello/hello.ko
license: GPL
author: Your Name
description: 一个简单的Hello World内核模块
version: 1.0
depends:
retpoline: Y
name: hello
vermagic: 5.15.0-91-generic SMP mod_unload modversions
看到没?我们在代码里写的MODULE_LICENSE、MODULE_AUTHOR这些信息,全都在这里显示出来了。vermagic这一行特别重要,它记录了模块编译时的内核版本和配置,如果和你运行的内核不匹配,模块就加载不了。
4.5 知识体系图
下面这张图总结了本章的核心知识点和流程关系:
4.6 常见问题与排查
写第一个模块时,你大概率会遇到下面这些问题:
| 错误现象 | 可能原因 | 解决方法 |
|---|---|---|
| make: Nothing to be done | Makefile格式错误,tab被替换成空格 | 检查all:和clean:下面的命令是否用了Tab缩进 |
| Invalid module format | 内核版本或配置不匹配 | 确认KDIR指向正确的内核源码 |
| insmod: ERROR: could not insert module | 模块依赖缺失或权限不足 | 使用sudo,检查dmesg查看具体错误 |
| rmmod: ERROR: Module hello is in use | 有其他模块依赖本模块 | 先卸载依赖模块,或使用rmmod -f强制卸载 |
好了,到这里你已经掌握了内核模块开发的基本流程。从代码框架到Makefile,从编译到加载验证,这一整套流程在后续所有驱动开发中都会反复用到。把这个Hello World跑通了,后面的路就好走了。