2、内核模块基础:模块的加载与卸载(insmod/rmmod)、模块参数传递、模块依赖与符号导出

Linux 内核模块是动态加载到内核中的代码片段,用于扩展内核功能而不需要重新编译整个内核。本章将深入讲解模块的加载与卸载机制、参数传递方法、模块间的依赖关系以及符号导出技术,这些是驱动移植与设备树开发的基础能力。

2.1 模块加载与卸载核心机制

内核模块的生命周期由 insmod(加载)和 rmmod(卸载)两个用户态工具控制。模块内部通过 module_init() 和 module_exit() 宏声明入口与出口函数。

关键概念:模块加载时内核会调用 module_init 指定的函数完成资源申请、设备注册等初始化;卸载时调用 module_exit 指定的函数释放资源。模块一旦加载,便运行在内核空间,拥有最高特权级。
模块加载与卸载完整流程 insmod mydrv.ko rmmod mydrv sys_init_module / sys_delete_module module_init(my_init_function) 资源申请 / 设备注册 / 文件系统注册 module_exit(my_exit_function) 资源释放 / 设备注销 / 文件系统注销 模块状态: MODULE_STATE_LIVE 模块状态: MODULE_STATE_GOING 💡 模块加载后可通过 lsmod 查看已加载模块列表;rmmod 前需确保模块未被其他模块或进程引用。

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/ 下查看或修改参数。
模块参数传递数据流 insmod param_demo.ko debug_level=2 device_name="my_uart" 内核参数解析 module_param 宏展开 类型检查与赋值 sysfs 节点创建 模块变量 debug_level = 2 device_name = "my_uart" irq_numbers[0..3] 🔧 参数权限 perm=0 表示不在 sysfs 中暴露;perm=0644 表示所有用户可读、root 可写(运行时修改)。 🔧 字符串参数需注意内存生命周期,模块卸载前不要释放外部传入的字符串。

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
模块依赖与符号导出关系图 export_demo.ko EXPORT_SYMBOL(my_add) EXPORT_SYMBOL_GPL(my_multiply) 内核符号表: /proc/kallsyms import_demo.ko extern int my_add(int, int) extern int my_multiply(int, int) 运行时符号解析 符号引用 加载顺序: insmod export_demo.ko → insmod import_demo.ko 卸载顺序: rmmod import_demo → rmmod export_demo 📌 EXPORT_SYMBOL_GPL 导出的符号只能被 GPL 许可的模块使用,非 GPL 模块无法解析。 📌 使用 modprobe 加载模块时,会自动读取 /lib/modules/$(uname -r)/modules.dep 解决依赖。 📌 模块卸载时需确保引用计数为 0,可通过 lsmod 查看 Used by 列。

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. 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 查看详细的依赖解析过程。