第3章:内核模块基础:Linux内核模块框架
大家好,我是你们的老朋友。今天我们来聊聊嵌入式GPU驱动开发中最基础、也最绕不开的一环——Linux内核模块。说实话,很多刚入行的朋友觉得这玩意儿简单,不就是insmod和rmmod嘛。但我在项目中踩过的坑告诉我,这里面的门道可不少。
你想想看,GPU驱动本质上就是个内核模块。它要跟硬件寄存器打交道,要管理显存,要处理中断。如果连模块框架都没搞明白,后面调试起来会非常痛苦。我个人习惯,在写任何驱动之前,先把模块骨架搭好,打印调试信息确认加载卸载流程没问题,再往里填业务逻辑。
3.1 模块的入口与出口:module_init / module_exit
每个内核模块都有两个核心函数:初始化和清理。说白了,就是模块加载时干什么,卸载时干什么。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init mygpu_init(void)
{
printk(KERN_INFO "MyGPU: 模块加载成功\n");
// 在这里做硬件初始化、注册设备、分配资源等
return 0;
}
static void __exit mygpu_exit(void)
{
printk(KERN_INFO "MyGPU: 模块卸载成功\n");
// 在这里释放资源、注销设备
}
module_init(mygpu_init);
module_exit(mygpu_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("嵌入式GPU驱动示例模块");
这里有个细节要注意:__init 和 __exit 这两个宏。它们告诉内核,初始化函数在模块加载完成后可以丢弃,释放内存。我在项目中遇到过,有人忘了加 __init,结果模块占用的内存一直没释放。虽然不影响功能,但嵌入式设备内存本来就紧张,能省一点是一点。
核心要点:
module_init()指定模块加载时执行的函数module_exit()指定模块卸载时执行的函数- 初始化函数返回0表示成功,返回负数表示失败
- 一定要加
MODULE_LICENSE("GPL"),否则内核会报“tainted”警告
3.2 模块参数传递:让驱动更灵活
写驱动的时候,有些参数不想写死在代码里。比如GPU的时钟频率、显存大小、中断号等。这时候就需要模块参数了。
#include <linux/moduleparam.h>
static int gpu_freq = 500; // 默认500MHz
static char *gpu_name = "default_gpu";
module_param(gpu_freq, int, 0644);
module_param(gpu_name, charp, 0644);
MODULE_PARM_DESC(gpu_freq, "GPU核心频率 (MHz)");
MODULE_PARM_DESC(gpu_name, "GPU设备名称");
加载模块时就可以这样传参:
insmod mygpu.ko gpu_freq=800 gpu_name="mali_gpu"
嗯,这里要注意权限位 0644。它表示这个参数在 /sys/module/mygpu/parameters/ 下可见,并且允许读取和root写入。我曾经在调试时把权限设成了 0,结果死活看不到参数值,还以为是模块没加载成功。折腾了半天才发现是权限问题。
我的经验:调试阶段建议把参数权限设为 0644,这样可以在运行时通过 echo 1000 > /sys/module/mygpu/parameters/gpu_freq 动态修改参数,不用反复卸载加载模块。但产品发布前记得改成只读,防止用户误操作。
3.3 内核打印:printk 与调试等级控制
驱动开发离不开调试打印。用户态用printf,内核态用printk。但printk的用法比printf复杂一些,因为它有日志等级。
| 宏定义 | 数值 | 含义 | 使用场景 |
|---|---|---|---|
| KERN_EMERG | 0 | 紧急情况 | 系统崩溃前 |
| KERN_ALERT | 1 | 需要立即处理 | 严重硬件错误 |
| KERN_CRIT | 2 | 临界条件 | 驱动初始化失败 |
| KERN_ERR | 3 | 错误 | 功能异常 |
| KERN_WARNING | 4 | 警告 | 非致命问题 |
| KERN_NOTICE | 5 | 普通提示 | 模块加载卸载 |
| KERN_INFO | 6 | 信息 | 调试信息 |
| KERN_DEBUG | 7 | 调试 | 详细调试 |
为什么会这样设计?因为嵌入式设备可能没有显示器,日志通过串口输出。如果所有打印都输出,串口会被刷爆,影响性能。所以内核用日志等级来控制哪些打印能出来。
// 实际使用示例
printk(KERN_INFO "MyGPU: 检测到设备ID 0x%x\n", device_id);
printk(KERN_ERR "MyGPU: 寄存器读写超时!\n");
printk(KERN_DEBUG "MyGPU: 当前频率 = %d MHz\n", gpu_freq);
控制调试等级的方法:
- 运行时:
echo 7 > /proc/sys/kernel/printk打开所有打印 - 启动参数:在bootargs里加
loglevel=4只显示KERN_WARNING及以上 - 代码控制:用
#define DEBUG配合pr_debug()条件编译
避坑指南:我曾经在产品发布前忘了关掉KERN_DEBUG级别的打印,结果GPU在高负载时每秒输出几千条日志,串口直接卡死,导致系统响应变慢。所以发布版本一定要把调试打印关掉,或者用 pr_debug() 配合 DEBUG 宏控制。
3.4 模块加载与卸载流程
整个流程其实不复杂,但每一步都可能出问题。我画个流程图帮你理清思路:
- 加载流程:
- 用户执行
insmod mygpu.ko - 内核分配模块内存,解析ELF格式
- 解析模块参数(如果有)
- 调用
module_init指定的函数 - 初始化函数执行硬件探测、资源申请
- 返回0,模块状态变为“存活”
- 用户执行
- 卸载流程:
- 用户执行
rmmod mygpu - 内核检查模块引用计数是否为0
- 调用
module_exit指定的函数 - 清理函数释放资源、注销设备
- 内核释放模块内存
- 用户执行
这里有个坑:如果模块被其他模块依赖,或者有进程在使用它,引用计数不为0,rmmod会失败。我在调试GPU驱动时,经常遇到 rmmod: ERROR: Module mygpu is in use。这时候用 lsmod 看看谁在用,或者 fuser -k /dev/mygpu 杀掉占用进程。
调试小技巧:
lsmod:查看已加载模块列表modinfo mygpu.ko:查看模块信息,包括参数描述dmesg | tail:查看内核打印日志cat /proc/modules:查看模块内存占用
好了,这一章的内容就到这里。模块框架是驱动开发的基石,看似简单,但每个细节都值得深究。下一章我们会深入GPU驱动的具体初始化流程,到时候会用到今天讲的这些知识。记住,写驱动就像盖房子,地基打牢了,后面才稳当。