2. Linux内核模块基础:模块概念、模块加载与卸载、模块参数、模块依赖、模块许可证
2.1 内核模块概念
Linux内核采用单内核+模块化架构。内核本身是一个完整的静态二进制文件,但通过内核模块(Kernel Module)机制,可以在运行时动态地向内核添加或移除功能,无需重新编译整个内核或重启系统。
内核模块本质上是一段目标代码(.ko 文件),它被设计为在内核空间运行,可以访问内核的符号、函数和数据结构。常见的模块包括设备驱动、文件系统、网络协议、系统调用扩展等。
核心优势:动态扩展、按需加载、减小内核体积、便于开发和调试。
类比理解:内核就像操作系统的主板,模块就像可插拔的扩展卡(显卡、网卡)。需要时插入(insmod),不需要时拔出(rmmod),主板本身始终运行。
2.2 模块加载与卸载
内核模块的生命周期由一组用户空间工具和内核系统调用共同管理。核心操作包括加载、查看、卸载和查询模块信息。
2.2.1 常用命令
| 命令 | 功能 | 典型用法 |
|---|---|---|
| insmod | 加载模块(不自动解析依赖) | insmod hello.ko |
| rmmod | 卸载模块 | rmmod hello |
| modprobe | 智能加载/卸载(自动处理依赖) | modprobe hello |
| lsmod | 列出当前已加载的所有模块 | lsmod |
| modinfo | 查看模块详细信息 | modinfo hello.ko |
2.2.2 模块的入口与出口
每个内核模块必须定义两个宏:module_init() 和 module_exit(),分别指定模块加载和卸载时调用的函数。
#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 Linux Expert");
MODULE_DESCRIPTION("A simple hello world module");
注意:
__init 和 __exit 标记告诉内核:初始化函数在完成后可以释放内存;出口函数在模块被内置(非模块化)时可以忽略。这有助于优化内存占用。
2.3 模块参数
内核模块支持在加载时传递参数,使得同一份模块代码可以适应不同的硬件配置或运行模式。参数通过 module_param() 宏声明。
2.3.1 参数声明语法
#include <linux/moduleparam.h>
static int irq = 10;
static char *name = "default_dev";
static int debug = 0;
module_param(irq, int, 0644);
module_param(name, charp, 0644);
module_param(debug, int, 0644);
MODULE_PARM_DESC(irq, "IRQ number for the device");
MODULE_PARM_DESC(name, "Device name string");
MODULE_PARM_DESC(debug, "Enable debug output (0/1)");
2.3.2 参数传递方式
| 方式 | 示例 | 说明 |
|---|---|---|
| insmod 传参 | insmod mydrv.ko irq=33 name="eth1" debug=1 |
直接跟在 .ko 文件后,键值对形式 |
| modprobe 传参 | modprobe mydrv irq=33 name="eth1" |
自动处理依赖,参数语法相同 |
| 配置文件 | /etc/modprobe.d/mydrv.conf |
写入 options mydrv irq=33,系统启动时自动应用 |
参数权限位:
module_param 的第三个参数是 sysfs 文件权限(如 0644)。设为 0 表示不在 sysfs 中暴露该参数;设为非 0 值(如 0644)则可在运行时通过 /sys/module/<模块名>/parameters/ 查看或修改。
2.4 模块依赖
模块之间可能存在符号依赖关系:模块 A 使用了模块 B 导出的函数或变量。内核通过 符号导出(EXPORT_SYMBOL) 和 模块间依赖跟踪 来管理这种关系。
2.4.1 符号导出
// 模块 B:导出符号
#include <linux/module.h>
int my_add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(my_add);
// 模块 A:使用导出的符号
extern int my_add(int a, int b);
static int __init a_init(void)
{
int result = my_add(3, 5);
printk("Result: %d\n", result);
return 0;
}
2.4.2 依赖管理
| 工具/机制 | 作用 |
|---|---|
| depmod | 扫描所有模块,生成 modules.dep 依赖关系文件 |
| modprobe | 读取 modules.dep,自动加载所有依赖模块 |
| lsmod | 显示模块间的依赖关系(Used by 列) |
| modinfo - depends | 查看模块的直接依赖列表 |
依赖加载顺序:使用
insmod 加载模块 A 时,如果 A 依赖 B,必须先手动加载 B。而 modprobe 会自动先加载 B 再加载 A。卸载时顺序相反:先卸载 A,再卸载 B。
2.5 模块许可证
Linux 内核使用 GNU General Public License v2 (GPLv2) 作为许可证。内核模块必须声明其许可证,以表明模块是否符合内核的许可要求。
2.5.1 许可证声明
MODULE_LICENSE("GPL"); // 最常用,完全兼容 GPL
MODULE_LICENSE("GPL v2"); // 明确 GPL v2 版本
MODULE_LICENSE("GPL and additional rights"); // GPL + 额外权利
MODULE_LICENSE("Dual BSD/GPL"); // 双许可证
MODULE_LICENSE("Dual MPL/GPL"); // 双许可证
MODULE_LICENSE("Proprietary"); // 专有/闭源模块
2.5.2 许可证对符号可见性的影响
内核使用 EXPORT_SYMBOL_GPL() 宏导出的符号,只有声明为 GPL 兼容许可证的模块才能使用。这保护了内核核心代码不被闭源模块直接调用。
| 导出宏 | 可用模块 | 典型用途 |
|---|---|---|
| EXPORT_SYMBOL() | 所有模块(包括 Proprietary) | 通用接口,如内存分配、打印函数 |
| EXPORT_SYMBOL_GPL() | 仅 GPL 兼容模块 | 核心内部接口,如文件系统、调度器 |
最佳实践:嵌入式 Linux 驱动开发中,强烈建议使用
MODULE_LICENSE("GPL")。这不仅符合内核社区规范,还能确保模块可以访问所有内核符号,避免因许可证不兼容导致的链接失败或内核污染警告。
2.6 本章小结
- 内核模块是 Linux 动态扩展机制的核心,允许在不重启系统的情况下添加驱动和功能。
- 加载与卸载通过
insmod/rmmod/modprobe管理,模块必须实现module_init()和module_exit()。 - 模块参数使用
module_param()声明,支持在加载时或通过 sysfs 动态配置。 - 模块依赖通过
EXPORT_SYMBOL和depmod管理,modprobe自动处理依赖顺序。 - 模块许可证必须使用
MODULE_LICENSE()声明,GPL 兼容模块才能使用EXPORT_SYMBOL_GPL导出的符号。