3、第一个字符设备驱动:模块初始化与退出函数、module_init/module_exit宏、LICENSE与作者信息
在嵌入式Linux驱动开发中,第一个字符设备驱动通常以内核模块的形式存在。内核模块允许我们在不重新编译整个内核的情况下,动态地向内核添加功能。本章将聚焦于构建一个最简字符设备驱动的骨架,重点讲解模块的初始化与退出机制、关键宏的使用以及模块信息的声明。
3.1 内核模块的基本结构
一个完整的内核模块至少包含两个核心函数:模块初始化函数和模块退出函数。这两个函数通过特定的宏注册到内核框架中,由内核在模块加载和卸载时自动调用。
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 命令到内核模块初始化/退出函数被调用的完整流程。
3.6 关键点总结
- 模块入口:使用
module_init()和module_exit()宏注册初始化与退出函数,这是模块与内核交互的契约。 - 函数标记:
__init和__exit标记帮助内核优化内存使用,初始化完成后可回收相关代码段。 - 许可证声明:
MODULE_LICENSE()是强制要求,缺失或声明为"Proprietary"将导致模块无法使用GPL-only的内核API。 - 模块元数据:
MODULE_AUTHOR()和MODULE_DESCRIPTION()虽然不是强制要求,但良好的文档习惯有助于团队协作和问题排查。 - 返回值检查:初始化函数返回非零值时,内核会回滚模块加载过程,不会留下半初始化的模块。