3. Linux内核模块基础
在Linux驱动开发中,内核模块(Loadable Kernel Module,LKM)是最基本的交付单元。它允许我们在不重启系统、不重新编译整个内核的前提下,把驱动代码动态插入内核空间运行,也能在不需要时安全卸载。理解模块的加载卸载机制、参数传递、符号导出、日志输出以及依赖与版本管理,是后续所有硬件联调工作的地基。本节围绕这五个核心点展开,帮助你把“写一个能跑起来的模块”升级为“写一个可维护、可调试、可复用的模块”。
一、模块的加载与卸载:从insmod到rmmod
内核模块本质是一个可重定位的ELF目标文件,扩展名为.ko。它不能像用户态程序那样直接执行,而是由内核的模块加载器把它映射进内核地址空间,完成符号解析和重定位后,调用模块的初始化函数。加载与卸载的入口由两个宏声明:
#include <linux/module.h>
#include <linux/init.h>
static int __init mydrv_init(void)
{
pr_info("mydrv: init\n");
return 0; /* 返回0表示加载成功 */
}
static void __exit mydrv_exit(void)
{
pr_info("mydrv: exit\n");
}
module_init(mydrv_init);
module_exit(mydrv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("engineer");
MODULE_DESCRIPTION("demo driver module");
加载时使用insmod mydrv.ko,卸载时使用rmmod mydrv。更推荐用modprobe,因为它会自动处理依赖关系并从/lib/modules/$(uname -r)/下查找模块。加载成功后,模块会出现在/proc/modules和lsmod的输出中。初始化函数返回非0值会导致加载失败,此时内核会回滚已分配的资源,因此init函数中出错必须释放已申请资源再返回。
二、模块参数:让驱动行为可配置
硬编码的驱动难以适配不同硬件。模块参数允许在加载时通过命令行传入配置,使用module_param系列宏声明:
static int debug_level = 0;
static char *dev_name = "mydrv0";
static int buf_size = 4096;
module_param(debug_level, int, 0644);
module_param(dev_name, charp, 0644);
module_param(buf_size, int, 0444);
MODULE_PARM_DESC(debug_level, "debug verbosity 0-3");
MODULE_PARM_DESC(buf_size, "DMA buffer size in bytes");
加载时即可传参:insmod mydrv.ko debug_level=2 buf_size=8192。参数类型支持int、uint、long、charp、bool以及数组形式module_param_array。权限位0644表示该参数会在/sys/module/mydrv/parameters/下生成可读写节点,运行时也能查看甚至修改(若为可写)。这是驱动调试时非常实用的“运行时旋钮”。
三、符号导出:模块之间的接口
当多个模块需要协作时,一个模块可以把函数或变量导出给其他模块使用,使用EXPORT_SYMBOL或EXPORT_SYMBOL_GPL:
int mydrv_register_client(struct client_ops *ops)
{
/* ... */
return 0;
}
EXPORT_SYMBOL_GPL(mydrv_register_client);
导出后,其他模块在加载时就能解析到这个符号。EXPORT_SYMBOL_GPL只允许GPL兼容许可证的模块使用,这是内核维护者鼓励开源的一种机制。若模块未声明MODULE_LICENSE("GPL"),加载依赖GPL符号的模块会失败,并提示Unknown symbol或disagrees about version of symbol。因此,导出符号时要谨慎:它一旦发布,就构成模块间的稳定接口,随意修改签名会破坏依赖方。
四、printk日志:内核里的printf
内核不能用printf,取而代之的是printk及其封装宏pr_info、pr_err、dev_dbg等。日志带有级别,级别越低越紧急:
| 级别宏 | 数值 | 典型用途 |
|---|---|---|
| KERN_EMERG | 0 | 系统不可用 |
| KERN_ALERT | 1 | 必须立即处理 |
| KERN_ERR | 3 | 驱动错误 |
| KERN_WARNING | 4 | 警告信息 |
| KERN_INFO | 6 | 常规提示 |
| KERN_DEBUG | 7 | 调试细节 |
日志输出到内核环形缓冲区,可用dmesg查看,也会被rsyslog写入/var/log/kern.log。控制台输出级别由/proc/sys/kernel/printk控制,格式为“当前、默认、最小、最大”。调试时常用echo 8 > /proc/sys/kernel/printk临时打开全部级别。注意printk在中断上下文和原子上下文中也可安全调用,但高频打印会拖慢系统,生产驱动应使用pr_debug配合dynamic_debug按需开启。
五、模块依赖与版本管理
模块之间可以互相依赖。例如usb_storage依赖scsi_mod。依赖关系记录在/lib/modules/$(uname -r)/modules.dep中,由depmod生成。modprobe读取该文件自动先加载被依赖模块。若手动insmod一个依赖未满足的模块,会报Unknown symbol in module。
版本管理方面,内核通过vermagic字符串校验模块与内核是否匹配,内容包括内核版本、SMP、抢占、模块版本等。使用modinfo mydrv.ko可查看。若内核开启了CONFIG_MODVERSIONS,还会对导出符号做CRC校验,符号签名不一致时加载失败。跨内核版本移植模块时,必须用目标内核的头文件重新编译,不能直接拷贝.ko。
rmmod当成“停止驱动”。实际上若模块的引用计数不为0(例如设备节点仍被打开),rmmod会失败并提示Module is in use。此时应先关闭使用者,或检查是否有其他模块依赖它。六、综合案例:一个可配置的调试模块
下面把上述知识点串起来,形成一个最小可用的调试模块,它导出符号、接收参数、打印日志:
static int dbg_level = 1;
module_param(dbg_level, int, 0644);
int dbg_emit(const char *tag, int val)
{
if (dbg_level > 0)
pr_info("dbg: %s=%d\n", tag, val);
return 0;
}
EXPORT_SYMBOL_GPL(dbg_emit);
static int __init dbg_init(void)
{
pr_info("dbg: loaded, level=%d\n", dbg_level);
return 0;
}
static void __exit dbg_exit(void)
{
pr_info("dbg: unloaded\n");
}
module_init(dbg_init);
module_exit(dbg_exit);
MODULE_LICENSE("GPL");
加载后,另一个模块即可调用dbg_emit;通过echo 0 > /sys/module/dbg/parameters/dbg_level可动态关闭日志。整个流程覆盖了模块生命周期、参数、符号、日志与依赖管理,是驱动联调中最常用的骨架。