2. 内核模块基础:模块的加载与卸载、module_init/module_exit机制、MODULE_LICENSE/MODULE_AUTHOR宏

内核模块是Linux内核动态扩展能力的核心载体。在编写proc文件系统驱动之前,必须透彻理解模块的生命周期管理、入口/出口函数注册机制以及元信息声明规范。本章从工程实战角度拆解这些基础要素,并给出可直接编译验证的代码模板。

2.1 模块生命周期:加载与卸载

一个内核模块在系统中的完整生命周期包含四个阶段:加载 → 初始化 → 运行 → 卸载。用户态通过insmod和rmmod命令触发内核态的模块加载与卸载流程。

关键路径: insmod → 系统调用 init_module → 内核执行模块构造函数 → 模块进入“存活”状态;rmmod → 系统调用 delete_module → 内核执行模块析构函数 → 模块从内存中移除。
工程提示: 使用 lsmod 可查看当前已加载模块列表及依赖关系;modinfo 可读取模块的元信息(LICENSE、AUTHOR、DESCRIPTION等)。
内核模块生命周期状态机 磁盘 .ko 文件 加载中 (init) 运行中 (存活) 卸载中 (exit) insmod 成功返回 rmmod 释放资源 从内存移除 文件系统存储 未进入内核空间

2.2 module_init / module_exit 机制

Linux内核使用两个宏——module_init() 和 module_exit()——来声明模块的入口函数和出口函数。这两个宏在编译时会被展开为特定的段属性,使得内核在加载/卸载时能够自动找到并调用对应的函数指针。

宏 触发时机 函数原型 典型用途
module_init(x) 模块加载时(insmod) int __init x(void) 注册设备、分配资源、创建proc条目
module_exit(x) 模块卸载时(rmmod) void __exit x(void) 注销设备、释放资源、移除proc条目
常见错误: 如果 module_init() 返回非0值,内核认为模块初始化失败,不会将模块标记为“存活”状态,且会自动回滚已分配的部分资源。此时 module_exit() 不会被调用,因此初始化函数中必须做好错误路径的清理工作。
module_init / module_exit 调用流程 insmod rmmod init_module() delete_module() module_init() my_proc_init module_exit() my_proc_exit 内核自动查找 .initcall 段 和 .exitcall 段中的函数指针

2.3 MODULE_LICENSE / MODULE_AUTHOR 宏

这些宏用于向内核和用户空间声明模块的元信息。它们被编译到模块的 .modinfo 段中,可通过 modinfo 命令读取。

宏 参数 作用 注意事项
MODULE_LICENSE(license) 字符串,如 "GPL" 声明模块许可证,影响内核是否允许使用GPL-only符号 非GPL兼容许可证可能导致内核拒绝加载某些功能
MODULE_AUTHOR(author) 字符串,如 "Zhang San" 声明模块作者信息 纯信息性,不影响功能
MODULE_DESCRIPTION(desc) 字符串 模块功能描述 建议填写,便于维护
MODULE_VERSION(ver) 字符串,如 "1.0" 模块版本号 用于版本追踪
许可证选择建议: 绝大多数内核驱动使用 "GPL" 或 "GPL v2"。如果模块需要调用 EXPORT_SYMBOL_GPL 导出的内核符号,则必须声明为 GPL 兼容许可证,否则链接阶段会报错。

2.4 完整工程模板:最小proc模块

下面给出一个可直接编译运行的内核模块模板,它实现了基本的加载/卸载流程,并声明了完整的元信息。该模板是后续章节创建proc文件系统的基础骨架。

/* minimal_proc_module.c */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>

#define PROC_NAME "myproc_demo"

static int __init myproc_init(void)
{
    int ret = 0;

    pr_info("myproc: module loading...\n");

    /* 在 /proc 下创建一个条目 */
    /* 具体实现将在后续章节展开 */
    pr_info("myproc: /proc/%s created\n", PROC_NAME);

    return ret;
}

static void __exit myproc_exit(void)
{
    /* 移除 /proc 下的条目 */
    pr_info("myproc: /proc/%s removed\n", PROC_NAME);
    pr_info("myproc: module unloading...\n");
}

module_init(myproc_init);
module_exit(myproc_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Driver Expert <expert@example.com>");
MODULE_DESCRIPTION("Minimal proc filesystem demo module");
MODULE_VERSION("1.0");
编译与测试: 将上述代码与一个简单的 Makefile 配合(obj-m += minimal_proc_module.o),执行 make -C /lib/modules/$(uname -r)/build M=$(pwd) modules 编译。然后 sudo insmod minimal_proc_module.ko 加载,sudo rmmod minimal_proc_module 卸载,通过 dmesg | tail 查看内核日志输出。
模块 .ko 文件内部元信息段结构 minimal_proc_module.ko .text myproc_init myproc_exit .initcall module_init 函数指针 .exitcall module_exit 函数指针 .modinfo license=GPL author=... version=1.0

2.5 常见问题与避坑指南

  • 模块加载失败(insmod: ERROR: could not insert module):检查 dmesg 查看具体错误;常见原因包括内核版本不匹配、符号未导出、依赖模块未加载。
  • 模块卸载时系统崩溃(kernel oops):通常是因为 module_exit() 中释放了仍在被其他模块或进程使用的资源。务必在卸载前确认引用计数归零。
  • MODULE_LICENSE 声明为 "Proprietary" 导致某些内核功能不可用:如果模块使用了 EXPORT_SYMBOL_GPL 导出的符号,内核会拒绝加载非GPL模块。
  • __init 和 __exit 标记的作用:__init 标记的函数在初始化完成后会被内核回收内存;__exit 标记的函数在模块编译进内核(非模块方式)时会被丢弃。不要在这些函数中放置需要持久存在的逻辑。
调试建议: 在 module_init() 和 module_exit() 函数中始终使用 pr_info() 或 printk() 输出关键日志,包括函数进入、参数值、错误码。这是内核驱动开发中最基础也最有效的调试手段。