4、Linux内核模块编程:从Hello World到实战
说到GPU驱动,很多人第一反应是「这玩意儿太底层了,碰不得」。其实不然。驱动说白了就是内核和硬件之间的翻译官,而学习内核模块编程,就是学会怎么跟内核打交道。
我个人习惯把内核模块比作「乐高积木」——你不需要重新烧录整个内核,就能动态地往里面添加功能。这对驱动开发来说太重要了。你想想看,每次改个驱动都要重启机器,那开发效率得多低?
4.1 内核模块的基本结构
每个内核模块都有两个核心函数:module_init 和 module_exit。前者是模块加载时的入口,后者是卸载时的出口。说白了,就是「进来干啥」和「走了留啥」。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_driver_init(void)
{
printk(KERN_INFO "Hello from GPU driver module!\n");
return 0;
}
static void __exit my_driver_exit(void)
{
printk(KERN_INFO "Goodbye from GPU driver module!\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple GPU driver module");
这里有个细节要注意:__init 和 __exit 这两个宏。它们告诉内核,这些函数在初始化或卸载完成后就可以释放掉了。我在项目中遇到过有人忘了加这两个宏,结果模块占着内存不释放,被内核大佬骂了一顿。
4.2 模块编译:Makefile 的艺术
编译内核模块和编译普通程序不一样。你不能直接 gcc hello.c 就完事了。内核模块需要和内核源码树一起编译,这样才能保证符号和接口的一致性。
obj-m := my_gpu_driver.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
嗯,这里要注意 KDIR 的路径。如果你自己编译了内核,路径可能不一样。我曾经因为路径写错,折腾了整整一个下午才发现是 Makefile 的问题。
编译完成后,你会得到 .ko 文件。这就是内核模块的二进制文件。用 insmod 加载,用 rmmod 卸载:
sudo insmod my_gpu_driver.ko
sudo rmmod my_gpu_driver
想看模块加载了没?用 lsmod 或者 cat /proc/modules。我个人习惯用 lsmod | grep my_gpu 快速定位。
4.3 printk:内核的 printf
调试内核模块不能像用户态程序那样用 printf。内核里用的是 printk。它和 printf 长得像,但多了个日志级别。
printk(KERN_EMERG "系统崩溃了!\n"); // 最高优先级
printk(KERN_ALERT "需要立即处理\n");
printk(KERN_CRIT "严重错误\n");
printk(KERN_ERR "出错了\n");
printk(KERN_WARNING "警告\n");
printk(KERN_NOTICE "需要注意\n");
printk(KERN_INFO "信息\n");
printk(KERN_DEBUG "调试信息\n"); // 最低优先级
日志级别决定了消息会不会显示在控制台上。默认情况下,只有优先级高于 KERN_WARNING 的消息才会打印到控制台。想看所有消息?用 dmesg 命令。
KERN_EMERG 级别打印调试信息,结果控制台被刷爆了,运维同事差点把我拉黑。记住:调试用 KERN_DEBUG,别乱用高级别。
4.4 模块参数传递
写驱动时,你经常需要让用户在加载模块时指定一些参数。比如 GPU 的基地址、中断号等。内核模块支持参数传递,用 module_param 宏就行。
static int gpu_irq = 16;
static char *gpu_name = "default_gpu";
static int debug_level = 0;
module_param(gpu_irq, int, 0644);
module_param(gpu_name, charp, 0644);
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(gpu_irq, "GPU interrupt number");
MODULE_PARM_DESC(gpu_name, "GPU device name");
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
加载时这样传参:
sudo insmod my_gpu_driver.ko gpu_irq=32 gpu_name="nvidia_gpu" debug_level=2
参数类型支持 int、long、charp(字符串指针)等。那个 0644 是文件权限,表示在 /sys/module/ 下生成的参数文件权限。你可以在运行时通过读写这些文件来修改参数值。
charp 指向的是用户空间传过来的指针。模块卸载后,这个指针就失效了。如果你要在模块生命周期内一直使用这个字符串,记得用 kstrdup 复制一份。
4.5 知识体系总览
下面这张图是我自己总结的内核模块编程核心逻辑,你看一眼就能明白整个流程:
这张图把整个流程串起来了。从模块结构到编译,再到加载调试和参数传递,每一步都是环环相扣的。我个人建议你先把 Hello World 跑通,然后再慢慢加功能。
insmod、rmmod、dmesg 这三个命令玩熟。我当年就是靠这三个命令,一步步摸清了内核模块的脾气。
好了,这一章的内容就到这里。记住:内核模块编程是驱动开发的基础,也是你理解操作系统内核的窗口。多动手,多踩坑,自然就熟练了。