3. 内核模块参数与符号导出:module_param宏的使用、EXPORT_SYMBOL机制、模块依赖关系。
在Linux内核模块开发中,模块参数与符号导出是实现模块间协作与运行时配置的核心机制。本章将深入剖析module_param宏的使用方法、EXPORT_SYMBOL机制的底层原理,以及由此衍生的模块依赖关系管理。掌握这些技术,是构建可复用、可配置、可维护的内核模块的基础。
3.1 内核模块参数:module_param宏
内核模块参数允许用户在加载模块时动态传递配置值,而无需重新编译内核或模块。这一机制通过module_param宏族实现,它本质上是将模块内的全局变量暴露给insmod或modprobe工具。
3.1.1 module_param宏族
标准module_param宏定义在<linux/moduleparam.h>中,其基本形式如下:
module_param(name, type, perm);
- name:模块内已定义的全局变量名。
- type:参数的数据类型,如
int、charp(字符串指针)、bool、long等。 - perm:参数在
/sys/module/<module_name>/parameters/下的文件权限,通常为0644(可读可写)或0444(只读)。0表示不在sysfs中暴露。
关键点:参数变量必须在模块全局作用域中声明,且不能是局部变量或动态分配的内存指针(除非使用
module_param_string等变体)。
3.1.2 参数类型与变体宏
| 宏 | 数据类型 | 说明 |
|---|---|---|
module_param(name, int, perm) |
int |
标准整型参数 |
module_param(name, long, perm) |
long |
长整型参数 |
module_param(name, charp, perm) |
char * |
字符串指针参数,内核会复制传入的字符串 |
module_param(name, bool, perm) |
bool |
布尔型参数,接受0/1、y/n、Y/N |
module_param_array(name, type, num, perm) |
数组 | 传递数组参数,num为指向数组长度的指针 |
module_param_string(name, string, len, perm) |
固定长度字符串 | 将参数复制到预分配的字符数组中 |
3.1.3 完整示例:带参数的可配置模块
以下代码展示了一个使用多种参数类型的字符设备模块框架:
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
static int debug_level = 0;
static char *device_name = "my_device";
static int buffer_size = 1024;
static int irq_numbers[4];
static int irq_count = 0;
module_param(debug_level, int, 0644);
module_param(device_name, charp, 0444);
module_param(buffer_size, int, 0644);
module_param_array(irq_numbers, int, &irq_count, 0444);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
MODULE_PARM_DESC(device_name, "Device name string");
MODULE_PARM_DESC(buffer_size, "Buffer size in bytes");
MODULE_PARM_DESC(irq_numbers, "Array of IRQ numbers");
static int __init param_demo_init(void)
{
int i;
printk(KERN_INFO "Module loaded with:\n");
printk(KERN_INFO " debug_level = %d\n", debug_level);
printk(KERN_INFO " device_name = %s\n", device_name);
printk(KERN_INFO " buffer_size = %d\n", buffer_size);
printk(KERN_INFO " irq_count = %d\n", irq_count);
for (i = 0; i < irq_count; i++)
printk(KERN_INFO " irq[%d] = %d\n", i, irq_numbers[i]);
return 0;
}
static void __exit param_demo_exit(void)
{
printk(KERN_INFO "Module unloaded.\n");
}
module_init(param_demo_init);
module_exit(param_demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Expert");
MODULE_DESCRIPTION("Module parameter demonstration");
加载示例:
sudo insmod param_demo.ko debug_level=2 device_name="test_dev" buffer_size=4096 irq_numbers=10,20,30
3.1.4 参数加载流程SVG
下图展示了从用户空间insmod命令到内核模块参数赋值的完整流程:
3.2 EXPORT_SYMBOL机制:符号导出与共享
内核模块之间需要共享函数和变量时,必须显式导出符号。EXPORT_SYMBOL及其变体EXPORT_SYMBOL_GPL是内核提供的符号导出宏,它们将符号放入内核符号表,供其他模块或内核核心使用。
3.2.1 导出宏对比
| 宏 | 许可证限制 | 使用场景 |
|---|---|---|
EXPORT_SYMBOL(symbol) |
无限制 | 导出给所有模块使用,包括非GPL模块 |
EXPORT_SYMBOL_GPL(symbol) |
仅GPL模块 | 强制使用者必须声明GPL许可证,保护知识产权 |
EXPORT_SYMBOL_NS(symbol, ns) |
无限制 | 带命名空间的导出,需使用者显式导入命名空间 |
EXPORT_SYMBOL_NS_GPL(symbol, ns) |
仅GPL模块 | 带命名空间的GPL限制导出 |
3.2.2 导出与导入实战
导出模块(exporter.c):
#include <linux/module.h>
#include <linux/init.h>
/* 导出一个简单的加法函数 */
int add_numbers(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(add_numbers);
/* 导出一个带GPL限制的函数 */
int multiply_numbers(int a, int b)
{
return a * b;
}
EXPORT_SYMBOL_GPL(multiply_numbers);
/* 导出一个全局变量 */
int shared_counter = 0;
EXPORT_SYMBOL_GPL(shared_counter);
static int __init exporter_init(void)
{
printk(KERN_INFO "Exporter module loaded.\n");
return 0;
}
static void __exit exporter_exit(void)
{
printk(KERN_INFO "Exporter module unloaded.\n");
}
module_init(exporter_init);
module_exit(exporter_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Symbol exporter module");
导入模块(importer.c):
#include <linux/module.h>
#include <linux/init.h>
/* 声明外部符号 */
extern int add_numbers(int a, int b);
extern int multiply_numbers(int a, int b);
extern int shared_counter;
static int __init importer_init(void)
{
int sum, product;
sum = add_numbers(10, 20);
product = multiply_numbers(5, 6);
printk(KERN_INFO "Importer: sum = %d, product = %d\n", sum, product);
printk(KERN_INFO "Importer: shared_counter = %d\n", shared_counter);
/* 修改导出的全局变量 */
shared_counter += 100;
printk(KERN_INFO "Importer: shared_counter after increment = %d\n", shared_counter);
return 0;
}
static void __exit importer_exit(void)
{
printk(KERN_INFO "Importer module unloaded.\n");
}
module_init(importer_init);
module_exit(importer_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Symbol importer module");
注意事项:
- 导入模块必须在导出模块之后加载,否则符号解析失败。
- 使用
EXPORT_SYMBOL_GPL导出的符号,导入模块必须声明MODULE_LICENSE("GPL"),否则加载时内核会拒绝解析。 - 符号导出后,其他模块可以直接调用,但需注意同步问题(如全局变量的并发访问)。
3.2.3 符号导出与解析流程SVG
下图展示了符号从导出到被其他模块解析使用的完整生命周期:
3.3 模块依赖关系:构建可复用的模块体系
当模块A导出的符号被模块B使用时,模块B就依赖于模块A。内核提供了完整的依赖管理机制,确保模块按正确顺序加载和卸载。
3.3.1 依赖关系的产生与声明
依赖关系通过以下方式产生和声明:
- 符号依赖:模块B使用
EXPORT_SYMBOL导出的符号,自动产生对导出模块的依赖。 - 显式依赖:使用
MODULE_SOFTDEP或MODULE_DEPEND宏声明软依赖或硬依赖。 - 设备依赖:通过设备树或ACPI表建立的设备级依赖。
3.3.2 依赖管理工具与文件
| 工具/文件 | 作用 | 使用方式 |
|---|---|---|
depmod |
生成模块依赖关系文件 | sudo depmod -a |
/lib/modules/$(uname -r)/modules.dep |
存储模块依赖关系 | 由depmod自动生成 |
modprobe |
智能加载模块及其依赖 | sudo modprobe importer |
lsmod |
查看当前加载的模块及依赖 | lsmod 查看 Used by 列 |
modinfo |
查看模块信息,包括依赖 | modinfo importer.ko |
3.3.3 依赖关系示例与加载顺序
假设有以下三个模块:
- core.ko:导出基础函数
core_init()和core_cleanup() - mid.ko:依赖
core.ko,导出mid_process() - app.ko:依赖
mid.ko(间接依赖core.ko)
正确的加载顺序为:core.ko → mid.ko → app.ko
卸载顺序相反:app.ko → mid.ko → core.ko
使用modprobe简化操作:
sudo modprobe app 会自动加载 core、mid 和 app 三个模块,并按依赖顺序加载。
3.3.4 模块依赖关系图SVG
下图展示了一个典型的多层模块依赖关系:
3.4 综合实战:构建带参数和符号导出的模块对
本节将前面所有知识点整合为一个完整的实战案例:一个可配置的日志模块(log_core)导出日志接口,另一个业务模块(business)使用这些接口。
3.4.1 log_core模块:参数配置 + 符号导出
/* log_core.c */
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
static int log_level = 3;
static char *log_prefix = "LOG";
module_param(log_level, int, 0644);
module_param(log_prefix, charp, 0444);
MODULE_PARM_DESC(log_level, "Log level (0-5)");
MODULE_PARM_DESC(log_prefix, "Log prefix string");
void log_message(const char *module_name, const char *msg)
{
if (log_level >= 3)
printk(KERN_INFO "[%s][%s] %s\n", log_prefix, module_name, msg);
}
EXPORT_SYMBOL_GPL(log_message);
int get_log_level(void)
{
return log_level;
}
EXPORT_SYMBOL_GPL(get_log_level);
static int __init log_core_init(void)
{
printk(KERN_INFO "Log core module loaded (level=%d, prefix=%s)\n",
log_level, log_prefix);
return 0;
}
static void __exit log_core_exit(void)
{
printk(KERN_INFO "Log core module unloaded.\n");
}
module_init(log_core_init);
module_exit(log_core_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Core logging module with parameters");
3.4.2 business模块:使用导出符号
/* business.c */
#include <linux/module.h>
#include <linux/init.h>
extern void log_message(const char *module_name, const char *msg);
extern int get_log_level(void);
static int __init business_init(void)
{
int level;
level = get_log_level();
log_message("business", "Module initializing...");
log_message("business", "Current log level is %d", level);
if (level >= 4)
log_message("business", "Verbose logging enabled");
return 0;
}
static void __exit business_exit(void)
{
log_message("business", "Module exiting...");
}
module_init(business_init);
module_exit(business_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Business module using exported symbols");
MODULE_SOFTDEP("pre: log_core"); /* 声明软依赖 */
加载与测试:
sudo insmod log_core.ko log_level=4 log_prefix="MYLOG"sudo insmod business.kodmesg | tail -10 查看输出lsmod | grep -E "log_core|business" 查看模块状态
3.5 本章小结
| 知识点 | 核心要点 | 实践建议 |
|---|---|---|
module_param |
运行时配置模块行为,支持多种数据类型 | 为每个参数添加MODULE_PARM_DESC描述 |
EXPORT_SYMBOL |
将符号暴露给其他模块,GPL变体保护许可证 | 优先使用EXPORT_SYMBOL_GPL保护知识产权
|