内核模块开发 platform驱动与ioctl接口实战 - 第1节

1. Linux内核模块基础

Linux 内核模块(Kernel Module)是一段可以在运行时动态加载到内核或从内核卸载的代码,无需重新编译整个内核或重启系统。模块机制是 Linux 内核可扩展性的核心设计之一,它允许开发者将设备驱动、文件系统、网络协议等内核功能以独立单元的形式开发和维护。

模块 vs 静态编译

对比维度 内核模块(动态) 静态编译(内置)
加载方式 运行时通过 insmod / modprobe 加载 内核启动时自动初始化
卸载方式 运行时通过 rmmod 卸载 无法卸载,始终驻留内存
内核体积 基础内核较小,按需加载 内核镜像较大,包含所有功能
调试迭代 快速修改、编译、加载测试 需重新编译内核并重启系统
适用场景 设备驱动、文件系统、协议栈扩展 核心调度、内存管理、必须启动项
关键认知:模块机制使得驱动开发与内核主线解耦。在嵌入式系统中,平台驱动(Platform Driver)几乎都以模块形式存在,便于跨内核版本维护和现场升级。

1.2 模块生命周期

每个内核模块都遵循严格的生命周期阶段,由内核模块加载器(kmod)和模块引用计数协同管理。

生命周期四阶段

  1. 加载(Loading):用户态调用 insmod 或 modprobe,内核执行模块初始化函数 module_init() 注册的入口。
  2. 运行(Running):模块驻留内核空间,提供服务(如设备文件操作、中断处理、sysfs 接口)。
  3. 卸载准备(Quiescing):用户态调用 rmmod,内核检查模块引用计数是否为 0,若为 0 则调用模块退出函数 module_exit()。
  4. 卸载(Unloading):模块退出函数执行清理(释放内存、注销设备、销毁接口),模块从内核地址空间移除。
内核模块生命周期流程图 加载 insmod / modprobe 运行 提供服务 卸载准备 引用计数检查 卸载 rmmod 引用计数 > 0 时禁止卸载 try_module_get / module_put
常见陷阱:模块卸载时如果引用计数不为 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

编译与测试步骤

  1. 编译模块:在源码目录执行 make,生成 hello.ko(内核对象文件)。
  2. 加载模块:sudo insmod hello.ko,无输出表示成功。
  3. 查看日志:dmesg | tail,应看到 "Hello, World! (Module loaded)"。
  4. 查看模块信息:modinfo hello.ko,显示作者、描述、许可证等。
  5. 卸载模块:sudo rmmod hello,再次 dmesg | tail 看到 "Goodbye, World!"。
Hello World 模块编译与运行流程 hello.c Makefile make hello.ko sudo insmod dmesg 查看
开发效率提示:在嵌入式开发中,建议在 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 声明对使用内核核心符号至关重要。