3. 内核模块基础:模块概念、模块加载与卸载、模块参数传递、模块依赖与符号导出

Linux 内核是一个单体内核(Monolithic Kernel),但通过内核模块(Loadable Kernel Module, LKM)机制实现了动态扩展能力。模块是可以在运行时独立加载到内核或从内核卸载的代码段,无需重新编译整个内核或重启系统。本章深入剖析模块的完整生命周期、参数化配置、依赖管理以及符号导出规则,为后续 GPIO 驱动开发奠定基础。

核心要点:模块机制是嵌入式 Linux 驱动开发的基础设施。理解模块的加载/卸载流程、参数传递方式以及符号导出规则,是编写可维护、可配置驱动程序的必备技能。

3.1 模块概念与生命周期

内核模块本质上是一个目标文件(.ko),包含初始化函数、清理函数以及可选的导出符号。模块的生命周期分为四个阶段:加载 → 初始化 → 运行 → 卸载。

内核模块生命周期 insmod 加载 module_init() 初始化 init_function() 运行中 提供服务 rmmod 卸载 module_exit() 关键宏 module_init(init_func) module_exit(exit_func)
提示:模块的 init 函数返回 0 表示成功,返回负数表示失败。内核会记录模块的引用计数,当引用计数不为 0 时,rmmod 操作会被拒绝。

3.2 模块加载与卸载

模块的加载与卸载通过用户空间工具 insmod、rmmod、modprobe 完成,内核则通过系统调用 init_module() 和 delete_module() 实现底层操作。

3.2.1 加载流程

  1. insmod 读取 .ko 文件到用户空间缓冲区。
  2. 调用 init_module() 系统调用,将模块代码复制到内核空间。
  3. 内核执行模块重定位,解析未定义符号。
  4. 调用模块的 module_init() 指定的初始化函数。
  5. 初始化成功则模块进入运行状态,失败则回滚并释放资源。

3.2.2 卸载流程

  1. rmmod 调用 delete_module() 系统调用。
  2. 内核检查模块引用计数是否为 0。
  3. 调用模块的 module_exit() 指定的清理函数。
  4. 释放模块占用的内核内存。

3.2.3 基础模块示例

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

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, GPIO Module loaded!\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, GPIO Module unloaded!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Driver Expert");
MODULE_DESCRIPTION("A simple GPIO demo module");
注意:__init 和 __exit 标记告诉内核,初始化函数在调用后可以释放内存,清理函数在模块内置时可以不编译。这有助于减少内核映像大小。

3.3 模块参数传递

模块参数允许在加载时向模块传递配置值,无需修改代码即可调整驱动行为。参数通过 module_param() 宏声明,支持多种数据类型。

3.3.1 参数声明宏

宏 数据类型 示例 说明
module_param(name, type, perm) int, long, charp, bool 等 module_param(gpio_num, int, 0644) 基本类型参数
module_param_array(name, type, nump, perm) 数组 module_param_array(pins, int, &count, 0444) 数组类型参数
module_param_string(name, string, len, perm) 字符串 module_param_string(dev_name, buf, 32, 0) 字符串参数

3.3.2 参数权限位

perm 参数控制参数在 /sys/module/<module_name>/parameters/ 下的文件权限:

  • 0:参数不在 sysfs 中显示,仅加载时可用。
  • 0444:只读,所有用户可查看。
  • 0644:所有者可读写,其他用户只读。

3.3.3 带参数的模块示例

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

static int gpio_pin = 17;          // 默认 GPIO 17
static char *gpio_label = "LED";   // 默认标签
static int gpio_pins[4];           // 数组参数
static int gpio_count;

module_param(gpio_pin, int, 0644);
MODULE_PARM_DESC(gpio_pin, "GPIO pin number (default: 17)");
module_param(gpio_label, charp, 0444);
MODULE_PARM_DESC(gpio_label, "GPIO label string");
module_param_array(gpio_pins, int, &gpio_count, 0444);
MODULE_PARM_DESC(gpio_pins, "Array of GPIO pins");

static int __init param_demo_init(void)
{
    int i;
    printk(KERN_INFO "GPIO pin: %d\n", gpio_pin);
    printk(KERN_INFO "GPIO label: %s\n", gpio_label);
    printk(KERN_INFO "GPIO count: %d\n", gpio_count);
    for (i = 0; i < gpio_count; i++)
        printk(KERN_INFO "  pin[%d] = %d\n", i, gpio_pins[i]);
    return 0;
}

