2. 内核模块基础:模块的加载与卸载、module_init/module_exit宏、模块许可证与作者信息、模块参数传递
Linux 内核模块是设备驱动开发的核心载体。与用户态程序不同,内核模块可以在运行时动态加载到内核空间,无需重新编译整个内核或重启系统。本章将系统讲解内核模块的基本结构、生命周期管理、元信息声明以及参数传递机制,为后续中断处理和 DMA 传输打下坚实基础。
module_init() 和 module_exit() 宏注册入口和出口函数,由内核在模块加载/卸载时自动调用。
2.1 模块的加载与卸载
内核模块的生命周期由两个关键操作控制:加载(insmod)和卸载(rmmod)。加载时内核执行模块的初始化函数,完成资源申请、设备注册等准备工作;卸载时执行清理函数,释放已占用的资源。
下图展示了内核模块从加载到卸载的完整生命周期流程:
模块加载与卸载的常用命令如下表所示:
| 命令 | 功能 | 示例 |
|---|---|---|
insmod |
加载内核模块(需指定完整路径) | insmod ./hello.ko |
rmmod |
卸载内核模块 | rmmod hello |
modprobe |
智能加载模块(自动处理依赖) | modprobe hello |
lsmod |
列出当前已加载的所有模块 | lsmod | grep hello |
modinfo |
查看模块的详细信息 | modinfo hello.ko |
insmod 加载模块时,必须提供模块文件的绝对路径或相对路径。而 rmmod 只需指定模块名称(不含 .ko 后缀)。如果模块正在被其他模块或进程使用,卸载操作会失败,内核会返回 "Module is in use" 错误。
2.2 module_init / module_exit 宏
module_init() 和 module_exit() 是 Linux 内核提供的两个关键宏,用于向内核注册模块的入口和出口函数。这两个宏的定义位于 <linux/module.h> 头文件中。
下面是一个完整的最小内核模块示例,展示了这两个宏的标准用法:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Kernel Module Loaded!\n");
return 0; // 返回0表示初始化成功
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel Module Unloaded!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Expert <expert@example.com>");
MODULE_DESCRIPTION("A simple Hello World kernel module");
MODULE_VERSION("1.0");
关键要点说明:
- __init 和 __exit 标记:
__init告诉内核该函数仅在初始化阶段使用,初始化完成后可以释放占用的内存;__exit标记的函数在模块卸载时调用,如果模块被编译进内核(非模块方式),该函数会被忽略。 - 返回值约定:初始化函数返回 0 表示成功,返回负数(如 -ENOMEM、-EIO)表示失败,内核会根据返回值决定是否保留模块。
- printk 日志级别:
KERN_INFO是日志级别宏,用于控制消息的输出优先级,可通过dmesg命令查看。
下图展示了 module_init 和 module_exit 在内核中的注册与回调机制:
2.3 模块许可证与作者信息
Linux 内核要求所有模块声明其许可证信息,这是为了遵守 GNU GPL 协议。内核通过许可证信息决定是否允许模块使用某些 GPL-only 的符号(函数和变量)。
常用的模块元信息宏如下表所示:
| 宏 | 用途 | 示例值 |
|---|---|---|
MODULE_LICENSE() |
声明模块许可证 | "GPL", "GPL v2", "BSD", "Proprietary" |
MODULE_AUTHOR() |
声明模块作者 | "John Doe <john@example.com>" |
MODULE_DESCRIPTION() |
模块功能描述 | "A sample device driver" |
MODULE_VERSION() |
模块版本号 | "1.0", "2.1.3" |
MODULE_ALIAS() |
为模块创建别名 | "char-major-10-30" |
MODULE_DEVICE_TABLE() |
声明设备 ID 表 | USB、PCI 设备表 |
EXPORT_SYMBOL_GPL() 导出的符号。这会影响对某些核心内核 API 的访问,例如部分内存管理函数和调试接口。
使用 modinfo 命令可以查看已编译模块的元信息:
$ modinfo hello.ko
filename: /home/user/hello.ko
license: GPL
author: Embedded Expert <expert@example.com>
description: A simple Hello World kernel module
version: 1.0
depends:
retpoline: Y
name: hello
vermagic: 5.15.0-generic SMP mod_unload modversions
2.4 模块参数传递
内核模块支持在加载时通过命令行参数传递配置值,这使得模块具有更好的灵活性和可配置性。模块参数使用 module_param() 和 module_param_array() 宏声明。
参数声明的基本语法如下:
#include <linux/moduleparam.h>
// 声明一个整型参数
static int irq_num = 10;
module_param(irq_num, int, 0644);
MODULE_PARM_DESC(irq_num, "IRQ number for the device");
// 声明一个字符串参数
static char *device_name = "my_device";
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(device_name, "Device name string");
// 声明一个整型数组参数
static int dma_channels[4] = {0, 1, 2, 3};
static int num_channels = 4;
module_param_array(dma_channels, int, &num_channels, 0644);
MODULE_PARM_DESC(dma_channels, "DMA channel numbers array");
参数类型与对应的加载方式如下表所示:
| C 类型 | module_param 类型标识 | 加载示例 |
|---|---|---|
int |
int |
insmod hello.ko irq_num=17 |
long |
long |
insmod hello.ko base_addr=0x1000 |
char * |
charp |
insmod hello.ko device_name="uart0" |
int[] |
int (配合 module_param_array) |
insmod hello.ko dma_channels=1,2,3 |
bool |
bool |
insmod hello.ko debug=1 |
参数权限位(上述示例中的 0644)控制参数在 sysfs 文件系统中的可见性和可写性:
- 0:参数完全不在 sysfs 中暴露,用户空间不可见。
- 0444:参数在 sysfs 中只读可见。
- 0644:参数在 sysfs 中可读,root 用户可写(运行时修改)。
下图展示了模块参数从用户空间传递到内核模块的完整路径:
MODULE_PARM_DESC 描述,方便其他开发者通过 modinfo 了解参数含义。
2.5 完整示例:带参数的可配置模块
下面是一个综合示例,将本章所学的所有知识点整合在一起:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
// 模块参数
static int irq = 10;
static char *name = "default_dev";
static int buffer_size = 1024;
module_param(irq, int, 0644);
module_param(name, charp, 0644);
module_param(buffer_size, int, 0444);
MODULE_PARM_DESC(irq, "IRQ number (default: 10)");
MODULE_PARM_DESC(name, "Device name string (default: default_dev)");
MODULE_PARM_DESC(buffer_size, "DMA buffer size in bytes (default: 1024)");
static int __init configurable_init(void)
{
printk(KERN_INFO "Configurable Module Loaded\n");
printk(KERN_INFO " IRQ: %d\n", irq);
printk(KERN_INFO " Device: %s\n", name);
printk(KERN_INFO " Buffer: %d bytes\n", buffer_size);
return 0;
}
static void __exit configurable_exit(void)
{
printk(KERN_INFO "Configurable Module Unloaded\n");
}
module_init(configurable_init);
module_exit(configurable_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Expert <expert@example.com>");
MODULE_DESCRIPTION("Configurable module with parameters");
MODULE_VERSION("2.0");
加载测试:
# 使用默认参数加载
sudo insmod configurable.ko
# 使用自定义参数加载
sudo insmod configurable.ko irq=33 name="spi_dev" buffer_size=4096
# 查看参数值
cat /sys/module/configurable/parameters/irq
cat /sys/module/configurable/parameters/name
# 运行时修改参数(需 root 权限)
echo 44 | sudo tee /sys/module/configurable/parameters/irq