2、内核模块基础:模块的加载与卸载(insmod/rmmod)、模块参数传递、模块依赖与符号导出
Linux 内核模块是动态加载到内核中的代码片段,用于扩展内核功能而不需要重新编译整个内核。本章将深入讲解模块的加载与卸载机制、参数传递方法、模块间的依赖关系以及符号导出技术,这些是驱动移植与设备树开发的基础能力。
2.1 模块加载与卸载核心机制
内核模块的生命周期由 insmod(加载)和 rmmod(卸载)两个用户态工具控制。模块内部通过 module_init() 和 module_exit() 宏声明入口与出口函数。
关键概念:模块加载时内核会调用 module_init 指定的函数完成资源申请、设备注册等初始化;卸载时调用 module_exit 指定的函数释放资源。模块一旦加载,便运行在内核空间,拥有最高特权级。
2.1.1 模块加载与卸载示例代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_module_init(void)
{
printk(KERN_INFO "MyDriver: 模块加载成功!\n");
/* 在此处进行硬件资源申请、设备注册等操作 */
return 0;
}
static void __exit my_module_exit(void)
{
printk(KERN_INFO "MyDriver: 模块卸载成功!\n");
/* 在此处释放已申请的资源 */
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux驱动专家");
MODULE_DESCRIPTION("模块加载与卸载示例");
实践要点:使用
__init 和 __exit 宏标记的函数在模块加载/卸载完成后会被内核自动释放内存,节省内核空间。MODULE_LICENSE 必须声明,否则内核会标记为“污染”。
2.2 模块参数传递
内核模块支持在加载时通过 module_param 宏向模块内部传递参数,实现运行时配置。参数类型支持 int、charp、bool、数组等。
| 宏定义 | 参数类型 | 示例 | 说明 |
|---|---|---|---|
module_param(name, type, perm) |
int / long / uint | module_param(debug, int, 0644) |
传递整型参数,perm 为 sysfs 权限 |
module_param(name, charp, perm) |
字符串指针 | module_param(name, charp, 0) |
传递字符串参数 |
module_param(name, bool, perm) |
布尔值 | module_param(enable, bool, 0) |
传递 true/false |
module_param_array(name, type, num, perm) |
数组 | module_param_array(irqs, int, &count, 0) |
传递数组,num 返回元素个数 |
2.2.1 参数传递示例
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int debug_level = 0;
static char *device_name = "default_dev";
static int irq_numbers[4];
static int irq_count;
module_param(debug_level, int, 0644);
module_param(device_name, charp, 0);
module_param_array(irq_numbers, int, &irq_count, 0);
MODULE_PARM_DESC(debug_level, "调试级别: 0-关闭, 1-基本信息, 2-详细信息");
MODULE_PARM_DESC(device_name, "设备名称字符串");
MODULE_PARM_DESC(irq_numbers, "IRQ号数组, 最多4个");
static int __init param_demo_init(void)
{
int i;
printk(KERN_INFO "ParamDemo: debug_level = %d\n", debug_level);
printk(KERN_INFO "ParamDemo: device_name = %s\n", device_name);
printk(KERN_INFO "ParamDemo: irq_count = %d\n", irq_count);
for (i = 0; i < irq_count; i++) {
printk(KERN_INFO "ParamDemo: irq[%d] = %d\n", i, irq_numbers[i]);
}
return 0;
}
static void __exit param_demo_exit(void)
{
printk(KERN_INFO "ParamDemo: 模块卸载\n");
}
module_init(param_demo_init);
module_exit(param_demo_exit);
MODULE_LICENSE("GPL");
加载命令示例:
参数之间用空格分隔,数组元素用逗号分隔。加载后可在
insmod param_demo.ko debug_level=2 device_name="my_uart" irq_numbers=33,34,35参数之间用空格分隔,数组元素用逗号分隔。加载后可在
/sys/module/param_demo/parameters/ 下查看或修改参数。
2.3 模块依赖与符号导出
内核模块之间可以存在依赖关系:一个模块可以导出符号(函数或变量)供其他模块使用。使用 EXPORT_SYMBOL 或 EXPORT_SYMBOL_GPL 宏导出符号,依赖模块在加载时自动解析这些符号。
2.3.1 符号导出示例
/* 导出模块: export_demo.c */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
int my_add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(my_add);
int my_multiply(int a, int b)
{
return a * b;
}
EXPORT_SYMBOL_GPL(my_multiply);
static int __init export_init(void)
{
printk(KERN_INFO "ExportDemo: 符号导出模块加载\n");
return 0;
}
static void __exit export_exit(void)
{
printk(KERN_INFO "ExportDemo: 符号导出模块卸载\n");
}
module_init(export_init);
module_exit(export_exit);
MODULE_LICENSE("GPL");
/* 依赖模块: import_demo.c */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
/* 声明外部符号 */
extern int my_add(int a, int b);
extern int my_multiply(int a, int b);
static int __init import_init(void)
{
int sum = my_add(10, 20);
int product = my_multiply(5, 6);
printk(KERN_INFO "ImportDemo: my_add(10,20) = %d\n", sum);
printk(KERN_INFO "ImportDemo: my_multiply(5,6) = %d\n", product);
return 0;
}
static void __exit import_exit(void)
{
printk(KERN_INFO "ImportDemo: 依赖模块卸载\n");
}
module_init(import_init);
module_exit(import_exit);
MODULE_LICENSE("GPL");
⚠️ 加载顺序要求:必须先加载导出符号的模块(export_demo.ko),再加载依赖模块(import_demo.ko)。卸载时顺序相反:先卸载依赖模块,再卸载导出模块。使用
modprobe 可自动处理依赖关系。
2.3.2 模块依赖关系表
| 命令/文件 | 作用 | 示例 |
|---|---|---|
lsmod |
列出当前所有已加载模块及其依赖 | lsmod | grep import |
modinfo |
查看模块信息,包括依赖的符号 | modinfo import_demo.ko |
/proc/modules |
内核模块列表文件,显示模块大小、依赖等 | cat /proc/modules |
/proc/kallsyms |
查看内核符号表,确认符号是否导出 | cat /proc/kallsyms | grep my_add |
depmod |
生成模块依赖关系文件 modules.dep | depmod -a |
2.4 模块编译与 Makefile 规范
内核模块的编译依赖内核源码树中的 Kbuild 系统。标准的模块 Makefile 模板如下:
# Makefile 模板
obj-m := my_driver.o
# 如果模块由多个源文件组成
# my_driver-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
install:
$(MAKE) -C $(KDIR) M=$(PWD) modules_install
depmod -a
编译与测试步骤:
1.
2.
3.
4.
5.
1.
make — 编译生成 .ko 文件2.
sudo insmod my_driver.ko — 加载模块3.
lsmod | grep my_driver — 确认加载4.
dmesg | tail — 查看内核日志5.
sudo rmmod my_driver — 卸载模块
2.5 常见问题与避坑指南
- 模块加载失败:检查内核版本是否匹配,使用
modinfo查看模块支持的 vermagic。 - 符号未定义:确认导出模块已加载,且符号在
/proc/kallsyms中可见。 - 参数类型不匹配:insmod 传入的参数类型必须与 module_param 声明的类型一致。
- 卸载时系统崩溃:检查 module_exit 中是否释放了所有资源,是否存在悬空指针。
- 模块依赖循环:模块 A 导出符号给 B,B 又导出符号给 A,会导致加载死锁。
- 权限问题:加载/卸载模块需要 root 权限,普通用户无法操作。
调试技巧:在模块代码中多使用
printk 打印调试信息,通过 dmesg -w 实时查看内核日志。对于复杂依赖,使用 modprobe -v 查看详细的依赖解析过程。