Linux内核驱动开发实战:从字符设备到设备树 - 第2节
2.1 内核模块概念
Linux 内核模块(Kernel Module)是一段可以在运行时动态加载到内核或从内核卸载的代码片段。它允许在不重新编译整个内核、不重启系统的情况下扩展内核功能。模块机制是 Linux 内核可扩展性的核心基石。
核心思想:模块将内核从“静态大内核”转变为“动态可组合内核”。驱动、文件系统、网络协议等都可以作为模块独立开发、独立加载。
2.1.1 模块与静态编译的对比
| 对比维度 | 内核模块(.ko) | 静态编译(built-in) |
|---|---|---|
| 加载方式 | 运行时动态加载/卸载 | 内核启动时自动包含 |
| 灵活性 | 高,可按需加载,节省内存 | 低,所有功能固定在内核中 |
| 调试与开发 | 方便,可快速迭代测试 | 需重新编译内核并重启 |
| 启动速度 | 不影响启动时间(除非自动加载) | 启动时全部初始化,可能变慢 |
| 安全性 | 需签名验证机制防止恶意模块 | 代码固化在内核中 |
| 典型场景 | 设备驱动、文件系统、网络协议 | 核心调度、内存管理、必用驱动 |
2.1.2 模块的组成结构
一个标准的内核模块至少包含以下要素:
- 入口函数:模块加载时执行,通常用于注册设备、分配资源。
- 出口函数:模块卸载时执行,用于释放资源、注销设备。
- 许可证声明:声明模块使用的开源许可证,影响内核符号导出。
- 作者与描述信息:可选但推荐,便于维护和识别。
提示:模块运行在内核空间,拥有最高权限。一个错误的模块可能导致系统崩溃(Oops 或 Kernel Panic),开发时建议在虚拟机或开发板上测试。
2.2 模块加载与卸载
2.2.1 用户态工具
Linux 提供了一组用户态工具来管理内核模块:
| 命令 | 功能 | 常用选项 |
|---|---|---|
insmod |
加载指定模块文件 | 无依赖解析,需手动处理依赖 |
rmmod |
卸载指定模块 | -f 强制卸载(危险) |
modprobe |
智能加载/卸载,自动处理依赖 | -r 卸载,--show-depends 查看依赖 |
lsmod |
列出当前已加载的所有模块 | 显示模块大小、被引用次数 |
modinfo |
查看模块详细信息 | 显示作者、许可证、参数、依赖 |
2.2.2 加载与卸载流程
- 加载流程:用户执行
insmod hello.ko→ 内核调用init_module()系统调用 → 将模块二进制复制到内核空间 → 执行模块入口函数 → 模块状态变为“活动”。 - 卸载流程:用户执行
rmmod hello→ 内核调用delete_module()系统调用 → 检查模块引用计数是否为 0 → 执行模块出口函数 → 释放模块占用的内存。
注意:如果模块被其他模块依赖(引用计数 > 0),则无法卸载。使用
lsmod 查看 Used by 列可确认依赖关系。强制卸载可能导致内核崩溃。
2.3 module_init 与 module_exit 宏
module_init() 和 module_exit() 是内核提供的两个关键宏,用于指定模块的入口和出口函数。它们隐藏了内核模块注册的底层细节,是驱动开发者最常使用的接口。
2.3.1 宏定义与作用
| 宏 | 原型 | 作用 | 执行时机 |
|---|---|---|---|
module_init() |
module_init(x) |
将函数 x 标记为模块入口 | 模块加载时自动调用 |
module_exit() |
module_exit(x) |
将函数 x 标记为模块出口 | 模块卸载时自动调用 |
2.3.2 完整示例代码
以下是一个最简单的内核模块,展示了 module_init 和 module_exit 的用法:
#include <linux/module.h> // 所有模块都需要
#include <linux/kernel.h> // printk 等内核函数
#include <linux/init.h> // __init, __exit 宏
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Kernel Module!\n");
printk(KERN_INFO "Module loaded successfully.\n");
return 0; // 0 表示成功,负数表示失败
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel Module!\n");
printk(KERN_INFO "Module unloaded.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Driver Developer");
MODULE_DESCRIPTION("A simple Hello World kernel module");
MODULE_VERSION("1.0");
关键点:
__init标记:提示内核该函数仅在初始化阶段使用,执行后可释放内存。__exit标记:提示内核该函数仅在模块卸载时使用,如果模块静态编译则可忽略。- 入口函数返回
int:0 表示成功,负值(如-ENOMEM)表示失败,模块加载将中止。 - 出口函数返回
void:卸载操作不允许失败。
2.4 LICENSE 与作者信息声明
2.4.1 许可证声明
MODULE_LICENSE() 宏用于声明模块使用的开源许可证。内核根据许可证决定是否允许模块使用某些“仅 GPL 兼容”的内核符号。
| 许可证字符串 | 含义 | 是否兼容 GPL |
|---|---|---|
"GPL" |
GNU General Public License v2 | 是 |
"GPL v2" |
GPL 第二版 | 是 |
"GPL and additional rights" |
GPL 附加权限 | 是 |
"Dual BSD/GPL" |
双许可证 BSD 或 GPL | 是 |
"Dual MPL/GPL" |
双许可证 MPL 或 GPL | 是 |
"Proprietary" |
专有许可证 | 否 |
重要:如果模块声明为
"Proprietary",则无法调用使用 EXPORT_SYMBOL_GPL() 导出的内核符号。许多核心内核接口(如内存管理、文件系统操作)都标记为 GPL-only,专有模块将无法使用。
2.4.2 作者与描述信息
内核提供了多个信息声明宏,用于在模块二进制文件中嵌入元数据:
MODULE_AUTHOR("name <email>"):声明模块作者。MODULE_DESCRIPTION("description"):简要描述模块功能。MODULE_VERSION("version"):模块版本号。MODULE_ALIAS("alias"):为模块创建别名,便于自动加载。MODULE_DEVICE_TABLE(type, table):声明设备表,用于热插拔自动加载。
2.5 模块编译与 Makefile
内核模块的编译依赖内核源码树或内核头文件包。标准的 Makefile 模板如下:
# Makefile for hello kernel module
obj-m := hello.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
编译与测试步骤:
- 将
hello.c和Makefile放在同一目录。 - 执行
make编译,生成hello.ko。 - 执行
sudo insmod hello.ko加载模块。 - 执行
dmesg | tail查看内核日志输出。 - 执行
sudo rmmod hello卸载模块。 - 再次执行
dmesg | tail查看卸载日志。
2.6 本章小结
- 内核模块是 Linux 动态扩展机制的核心,允许运行时加载/卸载驱动和功能。
module_init()和module_exit()是模块的入口和出口,必须成对出现。MODULE_LICENSE()决定模块能否使用 GPL-only 内核符号,专有模块受限严重。- 作者、描述、版本等信息声明有助于模块管理和维护。
- 使用
insmod/rmmod/modprobe/lsmod/modinfo管理模块生命周期。 - Makefile 通过内核构建系统编译模块,
obj-m指定目标模块名。