Linux设备驱动开发与内核模块动态部署 - 第2节

2.1 内核模块概念

Linux 内核模块(Kernel Module)是一段可以在运行时动态加载到内核或从内核卸载的代码片段。它允许在不重新编译整个内核、不重启系统的情况下扩展内核功能。模块机制是 Linux 内核可扩展性的核心支柱之一,也是设备驱动最常用的部署形式。

核心定义: 内核模块本质上是目标文件(.ko 文件),包含初始化/退出函数、符号导出表以及依赖信息。它运行在内核空间,拥有最高特权级(Ring 0)。

2.1.1 模块的物理形态

一个编译完成的内核模块通常以 .ko(Kernel Object)为后缀,存放在 /lib/modules/$(uname -r)/ 目录下。使用 file 命令可以查看其 ELF 格式信息:

$ file hello.ko
hello.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=..., not stripped

模块文件包含以下关键段:

  • .init.text — 模块初始化函数代码段,加载时执行一次后释放。
  • .exit.text — 模块退出函数代码段,卸载时执行。
  • .modinfo — 模块信息段,包含作者、描述、许可证、依赖等元数据。
  • .symtab / .strtab — 符号表和字符串表,用于符号导出与解析。
  • .modversions — 版本校验信息,确保模块与当前内核 ABI 兼容。

2.1.2 模块的依赖关系

模块之间可以存在依赖。例如,USB 存储驱动依赖于 SCSI 核心模块。依赖信息通过 depmod 工具生成,存储在 modules.dep 文件中:

$ depmod -a
$ cat /lib/modules/$(uname -r)/modules.dep | grep usb-storage
kernel/drivers/usb/storage/usb-storage.ko: kernel/drivers/scsi/scsi_mod.ko kernel/drivers/usb/core/usbcore.ko
提示: 使用 modinfo -F depends hello.ko 可以快速查看模块的直接依赖列表,无需手动解析 modules.dep。
内核模块文件结构(.ko ELF 格式) hello.ko 模块文件 .init.text .exit.text .modinfo .symtab .strtab .modversions .data / .bss .rodata 每个段在内核加载时被映射到不同的内存区域,.init.text 在初始化完成后可被释放

2.2 模块与应用程序的区别

内核模块虽然也是 C 语言编写的二进制程序,但与用户空间的应用程序存在本质差异。理解这些差异是编写正确驱动的前提。

对比维度 内核模块 用户空间应用程序
运行空间 内核空间(Ring 0) 用户空间(Ring 3)
入口函数 module_init() / module_exit() main()
内存访问 直接访问物理内存、I/O 端口 虚拟地址空间,受 MMU 保护
标准库 不可使用 libc(如 printf、malloc) 可链接任意用户态库
调试方式 printk / ftrace / kdump / kgdb gdb / strace / valgrind
错误影响 内核 panic / 系统崩溃 进程 segfault / 可恢复
浮点运算 默认禁用,需手动保存 FPU 状态 正常使用
链接方式 动态链接到内核符号表 静态/动态链接到用户态库
生命周期 由 insmod/rmmod 或内核自动管理 由 shell 或进程管理器管理
关键警告: 模块运行在内核空间,一个空指针解引用就会导致整个系统崩溃。必须使用内核专用 API(如 kmalloc、printk),绝不能调用 malloc、printf 等用户态函数。

2.2.1 内核专用 API 示例

以下对比展示模块与应用程序在内存分配和输出上的差异:

/* 用户空间应用程序 */
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    char *buf = malloc(1024);
    printf("buf = %p\n", buf);
    free(buf);
    return 0;
}

/* 内核模块 */
#include <linux/module.h>
#include <linux/slab.h>

static int __init my_init(void) {
    char *buf = kmalloc(1024, GFP_KERNEL);
    printk(KERN_INFO "buf = %p\n", buf);
    kfree(buf);
    return 0;
}
module_init(my_init);
内核模块 vs 用户空间应用程序:运行环境对比 用户空间 (Ring 3) 应用程序 libc 库 系统调用接口 (syscall) 内核空间 (Ring 0) 内核模块 内核核心 硬件抽象层 / 设备驱动框架 系统调用边界 — 权限隔离 syscall

