4. 模块加载与卸载函数
在嵌入式Linux字符设备驱动开发中,模块加载与卸载是驱动生命周期管理的核心入口。内核通过 module_init() 和 module_exit() 宏来注册驱动的初始化和清理函数,而 MODULE_LICENSE() 则声明模块的许可证,直接影响内核是否允许该模块使用某些GPL-only符号。
4.1 模块加载与卸载机制概述
Linux内核模块(.ko文件)在加载时,内核会调用 module_init() 指定的函数;在卸载时,调用 module_exit() 指定的函数。这两个宏本质上是将函数指针放入特定的ELF段(.init.text 和 .exit.text),由内核模块加载器在适当阶段执行。
module_init() 和一个 module_exit()。如果未定义,内核将拒绝加载该模块。
insmod 加载,rmmod 卸载。
4.2 module_init 与 module_exit 宏定义
这两个宏定义在 <linux/module.h> 中,其展开过程涉及 __init 和 __exit 属性标记,以及段分配。以下是典型定义(简化版):
/* 定义在 include/linux/module.h */
#define module_init(x) __initcall(x);
#define module_exit(x) __exitcall(x);
/* __init 属性:将函数放入 .init.text 段,加载完成后可释放 */
#define __init __section(".init.text")
#define __exit __section(".exit.text")
当模块被静态编译进内核时,__exit 标记的函数会被直接丢弃,因为无法卸载。只有作为模块编译时,module_exit() 才有效。
4.3 标准驱动模板代码
以下是一个完整的字符设备驱动模块框架,展示了 module_init、module_exit 和 MODULE_LICENSE 的标准用法:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h> /* 文件操作结构体 */
#include <linux/cdev.h> /* 字符设备 */
#include <linux/device.h> /* 设备类 */
#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME "my_class"
static int major_num;
static struct class *my_class = NULL;
static struct device *my_device = NULL;
static struct cdev my_cdev;
/* 模块加载函数 */
static int __init my_driver_init(void)
{
dev_t dev_num;
int ret;
/* 1. 动态分配设备号 */
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
major_num = MAJOR(dev_num);
pr_info("Allocated major number %d\n", major_num);
/* 2. 初始化 cdev 并添加到内核 */
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
unregister_chrdev_region(dev_num, 1);
pr_err("Failed to add cdev\n");
return ret;
}
/* 3. 创建设备类与设备节点(自动生成 /dev/xxx) */
my_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(my_class)) {
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_class);
}
my_device = device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME);
if (IS_ERR(my_device)) {
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_device);
}
pr_info("Driver loaded successfully\n");
return 0;
}
/* 模块卸载函数 */
static void __exit my_driver_exit(void)
{
dev_t dev_num = MKDEV(major_num, 0);
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("Driver unloaded\n");
}
/* 注册入口与出口 */
module_init(my_driver_init);
module_exit(my_driver_exit);
/* 许可证声明(必须) */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Engineer");
MODULE_DESCRIPTION("A simple character device driver example");
MODULE_VERSION("1.0");
module_init()alloc_chrdev_region() 和 cdev_add() 的返回值,导致加载失败时资源泄漏。务必在错误路径中释放已分配的资源。
4.4 MODULE_LICENSE 的作用与可选值
MODULE_LICENSE() 宏用于声明模块的许可证类型。内核根据此声明决定是否允许模块使用标记为 EXPORT_SYMBOL_GPL() 的符号。如果模块声明了非GPL兼容许可证,则无法调用GPL-only内核API。
| 许可证字符串 | 含义 | 能否使用 GPL-only 符号 | 常见场景 |
|---|---|---|---|
"GPL" |
GNU General Public License v2 | 是 | 开源驱动、内核主线模块 |
"GPL v2" |
GPL v2 专用 | 是 | 与GPL v2严格兼容 |
"GPL and additional rights" |
GPL + 额外许可 | 是 | 某些厂商开源驱动 |
"Dual BSD/GPL" |
BSD 或 GPL 双许可 | 是 | BSD项目移植 |
"Dual MPL/GPL" |
MPL 或 GPL 双许可 | 是 | Mozilla项目相关 |
"Proprietary" |
专有许可证 | 否 | 闭源商业驱动 |
MODULE_LICENSE(),内核会默认认为该模块是“专有”的,并会在加载时打印警告信息。对于嵌入式商业产品,如果驱动使用了GPL-only内核函数(如 class_create()、cdev_add()),则必须声明为 "GPL",否则加载会失败。
4.5 模块加载与卸载流程图
下图展示了从用户空间执行 insmod / rmmod 到内核执行 module_init() / module_exit() 的完整流程:
4.6 模块加载与卸载的执行顺序
在驱动开发中,module_init() 和 module_exit() 中的操作必须严格对称,否则会导致资源泄漏或内核崩溃。以下是标准执行顺序:
- 加载阶段(module_init):
- 注册设备号(
alloc_chrdev_region或register_chrdev_region) - 初始化 cdev 结构体并添加到内核(
cdev_init+cdev_add) - 创建设备类(
class_create) - 创建设备节点(
device_create) - 初始化硬件(GPIO、时钟、中断等)
- 注册设备号(
- 卸载阶段(module_exit):
- 销毁设备节点(
device_destroy) - 销毁设备类(
class_destroy) - 从内核删除 cdev(
cdev_del) - 释放设备号(
unregister_chrdev_region) - 释放硬件资源(GPIO、时钟、中断等)
- 销毁设备节点(
4.7 模块信息宏
除了 MODULE_LICENSE,内核还提供了其他信息宏用于描述模块,这些信息可通过 modinfo 命令查看:
| 宏 | 描述 | 示例 |
|---|---|---|
MODULE_AUTHOR |
作者信息 | MODULE_AUTHOR("Zhang San <zhangsan@example.com>"); |
MODULE_DESCRIPTION |
模块功能描述 | MODULE_DESCRIPTION("SPI Flash Driver for i.MX6"); |
MODULE_VERSION |
版本号 | MODULE_VERSION("2.1.0"); |
MODULE_ALIAS |
模块别名,用于自动加载 | MODULE_ALIAS("platform:my-device"); |
MODULE_DEVICE_TABLE |
设备表,用于热插拔匹配 | MODULE_DEVICE_TABLE(of, my_of_match); |
4.8 模块参数传递
在嵌入式调试中,经常需要在加载模块时传递参数。使用 module_param() 宏可以定义模块参数:
#include <linux/moduleparam.h>
static int debug_level = 0;
static char *device_name = "default_dev";
module_param(debug_level, int, 0644);
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
MODULE_PARM_DESC(device_name, "Device name string");
/* 加载时使用:insmod mydrv.ko debug_level=2 device_name="mydev" */
module_param 传递 debug_level 可以动态控制驱动日志输出,避免重新编译模块。配合 pr_debug() 使用效果更佳。
4.9 本章小结
module_init()和module_exit()是驱动加载/卸载的入口和出口,必须成对出现。MODULE_LICENSE("GPL")是使用内核GPL-only API的必要条件,闭源驱动无法调用class_create、cdev_add等函数。- 加载与卸载操作必须严格对称,遵循“后注册的先注销”原则,防止资源泄漏。
- 使用
module_param可以在加载时传递参数,提高调试灵活性。 - 通过
modinfo命令可以查看模块的许可证、作者、描述等信息。