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() 的完整流程:

模块加载与卸载流程 用户空间 insmod mydrv.ko rmmod mydrv 内核空间 内核模块加载器 (sys_init_module) 内核模块卸载器 (sys_delete_module) module_init() my_driver_init() module_exit() my_driver_exit() 设备注册成功 /dev/my_char_dev 创建 设备注销完成 /dev/my_char_dev 删除 MODULE_LICENSE("GPL") 必须声明

4.6 模块加载与卸载的执行顺序

在驱动开发中,module_init() 和 module_exit() 中的操作必须严格对称,否则会导致资源泄漏或内核崩溃。以下是标准执行顺序:

  1. 加载阶段(module_init):
    • 注册设备号(alloc_chrdev_region 或 register_chrdev_region)
    • 初始化 cdev 结构体并添加到内核(cdev_init + cdev_add)
    • 创建设备类(class_create)
    • 创建设备节点(device_create)
    • 初始化硬件(GPIO、时钟、中断等)
  2. 卸载阶段(module_exit):
    • 销毁设备节点(device_destroy)
    • 销毁设备类(class_destroy)
    • 从内核删除 cdev(cdev_del)
    • 释放设备号(unregister_chrdev_region)
    • 释放硬件资源(GPIO、时钟、中断等)
避坑指南: 卸载顺序必须与加载顺序完全相反。如果先释放设备号再删除 cdev,此时 cdev 可能仍在被访问,导致内核 panic。始终遵循“后注册的先注销”原则。

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 命令可以查看模块的许可证、作者、描述等信息。