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 命令触发,其内部流程如下:

  1. 加载(insmod): 用户执行 insmod hello.ko → 内核模块加载器解析 ELF 文件 → 分配内核内存 → 调用 module_init 注册的函数 → 函数返回 0 表示成功。
  2. 卸载(rmmod): 用户执行 rmmod hello → 内核检查模块引用计数是否为 0 → 调用 module_exit 注册的函数 → 释放模块占用的内存。
  3. 查看已加载模块: 使用 lsmod 命令或查看 /proc/modules 文件。
常见错误: 如果 module_init 函数返回非 0 值(如 -ENOMEM),内核会认为模块加载失败并自动回滚所有资源分配。如果模块正在被使用(引用计数 > 0),rmmod 会失败并提示 "Module is in use"。

下面这张 SVG 图展示了内核模块从编译到卸载的完整生命周期:

内核模块生命周期流程图 内核模块生命周期流程图 源代码 (.c) 编译 (Makefile) 模块文件 (.ko) insmod 加载 module_init() 内核空间运行 rmmod 卸载 module_exit() 资源释放完成 关键命令 编译:make 加载:insmod 卸载:rmmod 查看:lsmod 模块信息: modinfo ⚠ 注意: 加载失败返回 非0值会回滚 引用计数>0 无法卸载

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 在内核模块加载/卸载过程中的调用关系:

module_init 与 module_exit 调用关系图 module_init / module_exit 调用关系 用户空间 (shell) 系统调用层 内核模块加载器 module_init() module_exit() insmod → 系统调用 → 模块加载器 → 执行 module_init() rmmod → 系统调用 → 模块加载器 → 执行 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 图展示了内核模块从编写到调试的完整工作流程:

内核模块开发工作流 内核模块开发工作流 编写 .c 文件 编写 Makefile 执行 make 生成 .ko 文件 insmod 加载 dmesg 查看日志 测试功能 rmmod 卸载 调试反馈循环:修改代码 → 重新编译 → 重新加载

2.6 本章小结

本章涵盖了内核模块开发的核心基础:

  • 模块结构: 头文件、初始化函数、清理函数、宏注册是模块的四大支柱。
  • 加载与卸载: insmod 触发 module_init,rmmod 触发 module_exit,返回值和引用计数是成败关键。
  • 宏机制: module_init 和 module_exit 通过 ELF 段机制将函数注册到内核框架。
  • 元信息: MODULE_LICENSE 是强制要求,MODULE_AUTHOR、MODULE_DESCRIPTION 等是良好实践。
  • 编译与调试: Makefile 借助内核 Kbuild 系统,dmesg 是调试的第一工具。
实践建议: 立即动手编写一个最简单的 "Hello World" 模块,完成编译、加载、查看日志、卸载的完整流程。这是进入内核驱动开发世界的第一步,也是最重要的一步。