static void __exit param_demo_exit(void)
{
    printk(KERN_INFO "Param demo module unloaded\n");
}

module_init(param_demo_init);
module_exit(param_demo_exit);

MODULE_LICENSE("GPL");

加载时传递参数:

sudo insmod param_demo.ko gpio_pin=23 gpio_label="BUZZER" gpio_pins=10,11,12,13
模块参数传递流程 insmod 命令行 gpio_pin=23 label="LED" 内核参数解析 module_param() 宏 sysfs 接口 /sys/module/.../parameters/ 运行时修改参数(perm > 0) echo 25 > /sys/module/param_demo/parameters/gpio_pin

3.4 模块依赖与符号导出

模块之间可以存在依赖关系:一个模块可以导出符号(函数或变量),供其他模块使用。内核通过 EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL 宏控制符号的可见性。

3.4.1 符号导出宏

宏 可见范围 使用场景
EXPORT_SYMBOL(func) 所有模块(包括非 GPL 模块) 通用接口,如 GPIO 库函数
EXPORT_SYMBOL_GPL(func) 仅 GPL 许可的模块 内核核心接口,保护知识产权

3.4.2 模块依赖管理

当模块 B 使用了模块 A 导出的符号时,模块 B 依赖于模块 A。加载时必须先加载 A,再加载 B。卸载时顺序相反。

// 模块 A:gpio_core.c
#include <linux/module.h>
#include <linux/gpio.h>

int gpio_core_set_value(int pin, int value)
{
    // 实际 GPIO 操作
    gpio_set_value(pin, value);
    return 0;
}
EXPORT_SYMBOL(gpio_core_set_value);

static int __init core_init(void) { return 0; }
static void __exit core_exit(void) {}
module_init(core_init);
module_exit(core_exit);
MODULE_LICENSE("GPL");

// 模块 B:gpio_user.c
extern int gpio_core_set_value(int pin, int value);

static int __init user_init(void)
{
    gpio_core_set_value(17, 1);  // 使用模块 A 导出的函数
    return 0;
}
module_init(user_init);
MODULE_LICENSE("GPL");

3.4.3 查看模块依赖

使用 modinfo 和 lsmod 查看模块依赖关系:

# 查看模块信息
modinfo gpio_core.ko
# 输出示例:
# filename:       gpio_core.ko
# license:        GPL
# depends:        
# vermagic:       5.10.0-rc7+

modinfo gpio_user.ko
# 输出示例:
# filename:       gpio_user.ko
# license:        GPL
# depends:        gpio_core
# vermagic:       5.10.0-rc7+

# 查看已加载模块的依赖树
lsmod | grep gpio
# gpio_user              16384  0
# gpio_core              20480  1 gpio_user
模块依赖与符号导出关系 模块 A: gpio_core EXPORT_SYMBOL(func) 提供底层 GPIO 操作 模块 B: gpio_user extern 声明使用 依赖 gpio_core 符号引用 内核符号表 (kallsyms) /proc/kallsyms | depmod 生成依赖信息

3.5 模块工具与调试

掌握以下工具链,可以高效管理模块生命周期:

工具 功能 常用选项
insmod 加载模块 insmod module.ko [param=value]
rmmod 卸载模块 rmmod module_name
modprobe 智能加载/卸载(自动处理依赖) modprobe -r module_name(卸载)
lsmod 列出已加载模块 显示模块名、大小、被谁使用
modinfo 查看模块信息 modinfo module.ko
depmod 生成模块依赖文件 depmod -a
调试技巧:使用 dmesg 查看模块的 printk 输出。在开发阶段,建议将模块参数 perm 设为 0,避免 sysfs 暴露内部配置;调试完成后根据需要调整权限。

3.6 本章小结

内核模块是 Linux 驱动开发的基石。通过本章学习,你应掌握:

  • 模块的生命周期:加载 → 初始化 → 运行 → 卸载。
  • module_init() 和 module_exit() 的正确使用。
  • module_param() 系列宏实现灵活的运行时配置。
  • EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL 控制符号可见性。
  • 使用 modprobe/depmod 管理模块依赖。