3、第一个字符设备驱动:模块初始化与退出函数、module_init/module_exit宏、LICENSE与作者信息

在嵌入式Linux驱动开发中,第一个字符设备驱动通常以内核模块的形式存在。内核模块允许我们在不重新编译整个内核的情况下,动态地向内核添加功能。本章将聚焦于构建一个最简字符设备驱动的骨架,重点讲解模块的初始化与退出机制、关键宏的使用以及模块信息的声明。

3.1 内核模块的基本结构

一个完整的内核模块至少包含两个核心函数:模块初始化函数和模块退出函数。这两个函数通过特定的宏注册到内核框架中,由内核在模块加载和卸载时自动调用。

核心概念:模块初始化函数在模块被加载(insmod)时执行,负责资源的申请和注册;模块退出函数在模块被卸载(rmmod)时执行,负责资源的释放和注销。

3.1.1 模块初始化函数

初始化函数是模块的入口点。它必须返回一个整型值:返回 0 表示初始化成功;返回负数(通常为 -ENOMEM、-EBUSY 等错误码)表示初始化失败,内核将中止模块加载。

static int __init my_driver_init(void)
{
    printk(KERN_INFO "My first character driver initialized.\n");
    // 在此处注册字符设备、分配内存、初始化硬件等
    return 0;
}

函数声明中的 __init 标记是一个内核宏,它告诉内核:该函数仅在初始化阶段使用。一旦模块加载完成,内核可以回收该函数占用的内存,以节省系统资源。

3.1.2 模块退出函数

退出函数在模块卸载时被调用,负责清理模块占用的所有资源。它没有返回值。

static void __exit my_driver_exit(void)
{
    printk(KERN_INFO "My first character driver exited.\n");
    // 在此处注销字符设备、释放内存、关闭硬件等
}

函数声明中的 __exit 标记与 __init 类似,表示该函数仅在模块卸载时使用。如果模块被编译进内核(而非动态加载),__exit 标记的函数将被忽略,因为内核不会卸载自身。

3.2 module_init 与 module_exit 宏

仅仅定义初始化函数和退出函数是不够的,内核并不知道哪个函数是初始化函数、哪个是退出函数。我们需要使用 module_init() 和 module_exit() 宏来向内核注册这两个入口点。

宏名称 作用 参数 调用时机
module_init() 指定模块的初始化函数 初始化函数名 执行 insmod 时
module_exit() 指定模块的退出函数 退出函数名 执行 rmmod 时
module_init(my_driver_init);
module_exit(my_driver_exit);

这两个宏通常放在源文件的末尾。它们会被内核的构建系统展开为特定的链接器指令,将函数地址放入内核的 .initcall 和 .exitcall 段中。

最佳实践:始终将 module_init() 和 module_exit() 成对出现。如果只注册了初始化函数而没有注册退出函数,模块将无法被正常卸载,可能导致系统资源泄漏。

3.3 模块信息:LICENSE 与作者信息

Linux内核要求所有模块声明其许可证信息,这是为了遵守GPL协议。此外,声明作者和描述信息有助于模块的管理和维护。

3.3.1 MODULE_LICENSE

MODULE_LICENSE() 宏用于声明模块的许可证。内核根据许可证信息决定是否允许模块使用某些GPL-only的符号(函数或变量)。

许可证字符串 含义 能否使用GPL-only符号
"GPL" GNU General Public License v2 或更高版本 是
"GPL v2" GNU General Public License v2 是
"Dual BSD/GPL" BSD 或 GPL 双许可证 是
"Proprietary" 专有许可证 否
MODULE_LICENSE("GPL");

3.3.2 MODULE_AUTHOR 与 MODULE_DESCRIPTION

这两个宏用于提供模块的元数据,方便用户通过 modinfo 命令查看模块信息。

MODULE_AUTHOR("Your Name <your.email@example.com>");
MODULE_DESCRIPTION("A simple example character device driver");

3.4 完整的最小模块示例

将以上所有元素组合在一起,就构成了一个完整的、可编译的最小字符设备驱动模块。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init my_driver_init(void)
{
    printk(KERN_INFO "Hello, my first character driver!\n");
    return 0;
}

static void __exit my_driver_exit(void)
{
    printk(KERN_INFO "Goodbye, my first character driver!\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Dev <dev@example.com>");
MODULE_DESCRIPTION("My first character device driver module");

3.5 模块加载与卸载流程

下面的SVG图展示了从用户空间执行 insmod 和 rmmod 命令到内核模块初始化/退出函数被调用的完整流程。

模块加载与卸载流程 用户空间 insmod rmmod 内核空间 系统调用层 模块加载器 module_init() module_exit() 流程说明: 1. 用户执行 insmod 命令,触发系统调用进入内核空间。 2. 内核模块加载器解析模块文件,定位 module_init() 指定的函数地址。 3. 调用初始化函数,执行驱动注册逻辑。若返回非0,则加载失败。 4. 用户执行 rmmod 命令,内核调用 module_exit() 指定的退出函数进行清理。

3.6 关键点总结

  • 模块入口:使用 module_init() 和 module_exit() 宏注册初始化与退出函数,这是模块与内核交互的契约。
  • 函数标记:__init 和 __exit 标记帮助内核优化内存使用,初始化完成后可回收相关代码段。
  • 许可证声明:MODULE_LICENSE() 是强制要求,缺失或声明为 "Proprietary" 将导致模块无法使用GPL-only的内核API。
  • 模块元数据:MODULE_AUTHOR() 和 MODULE_DESCRIPTION() 虽然不是强制要求,但良好的文档习惯有助于团队协作和问题排查。
  • 返回值检查:初始化函数返回非零值时,内核会回滚模块加载过程,不会留下半初始化的模块。
常见错误:初学者容易忘记在退出函数中释放初始化函数中申请的资源(如内存、中断、设备号等)。这会导致模块卸载后系统资源泄漏,甚至在下一次加载时因资源冲突而失败。务必保持初始化与退出逻辑的对称性。