2.3 模块生命周期

内核模块从加载到卸载经历四个明确的阶段。每个阶段都有对应的内核回调函数和状态转换。理解生命周期是正确编写初始化/退出逻辑的基础。

2.3.1 生命周期四阶段

  1. 加载阶段(Loading) — 通过 insmod 或 modprobe 将 .ko 文件读入内核,解析依赖,分配内存,执行模块初始化函数。
  2. 运行阶段(Running) — 模块初始化成功后进入运行态,提供设备操作接口(如 file_operations),响应系统调用或硬件中断。
  3. 卸载阶段(Unloading) — 通过 rmmod 触发模块退出函数,释放资源,注销设备,从内核符号表移除。
  4. 释放阶段(Cleanup) — 内核回收模块占用的所有内存和系统资源,模块完全消失。

2.3.2 关键回调函数

回调函数 触发时机 典型职责 返回值
module_init() 模块加载时自动调用 注册设备、分配资源、初始化硬件 0 成功 / 负值失败
module_exit() 模块卸载时自动调用 注销设备、释放内存、关闭硬件 void
__init 标记 初始化函数执行后释放代码段 标记初始化专用函数,节省内存 —
__exit 标记 仅模块卸载时编译进内核 标记退出函数,非卸载时丢弃 —
最佳实践: 使用 __init 和 __exit 宏标记初始化/退出函数。当模块被静态编译进内核时,__exit 标记的函数会被完全丢弃,减少内核映像体积。

2.3.3 生命周期状态机

内核模块生命周期状态机 未加载 .ko 文件在磁盘 加载中 执行 init 函数 运行中 提供服务 卸载中 执行 exit 函数 已释放 资源回收完成 insmod init 成功 rmmod exit 完成 init 失败 模块引用计数为 0 时才能进入卸载阶段;若设备正被使用,rmmod 会返回 EBUSY

2.3.4 引用计数机制

内核使用引用计数(reference count)防止模块在正被使用时被卸载。每个模块维护一个 refcnt 字段:

  • 当有进程打开模块管理的设备文件时,引用计数 +1。
  • 当进程关闭设备文件时,引用计数 -1。
  • 只有引用计数为 0 时,rmmod 才能成功卸载模块。
# 查看模块引用计数
$ lsmod | grep hello
hello                  16384  0          # 第三列 0 表示引用计数为 0

# 打开设备后引用计数变为 1
$ cat /dev/hello &
$ lsmod | grep hello
hello                  16384  1          # 引用计数为 1,无法卸载

# 卸载尝试会失败
$ rmmod hello
rmmod: ERROR: Module hello is in use
内核实现: 现代内核(5.x+)使用 try_module_get() 和 module_put() 原子操作管理引用计数。驱动开发者通常不需要手动调用,VFS 层会在 open()/release() 时自动处理。

2.3.5 模块加载/卸载命令速查

命令 功能 自动处理依赖 常用选项
insmod hello.ko 直接加载模块 否 —
modprobe hello 加载模块及其依赖 是 -r 卸载,--show-depends 查看依赖
rmmod hello 卸载模块 否 -f 强制卸载(危险)
modinfo hello.ko 查看模块元信息 — -p 查看参数,-F depends 查看依赖
lsmod 列出所有已加载模块 — 输出:模块名、大小、引用计数、依赖

2.4 本章小结

  • 内核模块是 .ko 格式 的 ELF 目标文件,包含初始化/退出代码段、符号表和版本信息。
  • 模块与应用程序在 运行空间、入口函数、库支持、错误影响 等方面存在根本差异。
  • 模块生命周期分为 加载 → 运行 → 卸载 → 释放 四个阶段,由 module_init 和 module_exit 回调驱动。
  • 引用计数机制确保模块在正被使用时不会被意外卸载,lsmod 的第三列显示当前引用计数。
  • 使用 insmod/modprobe/rmmod 管理模块,modprobe 会自动处理依赖关系。