4. 内核模块参数与符号导出:module_param、EXPORT_SYMBOL、模块间函数调用
在 Linux 内核驱动开发中,模块不仅需要接收外部配置(如设备数量、缓冲区大小),还需要将内部函数或变量暴露给其他模块使用。本章聚焦于 模块参数传递 与 符号导出 两大机制,并深入分析模块间函数调用的实现原理与注意事项。
核心要点: module_param 实现用户态/启动参数配置内核模块;EXPORT_SYMBOL 实现模块间符号共享;模块间调用需处理符号依赖与加载顺序。
4.1 模块参数:module_param 系列宏
模块参数允许在加载模块时(insmod 或 modprobe)或通过 /sys/module/<name>/parameters/ 动态调整模块行为,无需重新编译。
4.1.1 基本用法
#include <linux/moduleparam.h>
static int irq_num = 10;
static char *dev_name = "mydevice";
static int debug_level = 0;
module_param(irq_num, int, 0644);
module_param(dev_name, charp, 0444);
module_param(debug_level, int, 0660);
MODULE_PARM_DESC(irq_num, "IRQ number for the device");
MODULE_PARM_DESC(dev_name, "Device name string");
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
| 参数 | 说明 |
|---|---|
name |
变量名,必须与模块内全局变量一致 |
type |
数据类型:byte、short、int、uint、long、ulong、charp、bool、invbool |
perm |
sysfs 文件权限:0 表示不创建 sysfs 条目;0444 只读;0644 读写 |
加载示例:
运行时修改:
sudo insmod mydriver.ko irq_num=17 dev_name="uart0" debug_level=2运行时修改:
echo 3 > /sys/module/mydriver/parameters/debug_level
4.1.2 数组参数
static int ports[4] = {0x3f8, 0x2f8, 0x3e8, 0x2e8};
static int nr_ports;
module_param_array(ports, int, &nr_ports, 0444);
MODULE_PARM_DESC(ports, "Array of I/O port addresses");
加载时传递数组:insmod mydriver.ko ports=0x3f8,0x2f8,0x3e8,nr_ports 自动记录实际传入个数。
4.2 符号导出:EXPORT_SYMBOL 与 EXPORT_SYMBOL_GPL
内核模块默认只导出 init_module 和 cleanup_module。若希望其他模块调用本模块的函数或访问全局变量,必须显式导出符号。
4.2.1 导出宏对比
| 宏 | 可见性 | 使用场景 |
|---|---|---|
EXPORT_SYMBOL(func) |
所有模块(含非 GPL) | 通用接口,如字符设备注册函数 |
EXPORT_SYMBOL_GPL(func) |
仅 GPL 许可模块 | 核心内部接口,限制商业闭源模块使用 |
4.2.2 导出示例
// exporter.c — 导出模块
#include <linux/module.h>
int my_core_calc(int a, int b)
{
return a * b + 100;
}
EXPORT_SYMBOL(my_core_calc);
int my_gpl_only(void)
{
return 0x55;
}
EXPORT_SYMBOL_GPL(my_gpl_only);
MODULE_LICENSE("GPL");
// importer.c — 导入模块
extern int my_core_calc(int a, int b);
static int __init importer_init(void)
{
int result = my_core_calc(3, 7); // 调用导出函数
pr_info("Result from exporter: %d\n", result);
return 0;
}
module_init(importer_init);
MODULE_LICENSE("GPL");
依赖顺序: 必须先加载导出模块(exporter),再加载导入模块(importer)。卸载时顺序相反。使用
modprobe 可自动处理依赖。
4.3 模块间函数调用机制
模块间调用本质上是 内核符号表的动态链接。加载器在解析未定义符号时,会遍历已加载模块的导出符号表。
4.3.1 调用流程步骤
- 编译阶段: 导入模块(importer)记录未定义符号
func_a,生成重定位条目。 - 加载阶段:
insmod或modprobe扫描导入模块的未定义符号表。 - 符号查找: 遍历已加载模块的
__ksymtab段,匹配符号名。 - 许可检查: 若符号用
EXPORT_SYMBOL_GPL导出,检查导入模块 license 是否为 GPL。 - 重定位: 将导入模块中的符号引用替换为导出模块中的实际地址。
- 调用执行: 模块 B 调用
func_a()时,直接跳转到模块 A 的代码段。
4.4 模块依赖与加载顺序管理
当模块间存在符号依赖时,必须保证加载顺序正确。内核提供两种管理方式:
| 方式 | 命令/文件 | 说明 |
|---|---|---|
| 手动顺序 | insmod exporter.ko → insmod importer.ko |
简单但易出错,卸载时需逆序 |
| 自动依赖 | modprobe importer |
读取 /lib/modules/$(uname -r)/modules.dep,自动加载依赖模块 |
4.4.1 查看模块依赖
# 查看已加载模块的依赖关系
lsmod | grep exporter
# 查看模块文件依赖(需先运行 depmod)
modinfo -F depends importer.ko
# 手动生成依赖文件
sudo depmod -a
4.4.2 模块间调用注意事项
避坑指南:
- 循环依赖: 模块 A 导出符号给 B,B 又导出符号给 A,加载器无法解析,应重构设计。
- 符号冲突: 不同模块导出同名符号,后加载者会覆盖前者,导致不可预期行为。
- 卸载顺序: 若 B 正在使用 A 导出的函数,卸载 A 会导致 B 调用非法地址(内核 panic)。
- GPL 污染: 非 GPL 模块调用
EXPORT_SYMBOL_GPL符号,加载时被拒绝。
4.5 实战:构建可配置的字符设备驱动
综合运用模块参数与符号导出,实现一个可配置缓冲区大小的字符设备,并将核心操作函数导出供其他模块使用。
// char_device_exporter.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/slab.h>
static int buf_size = 1024;
module_param(buf_size, int, 0644);
MODULE_PARM_DESC(buf_size, "Buffer size in bytes (default: 1024)");
static char *device_buffer;
static int major;
int mydev_open(struct inode *inode, struct file *file)
{
pr_info("Device opened\n");
return 0;
}
EXPORT_SYMBOL(mydev_open);
ssize_t mydev_read(struct file *file, char __user *buf,
size_t len, loff_t *off)
{
// 简化实现:返回缓冲区内容
return simple_read_from_buffer(buf, len, off, device_buffer, buf_size);
}
EXPORT_SYMBOL(mydev_read);
ssize_t mydev_write(struct file *file, const char __user *buf,
size_t len, loff_t *off)
{
return simple_write_to_buffer(device_buffer, buf_size, off, buf, len);
}
EXPORT_SYMBOL(mydev_write);
static struct file_operations mydev_fops = {
.owner = THIS_MODULE,
.open = mydev_open,
.read = mydev_read,
.write = mydev_write,
};
static int __init exporter_init(void)
{
device_buffer = kmalloc(buf_size, GFP_KERNEL);
if (!device_buffer)
return -ENOMEM;
major = register_chrdev(0, "mydev_exporter", &mydev_fops);
if (major < 0) {
kfree(device_buffer);
return major;
}
pr_info("Exporter loaded: major=%d, buf_size=%d\n", major, buf_size);
return 0;
}
static void __exit exporter_exit(void)
{
unregister_chrdev(major, "mydev_exporter");
kfree(device_buffer);
pr_info("Exporter unloaded\n");
}
module_init(exporter_init);
module_exit(exporter_exit);
MODULE_LICENSE("GPL");
// char_device_importer.c
#include <linux/module.h>
extern int mydev_open(struct inode *, struct file *);
extern ssize_t mydev_read(struct file *, char __user *, size_t, loff_t *);
extern ssize_t mydev_write(struct file *, const char __user *, size_t, loff_t *);
static int __init importer_init(void)
{
pr_info("Importer loaded, ready to use exported functions\n");
pr_info("mydev_open @ %p\n", mydev_open);
pr_info("mydev_read @ %p\n", mydev_read);
pr_info("mydev_write @ %p\n", mydev_write);
return 0;
}
static void __exit importer_exit(void)
{
pr_info("Importer unloaded\n");
}
module_init(importer_init);
module_exit(importer_exit);
MODULE_LICENSE("GPL");
加载测试:
sudo insmod char_device_exporter.ko buf_size=2048sudo insmod char_device_importer.kodmesg | tail -5 查看输出ls -l /dev/mydev_exporter* 确认设备节点
4.6 本章小结
- module_param 提供灵活的模块配置接口,支持运行时动态调整。
- EXPORT_SYMBOL / EXPORT_SYMBOL_GPL 控制符号可见性,GPL 变体保护核心接口。
- 模块间调用 依赖内核符号表动态重定位,需严格管理加载/卸载顺序。
- 实战中 将模块参数与符号导出结合,可构建高度可配置、可扩展的驱动架构。