Linux内核GPIO驱动开发:从原理到实战应用 - 第3节
3.1 内核模块的加载与卸载
Linux 内核模块是内核在运行时可以动态加载和卸载的代码片段,无需重新编译整个内核或重启系统。GPIO 驱动通常以内核模块的形式存在,理解模块的加载与卸载机制是开发 GPIO 驱动的第一步。
3.1.1 模块生命周期
一个内核模块从被加载到最终卸载,经历以下四个阶段:
- 加载阶段:通过
insmod或modprobe将 .ko 文件插入内核空间。 - 初始化阶段:内核调用模块的初始化函数(由
module_init指定),完成资源申请、设备注册等操作。 - 运行阶段:模块在内核空间常驻,响应系统调用、中断或文件操作。
- 卸载阶段:通过
rmmod移除模块,内核调用清理函数(由module_exit指定),释放已占用的资源。
关键概念: 模块加载后运行在内核空间,拥有最高特权级(Ring 0)。任何未正确释放资源的模块卸载都可能导致内核崩溃或资源泄漏。
3.1.2 加载与卸载命令
| 命令 | 功能 | 依赖解析 | 常用选项 |
|---|---|---|---|
insmod |
直接加载指定 .ko 文件 | 不解析依赖 | -f 强制加载 |
modprobe |
智能加载模块及其依赖 | 自动解析 | -r 卸载,--show-depends 查看依赖 |
rmmod |
卸载指定模块 | 不检查依赖 | -f 强制卸载 |
modinfo |
查看模块信息 | — | -p 查看参数 |
开发建议: 调试阶段优先使用
insmod 和 rmmod,避免 modprobe 自动加载不必要的依赖模块。生产环境则推荐 modprobe 以确保依赖完整。
3.2 module_init / module_exit 机制
module_init 和 module_exit 是内核模块的两个核心宏,它们告诉内核模块的入口和出口在哪里。这两个宏通过 __init 和 __exit 属性标记,在编译链接阶段被放入特定的 ELF section 中。
3.2.1 宏定义与工作原理
在 <linux/module.h> 中,这两个宏的定义如下(简化版):
/* 模块初始化宏 */
#define module_init(x) __initcall(x);
/* 模块清理宏 */
#define module_exit(x) __exitcall(x);
内核在加载模块时,会扫描 .init.text 段找到初始化函数指针并调用;卸载时则扫描 .exit.text 段。初始化函数执行完毕后,__init 标记的代码占用的内存可以被回收,这是内核节省内存的一种手段。
注意: 如果模块被编译进内核(built-in),
module_exit 指定的函数不会被链接,因为 built-in 模块不允许卸载。此时 __exit 函数会被编译器丢弃。
3.2.2 标准模块模板
以下是一个最简的内核模块模板,展示了 module_init 和 module_exit 的标准用法:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init gpio_demo_init(void)
{
printk(KERN_INFO "GPIO Demo Module Loaded\n");
/* 在此处注册设备、申请GPIO、分配内存等 */
return 0;
}
static void __exit gpio_demo_exit(void)
{
printk(KERN_INFO "GPIO Demo Module Unloaded\n");
/* 在此处释放GPIO、注销设备、释放内存等 */
}
module_init(gpio_demo_init);
module_exit(gpio_demo_exit);
3.2.3 初始化与清理的执行流程
3.3 MODULE_LICENSE / MODULE_AUTHOR 宏
这些宏用于描述模块的元信息,它们不产生执行代码,而是将信息写入模块的 .modinfo 段。用户可以通过 modinfo 命令查看这些信息,内核也会根据 LICENSE 决定模块的行为。
3.3.1 宏定义与参数
| 宏 | 作用 | 参数示例 | 是否必需 |
|---|---|---|---|
MODULE_LICENSE |
声明模块许可证 | "GPL", "GPL v2", "Dual BSD/GPL", "Proprietary" |
是 |
MODULE_AUTHOR |
声明模块作者 | "Zhang San <zhangsan@example.com>" |
推荐 |
MODULE_DESCRIPTION |
模块功能描述 | "GPIO LED Driver for i.MX6UL" |
推荐 |
MODULE_VERSION |
模块版本号 | "1.0" |
推荐 |
MODULE_ALIAS |
模块别名 | "platform:gpio-led" |
可选 |
3.3.2 LICENSE 对内核行为的影响
内核根据 MODULE_LICENSE 的值决定是否允许模块使用某些 GPL-only 符号。以下表格列出了常见许可证的影响:
| 许可证字符串 | 是否 GPL 兼容 | 可使用 GPL-only 符号 | 典型场景 |
|---|---|---|---|
"GPL" |
是 | 是 | 开源内核驱动 |
"GPL v2" |
是 | 是 | 遵循 GPL v2 的驱动 |
"Dual BSD/GPL" |
是 | 是 | 双许可证项目 |
"Proprietary" |
否 | 否 | 闭源商业驱动 |
重要: 如果模块声明为
"Proprietary" 却试图调用 EXPORT_SYMBOL_GPL 导出的符号,内核会在加载时打印警告并拒绝链接。GPIO 子系统中许多核心函数(如 gpio_request)都是 GPL-only 符号,因此 GPIO 驱动必须声明为 GPL 兼容许可证。
3.3.3 完整模块元信息示例
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init gpio_demo_init(void)
{
printk(KERN_INFO "GPIO Demo: Module initialized\n");
return 0;
}
static void __exit gpio_demo_exit(void)
{
printk(KERN_INFO "GPIO Demo: Module exited\n");
}
module_init(gpio_demo_init);
module_exit(gpio_demo_exit);
/* 模块元信息 */
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Li Si <lisi@example.com>");
MODULE_DESCRIPTION("GPIO Demo Driver - Learning Module Basics");
MODULE_VERSION("1.0.0");
MODULE_ALIAS("gpio-demo");
3.3.4 查看模块信息
编译生成 gpio_demo.ko 后,使用 modinfo 命令查看:
$ modinfo gpio_demo.ko
filename: gpio_demo.ko
license: GPL v2
author: Li Si <lisi@example.com>
description: GPIO Demo Driver - Learning Module Basics
version: 1.0.0
alias: gpio-demo
depends:
name: gpio_demo
vermagic: 5.15.0-generic SMP mod_unload modversions
调试技巧: 如果模块加载失败,首先用
modinfo 检查 vermagic 是否与当前运行内核版本匹配。版本不匹配是模块加载失败最常见的原因之一。
3.4 模块参数传递
内核模块支持在加载时通过命令行传递参数,这为 GPIO 驱动提供了灵活的配置能力,例如指定 GPIO 编号、中断触发方式等。
3.4.1 参数声明宏
| 宏 | 数据类型 | 示例 |
|---|---|---|
module_param |
基本类型(int, long, charp 等) | module_param(gpio_num, int, 0644); |
module_param_array |
数组类型 | module_param_array(gpio_pins, int, &count, 0644); |
module_param_string |
字符串类型 | module_param_string(dev_name, buf, sizeof(buf), 0); |
3.4.2 带参数的 GPIO 模块示例
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
/* 模块参数:默认使用 GPIO 12 */
static int gpio_num = 12;
static char *gpio_label = "gpio_demo_led";
/* 声明参数,第三个参数是文件权限(/sys/module/.../parameters/ 下可见) */
module_param(gpio_num, int, 0644);
module_param(gpio_label, charp, 0644);
/* 描述参数 */
MODULE_PARM_DESC(gpio_num, "GPIO pin number to control");
MODULE_PARM_DESC(gpio_label, "Label for the GPIO");
static int __init gpio_param_init(void)
{
printk(KERN_INFO "GPIO Param Demo: pin=%d, label=%s\n",
gpio_num, gpio_label);
/* 后续章节将在此处调用 gpio_request() */
return 0;
}
static void __exit gpio_param_exit(void)
{
printk(KERN_INFO "GPIO Param Demo: unloaded\n");
}
module_init(gpio_param_init);
module_exit(gpio_param_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Wang Wu <wangwu@example.com>");
MODULE_DESCRIPTION("GPIO Module with Parameters");
MODULE_VERSION("1.0.0");
3.4.3 加载时传递参数
# 加载时指定 GPIO 编号和标签
$ sudo insmod gpio_param.ko gpio_num=17 gpio_label="my_led"
# 查看内核日志确认参数生效
$ dmesg | tail -1
[ 1234.567890] GPIO Param Demo: pin=17, label=my_led
# 运行时查看/修改参数(如果权限允许)
$ cat /sys/module/gpio_param/parameters/gpio_num
17
$ echo 23 | sudo tee /sys/module/gpio_param/parameters/gpio_num
23
GPIO 驱动实践: 将 GPIO 编号设计为模块参数,可以在不重新编译的情况下适配不同硬件平台。例如,同一份驱动代码在开发板 A 上使用 GPIO 12,在开发板 B 上使用 GPIO 17,只需在加载时传入不同参数即可。
3.5 本章小结
- 模块加载与卸载:
insmod/rmmod用于调试,modprobe用于生产环境。 - module_init / module_exit:定义模块的入口和出口函数,初始化函数执行后
__init内存可回收。 - MODULE_LICENSE:GPIO 驱动必须声明为 GPL 兼容,否则无法调用
gpio_request等核心 API。 - 模块参数:通过
module_param实现运行时配置,提高驱动的灵活性和可移植性。