4、内核模块入门:编写第一个GPU内核模块,理解module_init/exit与参数传递

说实话,很多做GPU驱动的新人,一上来就盯着复杂的硬件寄存器看,结果把自己看懵了。

我个人的建议是:先别急。咱们从最基础的内核模块开始。你想想看,驱动说白了就是个内核模块。搞懂模块的加载、卸载、参数传递,后面那些复杂的GPU调度、内存管理才有根基。

4.1 什么是内核模块?

内核模块,就是一段可以在运行时动态加载到Linux内核的代码。不需要重新编译整个内核,也不需要重启机器。

我在项目中遇到过好几次这种情况:调试GPU驱动时,发现某个寄存器配置不对。如果是把代码编进内核里,改一次就得重启一次,一天下来光重启就浪费两小时。用模块就不一样了——insmodrmmod,几秒钟搞定。

核心要点:

  • 模块是动态加载的,不是静态编译进内核的
  • 模块运行在内核空间,拥有最高权限
  • 模块出问题,整个系统可能崩溃(嗯,这里要注意)

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_initmodule_exit 就是两个宏。它们告诉内核:

  • 加载这个模块时,请调用 gpu_module_init 这个函数
  • 卸载这个模块时,请调用 gpu_module_exit 这个函数

为什么会这样设计?因为内核不知道你的模块里哪个函数是入口。你得用宏把它"注册"进去。我刚开始学的时候,老觉得这多此一举。后来才明白,这是内核模块框架的约定,不遵守就加载不了。

个人经验:

我习惯在 module_init 函数里做三件事:

  1. 打印模块版本信息(方便调试)
  2. 检查硬件是否存在(比如PCI设备ID)
  3. 分配必要的资源(内存、中断等)

别一股脑全塞进去,分步骤来,出问题了也好定位。

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设备注册、中断处理、内存分配,你就能跟上节奏了。记住,驱动开发没有捷径,但可以从最基础的模块开始,一步步搭起来。