第一章:Linux内核模块基础
各位同学,欢迎来到《AOSP内核驱动移植实战指南》。我是你们的老朋友,一个在Linux内核和嵌入式领域摸爬滚打了十几年的工程师。今天咱们开篇,先聊聊内核模块的基础。
说实话,我见过不少刚入行的朋友,一上来就急着写驱动代码,结果连模块怎么加载、怎么卸载都搞不清楚。嗯,这其实是个大坑。内核模块是驱动开发的基石,你想想看,如果连地基都没打牢,房子能稳吗?
1.1 模块加载与卸载
内核模块,说白了就是一段可以在系统运行时动态加载到内核的代码。它不像应用程序那样跑在用户空间,而是直接运行在内核态。我个人习惯把模块想象成「乐高积木」——你需要什么功能,就插上对应的模块,不需要了就拔掉。
先看一个最简单的模块代码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_driver_init(void)
{
printk(KERN_INFO "Hello, Kernel! My driver loaded.\n");
return 0;
}
static void __exit my_driver_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel! My driver unloaded.\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example driver");
这段代码里,module_init 和 module_exit 是两个宏,它们告诉内核:这个模块的入口函数和出口函数分别是什么。我在项目中遇到过有人把这两个宏写反了,结果模块加载时直接崩溃。嗯,这种低级错误其实很容易犯,大家要小心。
编译完成后,你会得到一个 .ko 文件。操作很简单:
# 加载模块
insmod my_driver.ko
# 卸载模块
rmmod my_driver
# 查看已加载的模块
lsmod
这里有个细节:insmod 和 modprobe 都能加载模块,但 modprobe 会自动处理依赖关系。我建议你平时调试用 insmod,部署时用 modprobe。
1.2 模块参数传递
很多时候,我们希望模块在加载时能接受一些参数,比如设备地址、中断号、调试级别等。内核提供了 module_param 宏来实现这个功能。
#include <linux/moduleparam.h>
static int debug_level = 0;
static char *device_name = "default";
module_param(debug_level, int, 0644);
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
MODULE_PARM_DESC(device_name, "Device name string");
加载时传递参数:
insmod my_driver.ko debug_level=2 device_name="my_device"
你可能会问:为什么参数权限是 0644?这个权限值决定了参数在 /sys/module/ 下的可见性。如果设为0,参数在运行时不可见,也不能修改。我个人习惯在调试阶段设为 0644,方便动态调整。
0644 权限。我曾经在项目中需要动态调整缓冲区大小,就是靠这个特性实现的。
1.3 模块依赖关系
模块之间不是孤立的。一个模块可能依赖另一个模块提供的功能。比如,一个字符设备驱动可能依赖一个底层I2C控制器驱动。
依赖关系通过符号引用来建立。当模块A使用了模块B导出的符号时,加载模块A之前必须先加载模块B。内核会自动记录这些依赖关系,你可以通过 modinfo 查看:
modinfo my_driver.ko
输出中会显示 depends 字段,列出所有依赖的模块。如果依赖的模块没有加载,insmod 会报错。但 modprobe 会自动加载依赖模块,这也是为什么我推荐部署时用 modprobe 的原因。
依赖关系文件保存在 /lib/modules/$(uname -r)/modules.dep 中。内核在启动时会读取这个文件,建立完整的依赖树。嗯,这里要注意:如果你手动修改了模块文件,记得运行 depmod 更新依赖信息。
1.4 内核符号导出
模块之间如何共享函数和变量?答案是通过符号导出。内核提供了 EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL 两个宏。
// 在模块A中导出一个函数
int my_helper_function(int arg)
{
return arg * 2;
}
EXPORT_SYMBOL(my_helper_function);
// 在模块B中声明并使用
extern int my_helper_function(int arg);
void some_function(void)
{
int result = my_helper_function(10);
}
EXPORT_SYMBOL 导出的符号对所有模块可见,而 EXPORT_SYMBOL_GPL 只对声明了GPL许可证的模块可见。我个人建议:如果你的代码是开源的,尽量用 EXPORT_SYMBOL_GPL,这能保护你的知识产权。
查看已导出的符号:
cat /proc/kallsyms | grep my_helper_function
我曾经在项目中遇到一个诡异的问题:模块B明明声明了外部函数,编译也通过了,但加载时总是提示「Unknown symbol」。排查了半天,才发现模块A没有导出这个符号。嗯,这种问题其实很常见,大家一定要养成检查符号导出的习惯。
EXPORT_SYMBOL,导致模块B加载失败。更坑的是,编译时没有任何警告,只有运行时才会报错。所以,写完代码后一定要用 modprobe 测试一下依赖关系。
小结
好了,第一章的内容就到这里。我们讲了模块的加载与卸载、参数传递、依赖关系和符号导出。这些都是内核驱动开发的基础,说白了就是「搭积木」的基本功。
下一章,我们会深入内核编译系统,看看Kconfig和Makefile是怎么工作的。到时候我会分享一些我在实际项目中的编译优化技巧。咱们下期见!