第2章:Linux内核模块基础
好,咱们正式开始写驱动了。不过别急,先得把内核模块这块地基打牢。我刚开始学驱动开发时,上来就想着怎么写复杂的硬件控制代码,结果连模块怎么加载都没搞明白,折腾了一下午。嗯,咱们一步步来。
2.1 内核模块的概念
Linux内核模块,说白了就是一段可以动态加载到内核中的代码。它不像应用程序那样跑在用户空间,而是直接运行在内核空间,拥有最高权限。
为什么要用模块?我举个例子。你写了一个POS机的打印机驱动,总不能每次改个bug就重新编译整个内核吧?那得等到猴年马月。模块机制允许你单独编译、单独加载,调试起来方便得多。
核心特点:
- 动态加载:不用重启系统,insmod一下就能用
- 独立编译:模块和内核源码可以分开编译
- 节省内存:用的时候加载,不用就卸载
- 版本兼容:但要注意,模块必须和内核版本匹配
你想想看,POS机这种嵌入式设备,内存本来就紧张。如果所有驱动都编译进内核,那得浪费多少资源?模块化设计,按需加载,这才是嵌入式开发的精髓。
2.2 编写第一个内核模块:Hello World
老规矩,学任何新东西都从Hello World开始。我当年写第一个内核模块时,打印了一行"Hello, POS World!",虽然简单,但看到内核日志里出现自己的输出,那感觉还是很爽的。
来看代码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, POS World!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, POS World!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple POS driver module");
这里有几个关键点,我重点说一下:
- module_init和module_exit:这是模块的入口和出口,必须要有。加载时调用hello_init,卸载时调用hello_exit
- printk:内核版的printf,输出到内核日志。KERN_INFO是日志级别,调试时常用
- __init和__exit:这两个宏告诉内核,这些函数只在初始化/退出时使用,用完后可以释放内存
- MODULE_LICENSE:必须声明许可证,不写的话内核会警告。我习惯用GPL
个人经验:printk的日志级别很重要。调试时用KERN_DEBUG,正式发布时用KERN_INFO或KERN_NOTICE。我曾经在量产版本里忘了改级别,结果控制台被调试信息刷屏,被测试同事追着骂了一周。
2.3 模块的加载与卸载
写好了模块,怎么让它跑起来?Linux提供了几个工具命令:
| 命令 | 作用 | 常用选项 |
|---|---|---|
| insmod | 加载模块 | 直接跟模块文件路径 |
| rmmod | 卸载模块 | 跟模块名(不含.ko) |
| modprobe | 智能加载/卸载 | 自动处理依赖关系 |
| lsmod | 查看已加载模块 | 显示模块列表和依赖 |
| modinfo | 查看模块信息 | 显示作者、描述、参数等 |
实际操作一下:
# 编译模块
make
# 加载模块
sudo insmod hello.ko
# 查看内核日志
dmesg | tail
# 卸载模块
sudo rmmod hello
# 再看日志,应该能看到退出信息
dmesg | tail
注意:我曾经在调试POS机串口驱动时,加载模块后忘了卸载就直接拔了设备,结果内核panic了。记住,卸载模块前要确保没有进程在使用它。rmmod失败时,用lsmod看看引用计数,不为0就别硬来。
2.4 模块参数传递
写驱动时,你肯定不希望把硬件地址、中断号这些参数写死在代码里。模块参数机制就是干这个的。
看个例子:
#include <linux/moduleparam.h>
static int irq_num = 10;
static char *device_name = "default";
module_param(irq_num, int, 0644);
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(irq_num, "IRQ number for the device");
MODULE_PARM_DESC(device_name, "Device name string");
加载时传参:
sudo insmod mydriver.ko irq_num=17 device_name="printer"
参数类型支持int、long、charp(字符串指针)、bool等。权限0644表示可以通过sysfs在运行时查看和修改。
我建议:调试阶段把参数权限设成0644,方便动态调整。但产品发布时,最好改成0444(只读),防止用户误操作。我在一个项目中就吃过这个亏,用户通过sysfs改了中断号,设备直接罢工了。
2.5 模块依赖与符号导出
实际项目中,模块之间经常有依赖关系。比如你写了一个通用的I2C核心模块,然后打印机驱动、密码键盘驱动都依赖它。怎么处理?
内核提供了符号导出机制:
// 在核心模块中导出函数
int i2c_read_data(int addr, char *buf, int len)
{
// 实现I2C读取
}
EXPORT_SYMBOL(i2c_read_data);
// 在打印机驱动中调用
extern int i2c_read_data(int addr, char *buf, int len);
依赖关系处理:
- EXPORT_SYMBOL:导出符号给其他模块使用
- EXPORT_SYMBOL_GPL:只允许GPL许可证的模块使用
- modprobe:自动处理依赖,先加载被依赖的模块
- depmod:生成模块依赖关系文件
避坑指南:我曾经写过一个POS机电源管理模块,导出了几个函数给其他驱动用。结果有个同事用了EXPORT_SYMBOL_GPL,但他的模块声明的是BSD许可证,加载时报错"Unknown symbol"。折腾了两天才发现是许可证不匹配。所以,团队协作时一定要统一许可证策略。
查看模块间的依赖关系:
# 查看模块依赖
lsmod
# 查看具体模块的依赖
modinfo mydriver.ko
# 手动加载依赖模块
sudo modprobe i2c_core
sudo insmod mydriver.ko
嗯,到这里内核模块的基础知识就差不多了。这些内容看着简单,但都是后面写复杂驱动的基础。我建议你亲手把Hello World模块编译加载一遍,感受一下内核模块的开发流程。下一章咱们就开始接触真正的硬件——GPIO操作,那才是驱动开发的重头戏。