内核模块开发:platform驱动与ioctl接口实战 - 第1节
1.1 内核模块概念
Linux 内核模块(Kernel Module)是一段可以在运行时动态加载到内核或从内核卸载的代码,无需重新编译整个内核或重启系统。模块机制是 Linux 内核可扩展性的核心设计之一,它允许开发者将设备驱动、文件系统、网络协议等内核功能以独立单元的形式开发和维护。
模块 vs 静态编译
| 对比维度 | 内核模块(动态) | 静态编译(内置) |
|---|---|---|
| 加载方式 | 运行时通过 insmod / modprobe 加载 |
内核启动时自动初始化 |
| 卸载方式 | 运行时通过 rmmod 卸载 |
无法卸载,始终驻留内存 |
| 内核体积 | 基础内核较小,按需加载 | 内核镜像较大,包含所有功能 |
| 调试迭代 | 快速修改、编译、加载测试 | 需重新编译内核并重启系统 |
| 适用场景 | 设备驱动、文件系统、协议栈扩展 | 核心调度、内存管理、必须启动项 |
关键认知:模块机制使得驱动开发与内核主线解耦。在嵌入式系统中,平台驱动(Platform Driver)几乎都以模块形式存在,便于跨内核版本维护和现场升级。
1.2 模块生命周期
每个内核模块都遵循严格的生命周期阶段,由内核模块加载器(kmod)和模块引用计数协同管理。
生命周期四阶段
- 加载(Loading):用户态调用
insmod或modprobe,内核执行模块初始化函数module_init()注册的入口。 - 运行(Running):模块驻留内核空间,提供服务(如设备文件操作、中断处理、sysfs 接口)。
- 卸载准备(Quiescing):用户态调用
rmmod,内核检查模块引用计数是否为 0,若为 0 则调用模块退出函数module_exit()。 - 卸载(Unloading):模块退出函数执行清理(释放内存、注销设备、销毁接口),模块从内核地址空间移除。
常见陷阱:模块卸载时如果引用计数不为 0,
rmmod 会返回 EBUSY。在平台驱动中,如果设备文件被进程打开(open() 未 close()),引用计数将阻止卸载,这是内核保护机制。
1.3 Hello World 模块编写
我们从一个最简内核模块开始,它仅在内核日志中打印 "Hello, World!" 和 "Goodbye, World!"。这个模板是所有平台驱动模块的起点。
源代码:hello.c
#include <linux/module.h> /* 所有模块都需要 */
#include <linux/kernel.h> /* printk 和 KERN_INFO */
#include <linux/init.h> /* __init 和 __exit 宏 */
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, World! (Module loaded)\n");
return 0; /* 返回 0 表示初始化成功 */
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, World! (Module unloaded)\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Driver Course");
MODULE_DESCRIPTION("A simple Hello World kernel module");
MODULE_VERSION("1.0");
代码结构解析
| 元素 | 说明 |
|---|---|
module_init() |
注册模块加载时调用的初始化函数,必须返回 int,0 表示成功,负数表示失败。 |
module_exit() |
注册模块卸载时调用的清理函数,无返回值。 |
__init / __exit |
内核宏,将函数放入特殊段,初始化完成后可释放内存(仅对静态编译或内置模块有效)。 |
printk() |
内核空间的打印函数,KERN_INFO 指定日志级别,输出可通过 dmesg 查看。 |
MODULE_* 宏 |
模块元信息描述,通过 modinfo 命令可查看。 |
1.4 模块编译:Makefile 与 Kbuild
内核模块的编译依赖内核源码树(或内核头文件包)提供的 Kbuild 系统。以下 Makefile 适用于 2.6 及以后的所有内核版本。
Makefile
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
编译与测试步骤
- 编译模块:在源码目录执行
make,生成hello.ko(内核对象文件)。 - 加载模块:
sudo insmod hello.ko,无输出表示成功。 - 查看日志:
dmesg | tail,应看到 "Hello, World! (Module loaded)"。 - 查看模块信息:
modinfo hello.ko,显示作者、描述、许可证等。 - 卸载模块:
sudo rmmod hello,再次dmesg | tail看到 "Goodbye, World!"。
开发效率提示:在嵌入式开发中,建议在 Makefile 中增加
ARCH 和 CROSS_COMPILE 变量,以支持交叉编译。例如:ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-。这是后续 platform 驱动移植的基础配置。
1.5 模块元信息与许可证
内核模块必须声明许可证,否则内核会认为模块是 "tainted"(污染)状态。GPL 许可证允许模块使用内核导出的 GPL-only 符号,这是编写平台驱动时的常见要求。
常用 MODULE_* 宏
| 宏 | 用途 | 示例 |
|---|---|---|
MODULE_LICENSE() |
声明许可证,影响符号可用性 | "GPL", "GPL v2", "Proprietary" |
MODULE_AUTHOR() |
作者信息 | "John Doe <john@example.com>" |
MODULE_DESCRIPTION() |
模块功能简述 | "Platform driver for XYZ sensor" |
MODULE_VERSION() |
版本号,用于热升级判断 | "1.2.3" |
MODULE_ALIAS() |
为模块提供别名,便于 modprobe 自动加载 | "platform:my-device" |
平台驱动关联:在后续章节中,每个 platform 驱动模块都必须包含
MODULE_ALIAS("platform:驱动名称"),以便内核设备模型在匹配设备时自动加载对应模块。
1.6 本章小结
- 内核模块是 Linux 驱动开发的基本单元,支持动态加载/卸载,无需重启系统。
- 模块生命周期包括加载、运行、卸载准备、卸载四个阶段,引用计数是卸载的安全锁。
- Hello World 模块展示了
module_init/module_exit的基本框架,所有平台驱动都基于此模板扩展。 - 编译使用内核 Kbuild 系统,Makefile 通过
-C指向内核源码树。 - 元信息宏(
MODULE_LICENSE等)是模块的"身份证",GPL 声明对使用内核核心符号至关重要。