第一章: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_initmodule_exit 是两个宏,它们告诉内核:这个模块的入口函数和出口函数分别是什么。我在项目中遇到过有人把这两个宏写反了,结果模块加载时直接崩溃。嗯,这种低级错误其实很容易犯,大家要小心。

编译完成后,你会得到一个 .ko 文件。操作很简单:

# 加载模块
insmod my_driver.ko

# 卸载模块
rmmod my_driver

# 查看已加载的模块
lsmod

这里有个细节:insmodmodprobe 都能加载模块,但 modprobe 会自动处理依赖关系。我建议你平时调试用 insmod,部署时用 modprobe

注意: 卸载模块时,内核会检查模块的引用计数。如果计数不为0,卸载会失败。我曾经在调试一个网络驱动时,忘了关闭socket连接,结果模块死活卸载不掉。排查了半天才发现是引用计数的问题。

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_SYMBOLEXPORT_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没有导出这个符号。嗯,这种问题其实很常见,大家一定要养成检查符号导出的习惯。

避坑指南: 我曾经因为忘记在模块A中调用 EXPORT_SYMBOL,导致模块B加载失败。更坑的是,编译时没有任何警告,只有运行时才会报错。所以,写完代码后一定要用 modprobe 测试一下依赖关系。

小结

好了,第一章的内容就到这里。我们讲了模块的加载与卸载、参数传递、依赖关系和符号导出。这些都是内核驱动开发的基础,说白了就是「搭积木」的基本功。

下一章,我们会深入内核编译系统,看看Kconfig和Makefile是怎么工作的。到时候我会分享一些我在实际项目中的编译优化技巧。咱们下期见!