2. Linux 内核模块基础:模块的编写、编译、加载与卸载

Linux 内核模块是内核在运行时动态加载和卸载的代码片段,它允许开发者在不重新编译整个内核的情况下扩展内核功能。本章将系统讲解模块的完整生命周期:从编写、编译到加载、卸载,并深入剖析 module_init/module_exit 宏、LICENSE 与作者信息等关键要素。

2.1 模块的基本结构

一个标准的内核模块通常包含以下核心部分:

  • 头文件包含:<linux/module.h>(模块通用接口)和 <linux/init.h>(初始化/退出宏)。
  • 入口函数:通过 module_init() 注册,模块加载时自动调用。
  • 出口函数:通过 module_exit() 注册,模块卸载时自动调用。
  • 许可证声明:使用 MODULE_LICENSE() 指定开源协议。
  • 作者与描述:使用 MODULE_AUTHOR() 和 MODULE_DESCRIPTION() 提供元信息。
💡 核心要点:模块入口和出口函数是内核模块的“构造函数”和“析构函数”,它们必须通过宏注册,而非直接调用。

2.2 模块编写实战:Hello World 示例

以下是一个最简内核模块的完整代码,它演示了模块的编写规范:

#include <linux/module.h>   // 所有模块都需要
#include <linux/init.h>     // 提供 module_init/module_exit 宏
#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);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Expert <expert@example.com>");
MODULE_DESCRIPTION("A simple Hello World kernel module for LED/GPIO course");
MODULE_VERSION("1.0");

2.3 module_init / module_exit 宏解析

宏名称 作用 注册的函数原型 返回值含义
module_init() 指定模块加载时自动调用的入口函数 int __init func(void) 0 成功;负数表示错误码
module_exit() 指定模块卸载时自动调用的出口函数 void __exit func(void) 无返回值
🔧 编译提示:__init 和 __exit 标记告诉内核:入口函数在初始化完成后可以释放内存(如果模块被编译进内核而非动态加载),出口函数在模块卸载后也可释放。这有助于节省内核内存。

2.4 模块的编译:Makefile 编写

内核模块的编译必须依赖内核源码树(或内核头文件包)。标准 Makefile 模板如下:

obj-m := hello.o

# KDIR 指向已编译的内核源码树路径
KDIR := /lib/modules/$(shell uname -r)/build

# PWD 是当前工作目录
PWD := $(shell pwd)

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

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

执行 make 后,会生成 hello.ko 文件,这就是可加载的内核模块目标文件。

2.5 模块的加载与卸载命令

操作 命令 说明
加载模块 sudo insmod hello.ko 将模块插入内核,调用 module_init 注册的函数
查看已加载模块 lsmod 列出所有内核模块及其依赖、使用计数
查看模块信息 modinfo hello.ko 显示作者、许可证、描述、参数等元数据
卸载模块 sudo rmmod hello 从内核移除模块,调用 module_exit 注册的函数
⚠️ 常见错误:如果 insmod 返回 Operation not permitted,请确认使用 sudo 或以 root 身份执行。如果返回 Unknown symbol,说明模块依赖的其他模块未加载或内核版本不匹配。

2.6 LICENSE 与作者信息:为什么重要?

内核模块必须声明许可证,这不仅是法律要求,也直接影响模块可使用的内核 API 范围:

  • GPL 兼容许可证(如 GPL、GPL v2):可以使用所有内核导出的符号,包括 EXPORT_SYMBOL_GPL 标记的函数。
  • 非 GPL 许可证(如 Proprietary):只能使用 EXPORT_SYMBOL 导出的符号,无法调用 GPL-only 接口。
  • 作者与描述:通过 modinfo 可查看,有助于模块管理和调试。

2.7 模块生命周期流程图

下图展示了内核模块从编写到卸载的完整生命周期:

内核模块生命周期流程图 编写 .c 源文件 编写 Makefile make 编译生成 .ko sudo insmod 加载 sudo rmmod 卸载 内核日志 dmesg 查看 模块入口:module_init 模块出口:module_exit 必须声明 MODULE_LICENSE GPL 才能使用全部 API

2.8 模块信息查看与调试技巧

加载模块后,可以通过以下方式验证模块是否正常工作:

  1. 查看内核日志:dmesg | tail -10 可看到 printk 输出的信息。
  2. 检查模块列表:lsmod | grep hello 确认模块已加载。
  3. 查看模块元数据:modinfo hello.ko 显示许可证、作者、描述等。
  4. 卸载验证:卸载后再次 dmesg 查看出口函数是否执行。
📌 实战建议:在嵌入式开发板上,建议在 Makefile 中指定交叉编译工具链的 CROSS_COMPILE 变量,并将 KDIR 指向目标板的内核源码路径。例如:CROSS_COMPILE=arm-linux-gnueabihf-。

2.9 本章小结

本章完成了 Linux 内核模块的完整入门:从编写最简模块、理解 module_init/module_exit 宏的注册机制,到编写 Makefile 编译、使用 insmod/rmmod 加载卸载,以及通过 MODULE_LICENSE 等宏声明模块信息。这些基础操作是后续 LED 驱动和 GPIO 中断响应调试的基石,请务必亲手编译并运行一次 Hello World 模块,确认开发环境正确配置。