2. 内核模块基础:模块加载与卸载、module_init/module_exit宏、LICENSE声明

在深入字符设备驱动的 read 与 write 接口实现之前,必须首先掌握内核模块的骨架。Linux 内核模块(Loadable Kernel Module, LKM)是一种可以在运行时动态加载到内核或从内核卸载的代码片段。本章将系统讲解模块的加载与卸载机制、入口/出口宏的定义规范以及许可证声明的法律与工程意义。

2.1 内核模块的生命周期

一个标准的内核模块经历以下四个阶段:

  1. 加载(Loading):通过 insmod 或 modprobe 将模块二进制文件插入内核地址空间。
  2. 初始化(Initialization):内核自动调用模块中声明的初始化函数,完成资源申请、设备注册等操作。
  3. 运行(Running):模块在内核空间提供服务,响应系统调用或中断事件。
  4. 卸载(Unloading):通过 rmmod 移除模块,内核调用清理函数释放资源。
关键概念: 模块加载后即成为内核的一部分,运行在特权级(Ring 0),拥有完全的内存访问权限。任何错误都可能导致系统崩溃(Kernel Panic),因此模块代码必须经过严格测试。
内核模块生命周期流程图 内核模块生命周期 模块文件 (.ko) 初始化 (init) 运行中 卸载 (exit) insmod → module_init() → 注册设备 → 提供服务 → rmmod → module_exit() → 释放资源

2.2 module_init 与 module_exit 宏

Linux 内核通过两个关键宏将模块的入口和出口函数注册到内核模块框架中:

宏名称 作用 函数原型 返回值
module_init() 指定模块加载时调用的初始化函数 int __init init_func(void) 0 表示成功,负值表示失败
module_exit() 指定模块卸载时调用的清理函数 void __exit exit_func(void) 无返回值
工程实践: 初始化函数应使用 __init 标记,清理函数应使用 __exit 标记。这些标记告诉内核:初始化代码在模块加载完成后可以被回收(如果内核配置了 CONFIG_MODULE_FORCE_UNLOAD),从而节省内存。

2.2.1 标准模块骨架代码

以下是一个最简内核模块的完整实现,展示了 module_init 和 module_exit 的标准用法:

#include <linux/module.h>    /* 所有模块都需要 */
#include <linux/kernel.h>    /* printk 和 KERN_INFO */
#include <linux/init.h>      /* __init 和 __exit 宏 */

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, kernel module loaded!\n");
    /* 在此处注册字符设备、分配内存、初始化锁等 */
    return 0;  /* 返回 0 表示初始化成功 */
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, kernel module unloaded!\n");
    /* 在此处释放资源、注销设备、销毁锁等 */
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Expert");
MODULE_DESCRIPTION("A minimal kernel module example");
常见错误: 初始化函数返回非零值(如 -ENOMEM 或 -EBUSY)时,模块加载会失败,内核会自动回滚已分配的资源。不要在返回错误前手动释放资源,内核会处理未完成的分配。

2.3 LICENSE 声明:法律与工程的双重意义

每个内核模块必须包含 MODULE_LICENSE 声明。这不仅是一个法律要求,还直接影响内核的行为:

  • GPL 兼容许可证(如 "GPL"、"GPL v2"):模块可以使用内核导出的所有符号,包括 EXPORT_SYMBOL_GPL 标记的符号。
  • 非 GPL 许可证(如 "Proprietary"、"BSD"):模块无法访问 EXPORT_SYMBOL_GPL 符号,且内核会在加载时打印 "tainted"(污染)警告。
  • 无声明:内核默认拒绝加载未声明许可证的模块。
许可证字符串 含义 能否使用 GPL-only 符号 内核污染标记
"GPL" GNU General Public License v2 是 无
"GPL v2" GPL 第二版 是 无
"Dual BSD/GPL" 双许可证 是 无
"Proprietary" 专有许可证 否 是 (Tainted)
"MIT" MIT 许可证 否 是 (Tainted)
调试提示: 在字符设备驱动开发中,如果使用了 EXPORT_SYMBOL_GPL 导出的内核函数(如 class_create、device_create 的某些变体),必须声明 MODULE_LICENSE("GPL"),否则链接阶段会报错 "Unknown symbol"。
模块加载与许可证检查流程 模块加载与许可证检查流程 用户执行 insmod 内核接收模块文件 检查 MODULE_LICENSE GPL 兼容 → 加载成功 非 GPL → 加载失败/污染 注意:即使加载成功,非 GPL 模块也会标记内核为 "Tainted",影响调试和 bug 报告

2.4 模块编译与加载实践

编写完模块代码后,需要编写 Makefile 来编译。以下是标准的内核模块 Makefile 模板:

obj-m := hello.o

KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) clean

编译与加载命令:

# 编译模块
make

# 加载模块
sudo insmod hello.ko

# 查看模块是否加载
lsmod | grep hello

# 查看内核日志
dmesg | tail

# 卸载模块
sudo rmmod hello

# 再次查看日志确认卸载
dmesg | tail
调试技巧: 使用 modinfo hello.ko 可以查看模块的详细信息,包括许可证、作者、描述和依赖关系。这是验证 MODULE_LICENSE 是否正确的快速方法。

2.5 本章小结

  • 内核模块通过 module_init() 和 module_exit() 宏定义生命周期函数。
  • 初始化函数应使用 __init 标记,返回 0 表示成功;清理函数使用 __exit 标记。
  • MODULE_LICENSE("GPL") 是字符设备驱动开发的必要条件,确保可以访问所有内核符号。
  • 模块加载失败时,通过 dmesg 查看内核日志是定位问题的首要手段。
  • 掌握模块骨架是后续实现字符设备 read 和 write 接口的基础。