2. 内核模块基础:模块加载与卸载、module_init/module_exit机制、模块参数传递、模块依赖与符号导出

2.1 内核模块概述

Linux内核模块(Loadable Kernel Module, LKM)是内核在运行时动态加载和卸载的代码片段。模块机制允许开发者在不重新编译整个内核、不重启系统的情况下扩展内核功能,是驱动开发的核心基础。

模块 vs 内置驱动 对比
特性内核模块(.ko)内置驱动(built-in)
加载方式动态加载/卸载编译进内核,随内核启动
灵活性高,可热插拔低,需重新编译内核
内存占用按需加载,节省内存始终驻留内存
调试便利性可单独编译、快速迭代需整体编译,周期长
适用场景驱动开发、实验性功能稳定、必须的驱动

2.2 模块加载与卸载流程

模块的生命周期由两个核心入口函数控制:module_init() 和 module_exit()。加载时内核调用初始化函数,卸载时调用清理函数。

模块加载与卸载流程 insmod / modprobe rmmod module_init() module_exit() 内核符号表注册 内核符号表注销 模块加载时自动调用 module_init(),卸载时自动调用 module_exit(),开发者只需实现这两个函数

2.3 module_init / module_exit 机制

这两个宏是模块的入口和出口声明,它们将指定的初始化/清理函数注册到内核模块框架中。

2.3.1 基本实现模板

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init my_driver_init(void)
{
    printk(KERN_INFO "My driver loaded successfully\n");
    /* 注册设备、分配资源等 */
    return 0;
}

static void __exit my_driver_exit(void)
{
    printk(KERN_INFO "My driver unloaded\n");
    /* 释放资源、注销设备等 */
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Linux kernel module example");
关键点:
  • __init 标记:初始化函数在调用后释放内存,节省内核空间
  • __exit 标记:卸载函数在模块编译进内核时被忽略
  • module_init() 必须返回 0 表示成功,负值表示失败
  • MODULE_LICENSE("GPL") 是必须的,否则内核会标记为“污染”

2.4 模块参数传递

模块支持在加载时通过命令行传递参数,使用 module_param() 宏声明。

2.4.1 参数声明与使用

#include <linux/moduleparam.h>

static int debug_level = 0;
static char *device_name = "default_dev";
static int array_param[4] = {0, 0, 0, 0};
static int array_count;

module_param(debug_level, int, 0644);
module_param(device_name, charp, 0644);
module_param_array(array_param, int, &array_count, 0644);

MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
MODULE_PARM_DESC(device_name, "Device name string");
MODULE_PARM_DESC(array_param, "Array of 4 integers");

2.4.2 加载时传参方式

命令说明
insmod my_module.ko debug_level=2传递整型参数
insmod my_module.ko device_name="mydev"传递字符串参数
insmod my_module.ko array_param=1,2,3,4传递数组参数
modprobe my_module debug_level=2modprobe 自动处理依赖
权限位说明: module_param 的第三个参数是 sysfs 文件权限(如 0644)。设为 0 表示不在 sysfs 中暴露该参数;非 0 值允许用户在运行时通过 /sys/module/<name>/parameters/ 查看或修改。

2.5 模块依赖与符号导出

模块之间可以存在依赖关系:一个模块可以导出符号(函数或变量)供其他模块使用。

2.5.1 符号导出机制

模块依赖与符号导出关系 模块 A(导出者) EXPORT_SYMBOL(func_a) EXPORT_SYMBOL_GPL(var_a) 模块 B(使用者) 调用 func_a() 模块 C(使用者) 使用 var_a 内核符号表 func_a (GPL) var_a (GPL) ... 其他符号 模块 B 和 C 依赖模块 A,加载时必须先加载 A;卸载时需先卸载 B 和 C 使用 EXPORT_SYMBOL_GPL 导出的符号只能被 GPL 许可的模块使用

2.5.2 导出符号的两种方式

宏适用范围使用限制
EXPORT_SYMBOL(func)所有模块无许可证限制
EXPORT_SYMBOL_GPL(func)仅 GPL 许可模块非 GPL 模块无法引用

2.5.3 模块依赖管理

/* 模块 A:导出符号 */
#include <linux/module.h>

void shared_function(void)
{
    printk(KERN_INFO "Shared function called\n");
}
EXPORT_SYMBOL(shared_function);

/* 模块 B:引用符号 */
extern void shared_function(void);

static int __init module_b_init(void)
{
    shared_function();  /* 调用模块 A 导出的函数 */
    return 0;
}
依赖处理命令:
  • depmod:生成模块依赖文件 modules.dep
  • modprobe:自动加载依赖模块(推荐使用)
  • lsmod:查看已加载模块及其依赖关系
  • modinfo my_module.ko:查看模块信息,包括依赖和参数

2.6 完整示例:带参数和符号导出的模块

/* calculator_module.c */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int op_count = 0;
module_param(op_count, int, 0644);
MODULE_PARM_DESC(op_count, "Number of operations to simulate");

int add_numbers(int a, int b)
{
    return a + b;
}
EXPORT_SYMBOL(add_numbers);

int multiply_numbers(int a, int b)
{
    return a * b;
}
EXPORT_SYMBOL_GPL(multiply_numbers);

static int __init calc_init(void)
{
    int i;
    printk(KERN_INFO "Calculator module loaded\n");
    printk(KERN_INFO "Operation count: %d\n", op_count);
    
    for (i = 0; i < op_count; i++) {
        printk(KERN_INFO "Test %d: add(3,4)=%d, mul(2,5)=%d\n",
               i+1, add_numbers(3,4), multiply_numbers(2,5));
    }
    return 0;
}

static void __exit calc_exit(void)
{
    printk(KERN_INFO "Calculator module unloaded\n");
}

module_init(calc_init);
module_exit(calc_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Driver Expert");
MODULE_DESCRIPTION("Calculator module with parameters and symbol export");

2.7 模块编译与测试

2.7.1 Makefile 模板

obj-m := calculator_module.o

KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) clean

2.7.2 测试步骤

  1. 编译模块:make
  2. 加载模块(带参数):sudo insmod calculator_module.ko op_count=3
  3. 查看模块信息:lsmod | grep calculator
  4. 查看内核日志:dmesg | tail -10
  5. 查看参数:cat /sys/module/calculator_module/parameters/op_count
  6. 卸载模块:sudo rmmod calculator_module
常见错误与排查:
  • “Unknown symbol”:模块依赖的符号未导出或依赖模块未加载
  • “Operation not permitted”:内核开启了模块签名验证,需关闭或签名
  • “Invalid module format”:模块与当前内核版本不匹配
  • “Module is already loaded”:模块已存在,需先卸载或使用不同名称

2.8 本章小结

知识点核心要点
模块加载/卸载insmod/rmmod 触发 module_init/module_exit
入口/出口宏module_init() 返回 0 成功,负值失败
模块参数module_param() 声明,支持 int/charp/array 等类型
符号导出EXPORT_SYMBOL / EXPORT_SYMBOL_GPL
依赖管理modprobe 自动处理依赖,depmod 生成依赖文件
调试手段dmesg 查看日志,/sys/module/ 查看参数