第4章:Linux内核模块基础
说实话,很多刚接触驱动开发的朋友,第一个拦路虎就是——搞不清楚内核模块到底是个啥。我当年也一样,看着那些.ko文件,心里直犯嘀咕:这玩意儿跟普通程序有啥区别?
嗯,今天咱们就把这事儿彻底聊透。
4.1 内核模块的结构
内核模块,说白了就是一段可以动态加载到内核里的代码。它不像应用程序那样有main()函数,而是用两个固定的入口函数: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, kernel module loaded!\n");
return 0;
}
static void __exit my_driver_exit(void)
{
printk(KERN_INFO "Goodbye, kernel module unloaded!\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple kernel module example");
这里有几个关键点,我挨个说一下:
- __init 和 __exit:这两个宏告诉内核,初始化函数用完后可以扔掉,节省内存。我在项目里见过有人忘了加,结果模块占着内存不放,虽然问题不大,但总觉得不够优雅。
- module_init / module_exit:这是注册入口和出口的宏。注意,它们不是函数调用,是宏定义。
- MODULE_* 宏:这些是模块的描述信息,虽然不写也能跑,但我建议你养成好习惯。有一次我调试一个第三方驱动,死活找不到作者,就是因为没写
MODULE_AUTHOR。
核心要点:内核模块没有main(),它的生命周期完全由init和exit函数控制。加载时执行init,卸载时执行exit。
4.2 模块加载与卸载
写好了模块代码,怎么让它跑起来?Linux提供了几个命令行工具:
| 命令 | 作用 | 常用参数 |
|---|---|---|
insmod |
加载模块 | 直接跟模块文件路径 |
rmmod |
卸载模块 | 跟模块名(不带.ko) |
modprobe |
智能加载/卸载 | 自动处理依赖关系 |
lsmod |
查看已加载模块 | 无参数 |
我个人习惯用modprobe而不是insmod。为什么?因为modprobe会自动帮你解决模块依赖。比如你的驱动依赖i2c-core,用insmod你得先手动加载i2c-core,而modprobe会帮你搞定这一切。
举个例子:
# 编译模块
make
# 加载模块(推荐)
sudo modprobe my_driver
# 或者用 insmod
sudo insmod my_driver.ko
# 查看模块是否加载成功
lsmod | grep my_driver
# 卸载模块
sudo modprobe -r my_driver
# 或者
sudo rmmod my_driver
我曾经踩过的坑:有一次我写了个存储芯片驱动,加载时一切正常,但卸载时系统直接崩溃。查了半天,发现是exit函数里没有正确释放中断。记住,卸载模块时,一定要把init里申请的所有资源都还回去——内存、中断、GPIO,一个都不能漏。
4.3 printk调试技巧
做驱动开发,调试手段有限。没有printf,没有gdb(至少早期没有),你靠什么?printk!
printk和printf很像,但多了个日志级别。来看:
printk(KERN_EMERG "系统崩溃了!\n"); // 级别0,最紧急
printk(KERN_ALERT "需要立即处理\n"); // 级别1
printk(KERN_CRIT "严重错误\n"); // 级别2
printk(KERN_ERR "出错了\n"); // 级别3
printk(KERN_WARNING "警告\n"); // 级别4
printk(KERN_INFO "信息\n"); // 级别5
printk(KERN_DEBUG "调试信息\n"); // 级别6
你想想看,如果所有printk都往控制台输出,那系统日志还不乱套了?所以内核用日志级别来控制哪些信息显示在控制台上。默认情况下,只有级别低于console_loglevel的消息才会显示。
查看当前控制台日志级别:
cat /proc/sys/kernel/printk
# 输出示例:4 4 1 7
# 第一个数字就是 console_loglevel
调试时我经常临时调高级别:
# 让所有级别的消息都显示在控制台
echo 8 > /proc/sys/kernel/printk
我的调试小技巧:开发阶段,我习惯在关键路径上加printk(KERN_DEBUG "..."),然后通过dmesg查看。发布前再用条件编译把调试信息关掉,比如:
#define DEBUG
#ifdef DEBUG
#define dbg_printk(fmt, ...) \
printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#else
#define dbg_printk(fmt, ...) do { } while (0)
#endif
4.4 模块参数传递
写驱动时,你肯定不想把一些配置值写死在代码里。比如存储芯片的I2C地址、中断号、采样频率等。这时候就需要模块参数了。
内核提供了module_param宏,用法很简单:
#include <linux/moduleparam.h>
static int irq_num = 10;
static char *device_name = "default_dev";
module_param(irq_num, int, 0644);
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(irq_num, "Interrupt number for the device");
MODULE_PARM_DESC(device_name, "Device name string");
加载时传参:
sudo insmod my_driver.ko irq_num=33 device_name="my_storage_chip"
或者用modprobe:
sudo modprobe my_driver irq_num=33 device_name="my_storage_chip"
这里有个细节要注意:module_param的第三个参数是文件权限,0644表示允许用户通过sysfs在运行时修改参数。我在项目中就遇到过这种情况——硬件调试时发现中断号不对,不用重新编译模块,直接:
echo 45 > /sys/module/my_driver/parameters/irq_num
嗯,这比重新编译快多了。
参数类型支持:int、long、charp(字符串指针)、bool、intarray(数组)等。数组参数用module_param_array宏。
4.5 一个完整的示例
把上面所有知识点串起来,写一个带参数的存储芯片驱动模块:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
static int chip_addr = 0x50;
static int bus_speed = 400;
static char *chip_model = "AT24C256";
module_param(chip_addr, int, 0644);
module_param(bus_speed, int, 0644);
module_param(chip_model, charp, 0644);
MODULE_PARM_DESC(chip_addr, "I2C address of the storage chip");
MODULE_PARM_DESC(bus_speed, "I2C bus speed in kHz");
MODULE_PARM_DESC(chip_model, "Storage chip model name");
static int __init storage_chip_init(void)
{
printk(KERN_INFO "Storage chip driver loading...\n");
printk(KERN_INFO "Chip model: %s\n", chip_model);
printk(KERN_INFO "I2C address: 0x%x\n", chip_addr);
printk(KERN_INFO "Bus speed: %d kHz\n", bus_speed);
// 这里做实际的硬件初始化
// 比如 i2c_add_driver() 等
return 0;
}
static void __exit storage_chip_exit(void)
{
printk(KERN_INFO "Storage chip driver unloading...\n");
// 清理资源
}
module_init(storage_chip_init);
module_exit(storage_chip_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Storage Driver Expert");
MODULE_DESCRIPTION("A configurable storage chip driver");
加载时传参:
sudo insmod storage_chip.ko chip_addr=0x52 bus_speed=100 chip_model="M24M02"
查看dmesg输出:
[ 1234.567890] Storage chip driver loading...
[ 1234.567895] Chip model: M24M02
[ 1234.567898] I2C address: 0x52
[ 1234.567901] Bus speed: 100 kHz
注意:模块参数虽然方便,但别滥用。我见过有人把整个驱动的配置项都做成参数,结果insmod命令行长得像天书。合理的做法是:把那些硬件相关的、需要灵活调整的参数暴露出来,其他的用#define或设备树配置。
好了,这一章的内容就到这里。内核模块是驱动开发的基础,说白了就是「加载-运行-卸载」这三个动作。把printk用好,把参数传递搞明白,后面写存储芯片驱动就会顺手很多。
下一章,咱们聊聊字符设备驱动框架——那才是真正开始写驱动的地方。