1. Linux内核模块基础

1.1 什么是内核模块

内核模块(Kernel Module)是一段可以在Linux系统运行时动态加载到内核或从内核卸载的代码片段。它允许在不重新编译整个内核、不重启系统的情况下扩展内核功能。模块通常用于实现设备驱动、文件系统、网络协议、系统调用等内核级功能。

核心特性: 动态加载、无需重启、按需扩展、独立编译。

1.2 模块与应用程序的区别

内核模块运行在内核空间,应用程序运行在用户空间,两者在运行环境、资源访问、开发调试等方面存在本质差异。下表从多个维度进行对比:

对比维度 内核模块 应用程序
运行空间 内核空间(Ring 0) 用户空间(Ring 3)
入口函数 module_init() / module_exit() main()
内存访问 直接访问物理内存、内核地址空间 只能访问虚拟地址空间,受MMU保护
系统调用 不能直接调用用户态系统调用 通过glibc封装调用系统调用
标准库 不能链接libc,只能使用内核API 可链接任意用户态库
调试方式 printk、kgdb、ftrace、kprobe gdb、strace、valgrind
崩溃影响 可能导致整个系统崩溃(Oops/Panic) 通常只影响当前进程
浮点运算 默认不支持,需手动保存FPU状态 正常支持
⚠ 关键提醒: 内核模块运行在特权级,一个指针错误就可能导致系统死机。开发模块时必须格外谨慎,做好边界检查和错误处理。

1.3 模块生命周期(init/exit)

每个内核模块都有明确的生命周期,从加载到卸载经历四个阶段:加载 → 初始化 → 运行 → 卸载。其中 module_init() 和 module_exit() 是开发者必须实现的两个核心入口。

💡 记忆口诀: "init 负责初始化,exit 负责清理;加载调 init,卸载调 exit。"

生命周期流程图

Linux内核模块生命周期 insmod / modprobe module_init() 执行 成功? 返回错误码 注册成功 模块运行中 module_exit() 执行 rmmod 触发卸载 释放资源、注销设备

标准模块代码框架

一个最简的内核模块至少包含以下结构:

#include <linux/module.h>    // 所有模块都需要
#include <linux/kernel.h>    // printk 等内核函数
#include <linux/init.h>      // __init / __exit 宏

static int __init my_module_init(void)
{
    printk(KERN_INFO "Hello, Kernel Module!\n");
    // 注册设备、分配资源、初始化硬件等
    return 0;  // 返回0表示成功,负值表示失败
}

static void __exit my_module_exit(void)
{
    printk(KERN_INFO "Goodbye, Kernel Module!\n");
    // 释放资源、注销设备、清理状态等
}

module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");              // 许可证声明
MODULE_AUTHOR("Your Name");         // 作者信息
MODULE_DESCRIPTION("A simple module"); // 模块描述
MODULE_VERSION("1.0");              // 版本号

生命周期各阶段详解

  1. 加载阶段(insmod / modprobe):
    • 用户态工具将模块 .ko 文件读入内核空间
    • 内核模块加载器解析 ELF 格式,进行符号解析和重定位
    • 检查模块依赖和版本信息(vermagic)
    • 调用 module_init 注册的初始化函数
  2. 初始化阶段(module_init):
    • 执行开发者编写的初始化代码
    • 注册设备驱动、分配内存、申请中断、创建文件系统等
    • 返回 0 表示成功;返回负值(如 -ENOMEM、-EIO)表示失败
    • 初始化失败时,内核自动卸载模块,不会进入运行态
  3. 运行阶段:
    • 模块注册的各类回调函数(如 open/read/write/ioctl)被内核调用
    • 模块代码常驻内核空间,直到被卸载
    • 此阶段模块可能被多个进程并发访问,需考虑竞态条件
  4. 卸载阶段(module_exit):
    • 用户执行 rmmod 或系统关机时触发
    • 执行开发者编写的清理代码
    • 释放所有已分配的资源,注销设备节点
    • 如果模块被其他模块引用,卸载会被拒绝(rmmod: ERROR: Module is in use)
📌 最佳实践: init 函数中每分配一个资源,都要考虑 exit 中如何释放。建议采用"goto 错误处理"模式,确保任何失败路径都能正确回滚已分配的资源。

模块引用计数

内核通过 module_refcount 跟踪模块的使用情况。当模块被其他模块依赖、或设备文件被打开时,引用计数会增加。卸载模块前,内核会检查引用计数是否为 0,不为 0 则拒绝卸载。

// 查看模块引用计数
$ lsmod
Module                  Size  Used by
my_driver              16384  1  # 引用计数为1
usb_storage            73728  0

模块参数传递

模块支持在加载时通过参数传递配置信息,方便调试和适配不同硬件:

// 模块代码中声明参数
static int debug_level = 0;
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");

static char *device_name = "default";
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(device_name, "Device name string");

// 加载时传递参数
$ sudo insmod my_module.ko debug_level=2 device_name="mydev0"
💡 调试技巧: 使用 modinfo 命令可以查看模块的 license、作者、描述、参数等信息:modinfo my_module.ko。

模块依赖关系

模块之间可以存在依赖。例如,USB 存储驱动依赖于 USB 核心模块。依赖关系由内核自动管理,加载模块时会先加载其依赖模块。

// 查看模块依赖树
$ modprobe --show-depends usb_storage
insmod /lib/modules/5.15.0/kernel/drivers/usb/core/usbcore.ko
insmod /lib/modules/5.15.0/kernel/drivers/scsi/scsi_mod.ko
insmod /lib/modules/5.15.0/kernel/drivers/usb/storage/usb-storage.ko

模块版本校验(vermagic)

内核在加载模块时会校验 vermagic 字符串,确保模块与当前运行的内核版本、编译器版本、SMP/抢占配置等匹配。不匹配时加载会失败:

$ sudo insmod my_module.ko
insmod: ERROR: could not insert module my_module.ko: Invalid module format
$ dmesg | tail
[ 1234.5678] my_module: version magic '5.15.0-generic SMP mod_unload ' 
             should be '5.15.0-rc7+ SMP mod_unload '
⚠ 常见错误: 模块编译环境的内核头文件版本与运行内核版本不一致,是导致 "Invalid module format" 的最常见原因。务必使用 uname -r 确认目标内核版本。