2. 内核模块基础:模块的加载与卸载、module_init与module_exit宏、模块许可证与作者信息
Linux 内核模块是内核在运行时可以动态加载和卸载的一段代码,它允许开发者在不重新编译整个内核、不重启系统的情况下扩展内核功能。本章将深入剖析内核模块的骨架结构、生命周期管理宏以及元信息声明,为后续编写字符设备驱动打下坚实基础。
核心概念:内核模块不是应用程序,它没有 main() 函数,而是通过
module_init() 和 module_exit() 宏向内核注册入口和出口。模块运行在内核空间,拥有最高特权级,任何错误都可能导致系统崩溃。
2.1 内核模块的基本结构
一个标准的内核模块至少包含以下四个部分:
- 头文件包含:
#include <linux/module.h>和#include <linux/kernel.h>是所有模块必须引入的头文件。 - 初始化函数: 模块加载时执行,通常命名为
xxx_init。 - 清理函数: 模块卸载时执行,通常命名为
xxx_exit。 - 宏注册: 使用
module_init()和module_exit()将上述函数注册到内核模块框架中。
下面是一个最简内核模块的完整代码示例:
#include <linux/module.h> // 包含模块相关的宏和函数
#include <linux/kernel.h> // 包含 printk 等内核打印函数
// 模块初始化函数,模块加载时调用
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);
编码规范提示: 初始化函数建议使用
__init 标记,清理函数使用 __exit 标记。这些标记告诉内核:初始化代码在模块加载完成后可以释放内存,清理代码在模块编译进内核时可以被丢弃。
2.2 模块的加载与卸载流程
内核模块的生命周期由用户空间通过 insmod 和 rmmod 命令触发,其内部流程如下:
- 加载(insmod): 用户执行
insmod hello.ko→ 内核模块加载器解析 ELF 文件 → 分配内核内存 → 调用module_init注册的函数 → 函数返回 0 表示成功。 - 卸载(rmmod): 用户执行
rmmod hello→ 内核检查模块引用计数是否为 0 → 调用module_exit注册的函数 → 释放模块占用的内存。 - 查看已加载模块: 使用
lsmod命令或查看/proc/modules文件。
常见错误: 如果
module_init 函数返回非 0 值(如 -ENOMEM),内核会认为模块加载失败并自动回滚所有资源分配。如果模块正在被使用(引用计数 > 0),rmmod 会失败并提示 "Module is in use"。
下面这张 SVG 图展示了内核模块从编译到卸载的完整生命周期:
2.3 module_init 与 module_exit 宏的深入解析
module_init() 和 module_exit() 是内核提供的宏,它们将开发者定义的函数指针注册到内核模块框架的特定段中。理解这两个宏的底层机制对于调试和高级编程至关重要。
| 宏名称 | 作用 | 函数原型要求 | 返回值含义 |
|---|---|---|---|
| module_init(x) | 将函数 x 注册为模块的入口点,加载时被调用 | int __init x(void) |
返回 0 成功;负值表示错误码 |
| module_exit(x) | 将函数 x 注册为模块的出口点,卸载时被调用 | void __exit x(void) |
无返回值 |
底层原理: 在编译时,
module_init() 宏将函数指针放入 ELF 文件的 .init.text 段,module_exit() 宏将函数指针放入 .exit.text 段。内核模块加载器在加载模块时会扫描这些特定段,找到入口函数并执行。如果模块被编译进内核(而非动态加载),__exit 标记的函数会被直接丢弃以节省内存。
下面这张 SVG 图展示了 module_init 和 module_exit 在内核模块加载/卸载过程中的调用关系:
2.4 模块许可证与作者信息
Linux 内核是 GPL 许可证下的开源项目,内核模块必须声明其许可证,否则内核会认为该模块是“污染”内核的专有模块。此外,良好的模块元信息有助于维护和调试。
常用的模块元信息宏如下:
| 宏名称 | 作用 | 示例 |
|---|---|---|
| MODULE_LICENSE() | 声明模块许可证,影响内核是否允许该模块使用某些 GPL-only 符号 | MODULE_LICENSE("GPL"); |
| MODULE_AUTHOR() | 声明模块作者信息 | MODULE_AUTHOR("Zhang San <zhangsan@example.com>"); |
| MODULE_DESCRIPTION() | 简要描述模块功能 | MODULE_DESCRIPTION("A simple character device driver"); |
| MODULE_VERSION() | 声明模块版本号 | MODULE_VERSION("1.0"); |
| MODULE_ALIAS() | 为模块创建别名,方便 modprobe 自动加载 | MODULE_ALIAS("char-major-240-0"); |
许可证陷阱: 如果模块使用了
EXPORT_SYMBOL_GPL() 导出的内核符号,但自身声明的是非 GPL 许可证(如 "Proprietary"),内核会拒绝加载该模块并打印 "module uses symbol from GPL-only symbol" 错误。常见的有效许可证值包括:"GPL"、"GPL v2"、"GPL and additional rights"、"Dual BSD/GPL"、"Dual MPL/GPL"。
下面是一个包含完整元信息的内核模块示例:
#include <linux/module.h>
#include <linux/kernel.h>
static int __init mydriver_init(void)
{
printk(KERN_INFO "MyDriver: Initializing...\n");
return 0;
}
static void __exit mydriver_exit(void)
{
printk(KERN_INFO "MyDriver: Exiting...\n");
}
module_init(mydriver_init);
module_exit(mydriver_exit);
// 模块元信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Li Si <lisi@kernel.org>");
MODULE_DESCRIPTION("MyDriver - A sample kernel module for learning");
MODULE_VERSION("2.1.0");
MODULE_ALIAS("mydriver");
查看模块信息: 编译生成
.ko 文件后,可以使用 modinfo mydriver.ko 命令查看模块的元信息,包括许可证、作者、描述、依赖等。这对于模块分发和调试非常有用。
2.5 模块编译与 Makefile 基础
内核模块的编译依赖内核源码树提供的 Kbuild 系统。一个典型的 Makefile 如下:
# Makefile for kernel module
obj-m := hello.o
# 如果模块由多个源文件组成,使用如下语法:
# hello-objs := file1.o file2.o
# 指定内核源码树路径,通常为 /lib/modules/$(shell uname -r)/build
KDIR := /lib/modules/$(shell uname -r)/build
# 当前目录
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
编译完成后,会在当前目录生成 hello.ko 文件。使用 insmod hello.ko 加载,rmmod hello 卸载,dmesg | tail 查看内核打印的日志信息。
调试技巧: 如果模块加载后没有看到预期的打印信息,请检查:1) 是否使用了正确的
printk 日志级别(如 KERN_INFO);2) 使用 dmesg -w 实时监控内核日志;3) 检查 /var/log/kern.log 或 journalctl -k。
下面这张 SVG 图展示了内核模块从编写到调试的完整工作流程:
2.6 本章小结
本章涵盖了内核模块开发的核心基础:
- 模块结构: 头文件、初始化函数、清理函数、宏注册是模块的四大支柱。
- 加载与卸载:
insmod触发module_init,rmmod触发module_exit,返回值和引用计数是成败关键。 - 宏机制:
module_init和module_exit通过 ELF 段机制将函数注册到内核框架。 - 元信息:
MODULE_LICENSE是强制要求,MODULE_AUTHOR、MODULE_DESCRIPTION等是良好实践。 - 编译与调试: Makefile 借助内核 Kbuild 系统,
dmesg是调试的第一工具。
实践建议: 立即动手编写一个最简单的 "Hello World" 模块,完成编译、加载、查看日志、卸载的完整流程。这是进入内核驱动开发世界的第一步,也是最重要的一步。