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");

编译与使用说明

  1. 创建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

  1. 编译和加载模块:
 

make sudo insmod simple_char.ko

  1. 创建设备节点:


sudo mknod /dev/simple_char c $(cat /proc/devices | grep simple_char | awk '{print $1}') 0

  1. 测试设备: 
echo "Hello" > /dev/simple_char
cat /dev/simple_char

  1. 卸载模块: 

sudo rmmod simple_char

许可证声明注意事项

内核模块必须明确声明许可证,否则会被标记为"污染内核"。常见的许可证声明方式有:

  • MODULE_LICENSE("GPL")
  • MODULE_LICENSE("Dual BSD/GPL")
  • MODULE_LICENSE("Proprietary") // 不推荐,可能导致某些内核功能不可用

这个示例展示了Linux内核模块开发的基本流程,包括模块初始化、设备操作实现、模块卸载等核心功能。

在Linux内核模块开发中,模块间的依赖关系处理是构建复杂内核功能的关键环节。合理的依赖管理能确保模块按正确顺序加载/卸载,避免符号未定义等运行时错误。以下是处理模块依赖关系的全面方案:


‌一、依赖关系的核心机制‌

  1. ‌符号导出与引用‌

    • ‌导出符号‌:通过EXPORT_SYMBOL(func_name)或EXPORT_SYMBOL_GPL(func_name)将模块内部的函数/变量暴露给其他模块。
    • ‌引用符号‌:通过extern声明使用其他模块导出的符号(如extern int (*other_module_func)(void))。
  2. ‌依赖声明文件(Module.symvers)‌

    • 编译时自动生成,记录模块导出的符号及其校验和(CRC),确保符号版本兼容性。
  3. ‌依赖顺序控制‌

    • 模块加载顺序需满足:被依赖模块先加载,依赖模块后加载。

‌二、依赖关系的处理方式‌

‌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。
‌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的风险‌:强制卸载可能导致其他模块崩溃,仅限调试使用。

‌四、依赖关系问题排查‌

  1. ‌常见错误‌

    • ‌"Unknown symbol"‌:依赖模块未加载或符号未导出。
    • ‌"Circular dependency detected"‌:循环依赖需重构代码。
    • ‌"Module version mismatch"‌:符号版本不兼容,需确保使用相同的Module.symvers。
  2. ‌调试工具‌

    • 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开发环境与目标环境一致

‌六、复杂场景处理‌

  1. ‌内核版本兼容性‌

    • 使用MODULE_VERSION和__attribute__((weak))处理符号版本差异。

    int __attribute__((weak)) old_api(void) { return -ENOSYS; }

  2. ‌动态加载/卸载‌

    • 在模块中实现module_exit回调,清理依赖符号的引用计数。
  3. ‌混合许可证依赖‌

    • GPL模块只能依赖GPL或GPL兼容模块(如MODULE_LICENSE("GPL"))。
    • 非GPL模块需通过EXPORT_SYMBOL_GPL隔离GPL符号。

‌七、总结‌

  • ‌核心原则‌:依赖关系应通过符号导出和加载顺序管理,而非硬编码路径。
  • ‌工具链‌:优先使用modprobe、depmod和lsmod,而非直接操作insmod/rmmod。
  • ‌错误预防‌:通过代码检查(如sparse工具)和测试矩阵覆盖不同加载顺序。

通过以上方法,可以确保Linux内核模块的依赖关系清晰、健壮,避免运行时错误。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