1. Linux内核模块基础

1.1 内核模块概念

Linux内核模块(Loadable Kernel Module, LKM)是一段可以在系统运行时动态加载到内核空间并执行的代码。它允许在不重新编译整个内核、不重启系统的情况下扩展内核功能,是Linux设备驱动开发的核心载体。

核心定义: 内核模块是运行在内核空间、拥有最高CPU特权级(Ring 0)的可动态装卸的二进制目标文件(.ko)。

1.2 模块与应用程序的本质区别

对比维度 内核模块 用户态应用程序
运行空间 内核空间(Ring 0) 用户空间(Ring 3)
入口/出口 module_init() / module_exit() main()
内存访问 直接访问物理内存、内核数据结构 通过虚拟地址,受MMU保护
库支持 只能使用内核导出的API(如printk) 可使用glibc等用户态库
浮点运算 默认禁止,需手动保存/恢复FPU状态 正常支持
崩溃影响 导致整个系统宕机(kernel panic) 仅终止当前进程
调试难度 高(依赖kgdb、kdump、ftrace) 低(gdb、Valgrind等工具成熟)
⚠️ 开发警示: 内核模块运行在Ring 0特权级,一个空指针解引用就会触发Oops甚至kernel panic。编写模块时必须对每一处指针访问、内存拷贝做边界检查。

1.3 模块加载与卸载流程

内核模块的生命周期由insmod(加载)和rmmod(卸载)两个用户态工具驱动,背后涉及系统调用、内核符号解析和模块引用计数等关键步骤。

加载流程(insmod)

  1. 文件读取: insmod 将 .ko 文件读入用户空间缓冲区。
  2. 系统调用: 通过 init_module() 或 finit_module() 系统调用进入内核。
  3. ELF解析: 内核解析模块的ELF节区,识别代码段、数据段、符号表。
  4. 符号解析: 将模块中引用的外部符号(如 printk)与内核符号表(/proc/kallsyms)匹配。
  5. 内存分配: 在内核空间分配连续虚拟内存,将模块代码和数据拷贝到目标地址。
  6. 重定位: 根据加载基地址修正模块内部的绝对地址引用。
  7. 调用初始化: 执行 module_init() 指定的初始化函数。
  8. 注册完成: 模块加入内核模块链表,引用计数置为0。

卸载流程(rmmod)

  1. 引用检查: 内核检查模块引用计数是否为0(try_module_get() 计数)。
  2. 调用清理: 执行 module_exit() 指定的清理函数(如注销设备、释放内存)。
  3. 符号清理: 从内核符号表中移除该模块导出的符号。
  4. 内存释放: 释放模块占用的内核内存。
  5. 链表移除: 将模块从内核模块链表中摘除。
💡 实用技巧: 使用 lsmod 查看当前已加载模块列表;modinfo <module_name> 查看模块作者、许可证、参数等元信息;modprobe 可自动处理模块依赖关系。
内核模块加载与卸载完整流程 insmod rmmod init_module() delete_module() ELF解析 符号解析 内存分配+重定位 调用 module_init() 加入模块链表 引用计数检查 调用 module_exit() 符号清理 内存释放 链表移除 加载流程(左)与卸载流程(右) 均需经过系统调用进入内核空间

1.4 最小内核模块代码结构

一个完整的内核模块至少包含许可证声明、初始化入口和清理出口。以下是最简模板:

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

static int __init my_module_init(void)
{
    printk(KERN_INFO "Hello, kernel module loaded!\n");
    return 0;  // 0表示成功,负数表示失败
}

static void __exit my_module_exit(void)
{
    printk(KERN_INFO "Goodbye, kernel module unloaded!\n");
}

module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");               // 必须声明
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A minimal kernel module example");
MODULE_VERSION("1.0");
🔑 关键点:
  • __init 标记的初始化函数在调用后会被内核自动释放,以节省内存。
  • __exit 标记的清理函数在模块被静态编译进内核时会被忽略。
  • MODULE_LICENSE("GPL") 是必须的,否则内核会标记模块为“污染”(tainted)。
  • printk 是内核空间的“printf”,输出可通过 dmesg 查看。

1.5 模块编译与实验

使用内核构建系统(Kbuild)编译模块,需要准备 Makefile:

obj-m := my_module.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

编译后生成 my_module.ko,执行以下命令验证:

# 加载模块
sudo insmod my_module.ko

# 查看内核日志
dmesg | tail -5

# 确认模块已加载
lsmod | grep my_module

# 卸载模块
sudo rmmod my_module

# 再次查看日志确认卸载
dmesg | tail -5
📌 实验输出示例:
[ 1234.567890] Hello, kernel module loaded!
[ 5678.901234] Goodbye, kernel module unloaded!

1.6 本章小结

  • 内核模块是动态加载到内核空间的代码片段,用于扩展内核功能而不需重启系统。
  • 模块与应用程序在运行空间、入口函数、库支持、崩溃影响等方面有本质区别。
  • 加载流程涉及ELF解析、符号解析、内存分配、重定位和初始化函数调用。
  • 卸载流程依赖引用计数检查,确保模块不再被使用时才释放资源。
  • 最小模块必须包含 module_init、module_exit 和 MODULE_LICENSE。