4、Linux内核模块编程:从Hello World到实战

说到GPU驱动,很多人第一反应是「这玩意儿太底层了,碰不得」。其实不然。驱动说白了就是内核和硬件之间的翻译官,而学习内核模块编程,就是学会怎么跟内核打交道。

我个人习惯把内核模块比作「乐高积木」——你不需要重新烧录整个内核,就能动态地往里面添加功能。这对驱动开发来说太重要了。你想想看,每次改个驱动都要重启机器,那开发效率得多低?

4.1 内核模块的基本结构

每个内核模块都有两个核心函数:module_initmodule_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 这两个宏。它们告诉内核,这些函数在初始化或卸载完成后就可以释放掉了。我在项目中遇到过有人忘了加这两个宏,结果模块占着内存不释放,被内核大佬骂了一顿。

小技巧: 模块加载成功后返回0,失败返回负数错误码。这个习惯一定要养成,否则内核会以为模块加载成功了,但实际上啥也没干。

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 知识体系总览

下面这张图是我自己总结的内核模块编程核心逻辑,你看一眼就能明白整个流程:

Linux内核模块编程核心流程 模块结构 module_init / module_exit 模块编译 Makefile + Kbuild 加载/卸载 insmod / rmmod 调试方法 printk + dmesg 日志级别控制 参数传递 module_param 运行时动态修改 核心要点 1. 模块是内核功能的动态扩展,无需重新编译内核 2. printk是内核调试的基石,学会控制日志级别很重要 3. 参数传递让模块更灵活,适合驱动配置场景

这张图把整个流程串起来了。从模块结构到编译,再到加载调试和参数传递,每一步都是环环相扣的。我个人建议你先把 Hello World 跑通,然后再慢慢加功能。

我的经验: 刚开始学内核模块时,别急着写复杂功能。先把 insmodrmmoddmesg 这三个命令玩熟。我当年就是靠这三个命令,一步步摸清了内核模块的脾气。

好了,这一章的内容就到这里。记住:内核模块编程是驱动开发的基础,也是你理解操作系统内核的窗口。多动手,多踩坑,自然就熟练了。


专注资料整理