3. 内核模块基础:模块概念、模块加载与卸载、模块参数、模块依赖与符号导出
Linux 内核是一个单体内核(Monolithic Kernel),但通过内核模块(Kernel Module)机制实现了动态扩展能力。模块是可以在运行时加载到内核或从内核卸载的代码片段,无需重新编译整个内核或重启系统。在 Android 内核驱动开发中,绝大多数设备驱动都以模块形式存在。
3.1 模块概念与生命周期
内核模块本质上是一个目标文件(.ko),包含初始化函数、清理函数以及可选的导出符号。模块的生命周期分为四个阶段:加载、初始化、运行、卸载。
核心要点:模块运行在内核空间,拥有最高特权级(Ring 0),可以访问所有硬件资源和内核数据结构。模块代码中的任何错误(如空指针解引用、死锁)都会导致内核崩溃(Kernel Panic)。
3.2 模块加载与卸载
模块的加载和卸载通过用户空间工具触发,最终调用内核系统调用完成。最简模块必须包含两个宏:module_init() 和 module_exit()。
3.2.1 最小内核模块示例
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Android Kernel!\n");
return 0; // 0 表示初始化成功
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Android Kernel!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module for Android");
3.2.2 加载与卸载命令
| 操作 | 命令 | 说明 |
|---|---|---|
| 加载模块 | insmod hello.ko |
直接加载,不处理依赖 |
| 加载模块(含依赖) | modprobe hello |
自动加载依赖模块 |
| 卸载模块 | rmmod hello |
卸载指定模块 |
| 查看已加载模块 | lsmod |
列出所有模块及使用计数 |
| 查看模块信息 | modinfo hello.ko |
显示作者、描述、参数等 |
Android 特有注意事项:在 Android 设备上,
insmod 和 rmmod 需要 root 权限。生产环境通常使用 modprobe 配合 /lib/modules/ 下的模块配置。部分 Android 内核启用了 CONFIG_MODULE_SIG_FORCE,要求模块必须经过签名验证才能加载。
3.3 模块参数
模块参数允许在加载时向模块传递配置值,无需重新编译。使用 module_param() 宏声明参数,支持基本数据类型和字符串。
3.3.1 参数声明与使用
#include <linux/moduleparam.h>
static int debug_level = 0;
static char *device_name = "default_dev";
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");
3.3.2 参数传递方式
- 命令行传递:
insmod mydriver.ko debug_level=2 device_name="sensor0" - modprobe 配置文件:在
/etc/modprobe.d/下创建.conf文件,写入options mydriver debug_level=2 - sysfs 运行时修改:如果权限位包含写权限(如 0644),可通过
/sys/module/mydriver/parameters/debug_level动态修改
| 参数类型 | 宏 | sysfs 权限 | 示例 |
|---|---|---|---|
| 整型 | module_param(name, int, perm) |
0644 | debug_level=2 |
| 布尔型 | module_param(name, bool, perm) |
0644 | enable_irq=1 |
| 字符串 | module_param(name, charp, perm) |
0444 | device_name="sensor" |
| 数组 | module_param_array(name, type, num, perm) |
0444 | irq_list=10,20,30 |
3.4 模块依赖与符号导出
模块之间可以存在依赖关系:一个模块可能使用另一个模块导出的函数或变量。内核通过符号导出(Symbol Export)机制实现模块间的接口共享。
3.4.1 符号导出宏
| 宏 | 可见性 | 使用场景 |
|---|---|---|
EXPORT_SYMBOL(symbol) |
所有模块可见 | 通用驱动接口 |
EXPORT_SYMBOL_GPL(symbol) |
仅 GPL 许可模块可见 | 需要 GPL 兼容性保护 |
3.4.2 导出与使用示例
导出模块(exporter.c):
#include <linux/module.h>
int add_numbers(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(add_numbers);
static int __init exporter_init(void)
{
pr_info("Exporter module loaded\n");
return 0;
}
static void __exit exporter_exit(void)
{
pr_info("Exporter module unloaded\n");
}
module_init(exporter_init);
module_exit(exporter_exit);
MODULE_LICENSE("GPL");
使用模块(importer.c):
#include <linux/module.h>
extern int add_numbers(int a, int b);
static int __init importer_init(void)
{
int result = add_numbers(10, 20);
pr_info("Result from exporter: %d\n", result);
return 0;
}
static void __exit importer_exit(void)
{
pr_info("Importer module unloaded\n");
}
module_init(importer_init);
module_exit(importer_exit);
MODULE_LICENSE("GPL");
3.4.3 依赖管理
- 自动依赖解析:
modprobe读取/lib/modules/$(uname -r)/modules.dep文件,自动加载所有依赖模块 - 手动查看依赖:
modinfo -F depends exporter.ko显示依赖列表 - 引用计数:内核维护每个模块的引用计数(
lsmod的Used by列),计数不为 0 时无法卸载
3.5 模块编译与 Makefile
内核模块的编译依赖内核源码树中的 Kbuild 系统。标准的模块 Makefile 模板如下:
obj-m := hello.o
# 如果模块由多个源文件组成
# hello-objs := main.o helper.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
Android 内核编译提示:在 AOSP 环境中,使用
make modules 或 make modules_install 编译和安装模块。交叉编译时需要设置 ARCH 和 CROSS_COMPILE 环境变量,并指定正确的内核源码路径。
3.6 本章小结
| 知识点 | 关键命令/宏 | 注意事项 |
|---|---|---|
| 模块加载 | insmod / modprobe |
Android 需 root 权限,可能要求模块签名 |
| 模块卸载 | rmmod |
引用计数为 0 才能卸载 |
| 模块参数 | module_param() |
sysfs 权限位控制运行时修改能力 |
| 符号导出 | EXPORT_SYMBOL() |
GPL 版本限制非 GPL 模块使用 |
| 依赖管理 | modprobe / modules.dep |
加载顺序:先依赖后使用;卸载顺序相反 |