Linux内核模块开发与动态加载
Linux内核模块是一种可以在运行时动态加载到内核中的代码,用于扩展内核功能而无需重新编译整个内核或重启系统。这种机制为系统提供了极大的灵活性。
主要应用场景
- 设备驱动开发
- 文件系统扩展
- 网络协议栈增强
- 安全模块实现
- 性能监控工具
技术细节实现
1. 模块编译与加载
内核模块通常使用Makefile进行编译,使用insmod/rmmod或modprobe命令加载和卸载。
2. 模块依赖与符号导出
模块间可以通过EXPORT_SYMBOL()宏共享函数和变量,建立依赖关系。
3. 内核子系统交互
模块可以调用内核提供的API与内存管理、进程调度等子系统交互。
实践案例:字符设备驱动
以下是实现一个简单字符设备驱动的内核模块代码:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "simple_char"
static int major_num;
static char msg[100] = {0};
static int device_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "Device opened\n");
return 0;
}
static ssize_t device_read(struct file *filp, char *buffer, size_t length, loff_t *offset) {
int bytes_read = 0;
if (*msg == 0) return 0;
while (length && *msg) {
put_user(*(msg++), buffer++);
length--;
bytes_read++;
}
return bytes_read;
}
static ssize_t device_write(struct file *filp, const char *buffer, size_t length, loff_t *offset) {
int i;
for (i = 0; i < length && i < 99; i++) {
get_user(msg[i], buffer + i);
}
msg[i] = 0;
return i;
}
static struct file_operations fops = {
.open = device_open,
.read = device_read,
.write = device_write,
};
static int __init simple_char_init(void) {
major_num = register_chrdev(0, DEVICE_NAME, &fops);
if (major_num < 0) {
printk(KERN_ALERT "Registering char device failed\n");
return major_num;
}
printk(KERN_INFO "Registered char device major number %d\n", major_num);
return 0;
}
static void __exit simple_char_exit(void) {
unregister_chrdev(major_num, DEVICE_NAME);
printk(KERN_INFO "Unregistered char device\n");
}
module_init(simple_char_init);
module_exit(simple_char_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");
编译与使用说明
- 创建Makefile文件:
-m := simple_char.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
- 编译和加载模块:
make sudo insmod simple_char.ko
- 创建设备节点:
sudo mknod /dev/simple_char c $(cat /proc/devices | grep simple_char | awk '{print $1}') 0
- 测试设备:
echo "Hello" > /dev/simple_char
cat /dev/simple_char
- 卸载模块:
sudo rmmod simple_char
许可证声明注意事项
内核模块必须明确声明许可证,否则会被标记为"污染内核"。常见的许可证声明方式有:
- MODULE_LICENSE("GPL")
- MODULE_LICENSE("Dual BSD/GPL")
- MODULE_LICENSE("Proprietary") // 不推荐,可能导致某些内核功能不可用
这个示例展示了Linux内核模块开发的基本流程,包括模块初始化、设备操作实现、模块卸载等核心功能。

在Linux内核模块开发中,模块间的依赖关系处理是构建复杂内核功能的关键环节。合理的依赖管理能确保模块按正确顺序加载/卸载,避免符号未定义等运行时错误。以下是处理模块依赖关系的全面方案:
一、依赖关系的核心机制
-
符号导出与引用
- 导出符号:通过
EXPORT_SYMBOL(func_name)或EXPORT_SYMBOL_GPL(func_name)将模块内部的函数/变量暴露给其他模块。 - 引用符号:通过
extern声明使用其他模块导出的符号(如extern int (*other_module_func)(void))。
- 导出符号:通过
-
依赖声明文件(
Module.symvers)- 编译时自动生成,记录模块导出的符号及其校验和(CRC),确保符号版本兼容性。
-
依赖顺序控制
- 模块加载顺序需满足:被依赖模块先加载,依赖模块后加载。
二、依赖关系的处理方式
1. 显式依赖声明(推荐)
- 使用
depends字段:在模块的Makefile或Kconfig中声明依赖关系。obj-m := dependent_module.o dependent_module-objs := dep1.o dep2.o # 声明依赖base_module.ko ccflags-y += -DDEPENDS_ON_BASE_MODULE - 动态检测依赖:在模块初始化时检查依赖符号是否存在。
if (!IS_ERR_OR_NULL(symbol_address)) { // 依赖符号存在,继续初始化 } else { printk(KERN_ERR "依赖模块未加载!\n"); return -ENODEV; }
2. 隐式依赖(通过符号导出)
- 自动依赖解析:当模块A引用模块B导出的符号时,内核加载器(
modprobe)会自动先加载模块B。- 示例:模块A使用
EXPORT_SYMBOL(b_func),模块B通过extern int b_func(void);调用,加载时modprobe moduleB会自动加载moduleA。
- 示例:模块A使用
3. 使用modprobe规则文件
- 创建依赖规则:在
/etc/modprobe.d/下创建规则文件(如my_modules.conf),定义显式依赖。# 强制先加载base_module install dependent_module /sbin/modprobe base_module; /sbin/insmod $CMDLINE_OPTS
三、依赖关系管理实践
1. 模块开发阶段
- 避免循环依赖:模块A依赖模块B,模块B又依赖模块A会导致加载失败。需重构设计(如合并模块或引入中间层)。
- 使用
MODINFO元数据:在模块代码中通过MODULE_INFO添加依赖描述(非强制,但便于文档化)。MODULE_INFO(depends, "base_module,another_module");
2. 部署与加载阶段
- 使用
modprobe而非insmod:modprobe能自动解析依赖关系,而insmod不会。# 正确方式:自动加载依赖 sudo modprobe dependent_module # 错误方式:可能导致符号未定义 sudo insmod dependent_module.ko - 检查依赖关系:通过
modinfo查看模块依赖。modinfo dependent_module.ko | grep depends
3. 卸载阶段的依赖处理
- 反向卸载顺序:卸载时需先卸载依赖模块,再卸载被依赖模块。
- 使用
rmmod --force的风险:强制卸载可能导致其他模块崩溃,仅限调试使用。
四、依赖关系问题排查
-
常见错误
- "Unknown symbol":依赖模块未加载或符号未导出。
- "Circular dependency detected":循环依赖需重构代码。
- "Module version mismatch":符号版本不兼容,需确保使用相同的
Module.symvers。
-
调试工具
dmesg:查看内核日志中的依赖错误。lsmod:检查已加载模块及其依赖。depmod -a:重新生成模块依赖关系数据库(/lib/modules/$(uname -r)/modules.dep)。
五、最佳实践总结
| 场景 | 推荐方案 | 示例 |
|---|---|---|
| 模块A调用模块B的函数 | 模块B导出符号,模块A引用 | EXPORT_SYMBOL(b_func) |
| 强制加载顺序 | 使用modprobe规则文件 | /etc/modprobe.d/rules.conf |
| 避免循环依赖 | 合并模块或引入中间层 | 将共享逻辑提取到新模块 |
| 符号版本兼容 | 统一使用相同的Module.symvers | 开发环境与目标环境一致 |
六、复杂场景处理
-
内核版本兼容性
- 使用
MODULE_VERSION和__attribute__((weak))处理符号版本差异。
int __attribute__((weak)) old_api(void) { return -ENOSYS; } - 使用
-
动态加载/卸载
- 在模块中实现
module_exit回调,清理依赖符号的引用计数。
- 在模块中实现
-
混合许可证依赖
- GPL模块只能依赖GPL或GPL兼容模块(如
MODULE_LICENSE("GPL"))。 - 非GPL模块需通过
EXPORT_SYMBOL_GPL隔离GPL符号。
- GPL模块只能依赖GPL或GPL兼容模块(如
七、总结
- 核心原则:依赖关系应通过符号导出和加载顺序管理,而非硬编码路径。
- 工具链:优先使用
modprobe、depmod和lsmod,而非直接操作insmod/rmmod。 - 错误预防:通过代码检查(如
sparse工具)和测试矩阵覆盖不同加载顺序。
通过以上方法,可以确保Linux内核模块的依赖关系清晰、健壮,避免运行时错误。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)