4、内核模块入门:编写第一个GPU内核模块,理解module_init/exit与参数传递
说实话,很多做GPU驱动的新人,一上来就盯着复杂的硬件寄存器看,结果把自己看懵了。
我个人的建议是:先别急。咱们从最基础的内核模块开始。你想想看,驱动说白了就是个内核模块。搞懂模块的加载、卸载、参数传递,后面那些复杂的GPU调度、内存管理才有根基。
4.1 什么是内核模块?
内核模块,就是一段可以在运行时动态加载到Linux内核的代码。不需要重新编译整个内核,也不需要重启机器。
我在项目中遇到过好几次这种情况:调试GPU驱动时,发现某个寄存器配置不对。如果是把代码编进内核里,改一次就得重启一次,一天下来光重启就浪费两小时。用模块就不一样了——insmod、rmmod,几秒钟搞定。
核心要点:
- 模块是动态加载的,不是静态编译进内核的
- 模块运行在内核空间,拥有最高权限
- 模块出问题,整个系统可能崩溃(嗯,这里要注意)
4.2 第一个GPU内核模块:Hello GPU
咱们直接上手写一个。这个模块啥也不干,就是加载时打印"Hello GPU",卸载时打印"Goodbye GPU"。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init gpu_module_init(void)
{
printk(KERN_INFO "Hello GPU: Module loaded\n");
return 0;
}
static void __exit gpu_module_exit(void)
{
printk(KERN_INFO "Goodbye GPU: Module unloaded\n");
}
module_init(gpu_module_init);
module_exit(gpu_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple GPU kernel module");
代码很简单对吧?但有几个细节我得跟你聊聊。
4.3 module_init 和 module_exit 到底干了啥?
说白了,module_init 和 module_exit 就是两个宏。它们告诉内核:
- 加载这个模块时,请调用
gpu_module_init这个函数 - 卸载这个模块时,请调用
gpu_module_exit这个函数
为什么会这样设计?因为内核不知道你的模块里哪个函数是入口。你得用宏把它"注册"进去。我刚开始学的时候,老觉得这多此一举。后来才明白,这是内核模块框架的约定,不遵守就加载不了。
个人经验:
我习惯在 module_init 函数里做三件事:
- 打印模块版本信息(方便调试)
- 检查硬件是否存在(比如PCI设备ID)
- 分配必要的资源(内存、中断等)
别一股脑全塞进去,分步骤来,出问题了也好定位。
4.4 参数传递:让模块更灵活
你想想看,如果每次加载模块都要改代码,那多麻烦。内核模块支持参数传递,加载时通过命令行传进去。
#include <linux/moduleparam.h>
static int gpu_debug = 0;
static char *gpu_name = "default_gpu";
module_param(gpu_debug, int, 0644);
module_param(gpu_name, charp, 0644);
MODULE_PARM_DESC(gpu_debug, "Enable debug mode (0=off, 1=on)");
MODULE_PARM_DESC(gpu_name, "GPU device name");
static int __init gpu_module_init(void)
{
printk(KERN_INFO "GPU module loaded\n");
printk(KERN_INFO "Debug mode: %d\n", gpu_debug);
printk(KERN_INFO "GPU name: %s\n", gpu_name);
return 0;
}
加载时这样用:
sudo insmod gpu_module.ko gpu_debug=1 gpu_name="Mali_G610"
我曾经踩过的坑:
参数权限别乱设。0644表示所有用户可读,只有root可写。如果你设成0666,普通用户也能改内核模块参数,这就有安全风险了。我在一个项目里就因为这个被安全审计骂了一顿。
4.5 参数类型与权限一览
| 类型 | 宏 | 说明 | 常见用途 |
|---|---|---|---|
| int | module_param(name, int, perm) | 整型参数 | 调试开关、频率值 |
| charp | module_param(name, charp, perm) | 字符串指针 | 设备名称、路径 |
| bool | module_param(name, bool, perm) | 布尔值 | 启用/禁用特性 |
| int array | module_param_array(name, int, &num, perm) | 整型数组 | 多个寄存器地址 |
4.6 编译与测试
写个简单的Makefile:
obj-m += gpu_module.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
编译:
make
加载与卸载:
sudo insmod gpu_module.ko
sudo rmmod gpu_module
查看内核日志:
dmesg | tail
调试小技巧:
我习惯在 printk 里加上模块名前缀,比如 printk(KERN_INFO "[GPU] ...")。这样 dmesg 里一搜 GPU 就能过滤出所有相关日志,省得在一堆内核消息里翻来翻去。
4.7 避坑指南
- 不要用
printf:内核里没有printf,只能用printk。我见过有人把用户态代码直接搬过来,编译报错一脸懵。 - 返回值别乱写:
module_init返回0表示成功,返回负数表示失败。我曾经返回了1,结果模块加载成功了但内核以为失败了,搞得资源泄漏。 - 卸载时要清理:你在
init里申请了内存、注册了中断,exit里一定要释放。否则模块卸载了,资源还占着,系统迟早崩给你看。
好了,这一章就到这。你把这个模块跑通了,后面讲GPU设备注册、中断处理、内存分配,你就能跟上节奏了。记住,驱动开发没有捷径,但可以从最基础的模块开始,一步步搭起来。