Linux内核GPIO驱动开发:从原理到实战应用 - 第3节

3.1 内核模块的加载与卸载

Linux 内核模块是内核在运行时可以动态加载和卸载的代码片段,无需重新编译整个内核或重启系统。GPIO 驱动通常以内核模块的形式存在,理解模块的加载与卸载机制是开发 GPIO 驱动的第一步。

3.1.1 模块生命周期

一个内核模块从被加载到最终卸载,经历以下四个阶段:

  1. 加载阶段:通过 insmod 或 modprobe 将 .ko 文件插入内核空间。
  2. 初始化阶段:内核调用模块的初始化函数(由 module_init 指定),完成资源申请、设备注册等操作。
  3. 运行阶段:模块在内核空间常驻,响应系统调用、中断或文件操作。
  4. 卸载阶段:通过 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 初始化与清理的执行流程

module_init / module_exit 执行流程 insmod gpio_demo.ko 内核加载 .ko 到内核空间 扫描 .init.text 段 调用 gpio_demo_init() 返回 0 → 加载成功 rmmod gpio_demo 调用 gpio_demo_exit() 关键说明 初始化返回非0值时 模块加载失败 内核自动清理已分配资源

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 实现运行时配置,提高驱动的灵活性和可移植性。