Linux内核驱动开发实战 从字符设备到设备树 - 第2节

2. 内核模块基础

Linux 内核模块(Kernel Module)是一段可以在运行时动态加载到内核或从内核卸载的代码片段。它允许在不重新编译整个内核、不重启系统的情况下扩展内核功能。模块机制是 Linux 内核可扩展性的核心基石。

核心思想:模块将内核从“静态大内核”转变为“动态可组合内核”。驱动、文件系统、网络协议等都可以作为模块独立开发、独立加载。

2.1.1 模块与静态编译的对比

对比维度 内核模块(.ko) 静态编译(built-in)
加载方式 运行时动态加载/卸载 内核启动时自动包含
灵活性 高,可按需加载,节省内存 低,所有功能固定在内核中
调试与开发 方便,可快速迭代测试 需重新编译内核并重启
启动速度 不影响启动时间(除非自动加载) 启动时全部初始化,可能变慢
安全性 需签名验证机制防止恶意模块 代码固化在内核中
典型场景 设备驱动、文件系统、网络协议 核心调度、内存管理、必用驱动

2.1.2 模块的组成结构

一个标准的内核模块至少包含以下要素:

  • 入口函数:模块加载时执行,通常用于注册设备、分配资源。
  • 出口函数:模块卸载时执行,用于释放资源、注销设备。
  • 许可证声明:声明模块使用的开源许可证,影响内核符号导出。
  • 作者与描述信息:可选但推荐,便于维护和识别。
提示:模块运行在内核空间,拥有最高权限。一个错误的模块可能导致系统崩溃(Oops 或 Kernel Panic),开发时建议在虚拟机或开发板上测试。
Linux 内核模块组成结构 hello.ko 模块文件 入口函数 module_init() 出口函数 module_exit() 许可证声明 MODULE_LICENSE() 作者/描述信息 MODULE_AUTHOR() 模块生命周期 insmod → 入口函数执行 → 模块注册成功 → 内核运行 rmmod → 出口函数执行 → 资源释放 → 模块从内核移除

2.2 模块加载与卸载

2.2.1 用户态工具

Linux 提供了一组用户态工具来管理内核模块:

命令 功能 常用选项
insmod 加载指定模块文件 无依赖解析,需手动处理依赖
rmmod 卸载指定模块 -f 强制卸载(危险)
modprobe 智能加载/卸载,自动处理依赖 -r 卸载,--show-depends 查看依赖
lsmod 列出当前已加载的所有模块 显示模块大小、被引用次数
modinfo 查看模块详细信息 显示作者、许可证、参数、依赖

2.2.2 加载与卸载流程

  1. 加载流程:用户执行 insmod hello.ko → 内核调用 init_module() 系统调用 → 将模块二进制复制到内核空间 → 执行模块入口函数 → 模块状态变为“活动”。
  2. 卸载流程:用户执行 rmmod hello → 内核调用 delete_module() 系统调用 → 检查模块引用计数是否为 0 → 执行模块出口函数 → 释放模块占用的内存。
注意:如果模块被其他模块依赖(引用计数 > 0),则无法卸载。使用 lsmod 查看 Used by 列可确认依赖关系。强制卸载可能导致内核崩溃。
模块加载与卸载流程 加载流程 (insmod) 用户执行 insmod 内核系统调用 执行入口函数 模块状态激活 卸载流程 (rmmod) 用户执行 rmmod 检查引用计数 执行出口函数 释放内核内存

2.3 module_init 与 module_exit 宏

module_init() 和 module_exit() 是内核提供的两个关键宏,用于指定模块的入口和出口函数。它们隐藏了内核模块注册的底层细节,是驱动开发者最常使用的接口。

2.3.1 宏定义与作用

宏 原型 作用 执行时机
module_init() module_init(x) 将函数 x 标记为模块入口 模块加载时自动调用
module_exit() module_exit(x) 将函数 x 标记为模块出口 模块卸载时自动调用

2.3.2 完整示例代码

以下是一个最简单的内核模块,展示了 module_init 和 module_exit 的用法:

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

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Kernel Module!\n");
    printk(KERN_INFO "Module loaded successfully.\n");
    return 0;  // 0 表示成功,负数表示失败
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, Kernel Module!\n");
    printk(KERN_INFO "Module unloaded.\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Driver Developer");
MODULE_DESCRIPTION("A simple Hello World kernel module");
MODULE_VERSION("1.0");
关键点:
  • __init 标记:提示内核该函数仅在初始化阶段使用,执行后可释放内存。
  • __exit 标记:提示内核该函数仅在模块卸载时使用,如果模块静态编译则可忽略。
  • 入口函数返回 int:0 表示成功,负值(如 -ENOMEM)表示失败,模块加载将中止。
  • 出口函数返回 void:卸载操作不允许失败。

2.4 LICENSE 与作者信息声明

2.4.1 许可证声明

MODULE_LICENSE() 宏用于声明模块使用的开源许可证。内核根据许可证决定是否允许模块使用某些“仅 GPL 兼容”的内核符号。

许可证字符串 含义 是否兼容 GPL
"GPL" GNU General Public License v2 是
"GPL v2" GPL 第二版 是
"GPL and additional rights" GPL 附加权限 是
"Dual BSD/GPL" 双许可证 BSD 或 GPL 是
"Dual MPL/GPL" 双许可证 MPL 或 GPL 是
"Proprietary" 专有许可证 否
重要:如果模块声明为 "Proprietary",则无法调用使用 EXPORT_SYMBOL_GPL() 导出的内核符号。许多核心内核接口(如内存管理、文件系统操作)都标记为 GPL-only,专有模块将无法使用。

2.4.2 作者与描述信息

内核提供了多个信息声明宏,用于在模块二进制文件中嵌入元数据:

  • MODULE_AUTHOR("name <email>"):声明模块作者。
  • MODULE_DESCRIPTION("description"):简要描述模块功能。
  • MODULE_VERSION("version"):模块版本号。
  • MODULE_ALIAS("alias"):为模块创建别名,便于自动加载。
  • MODULE_DEVICE_TABLE(type, table):声明设备表,用于热插拔自动加载。
模块信息声明与查看 模块源码中的声明 MODULE_LICENSE("GPL") MODULE_AUTHOR("Alice <alice@example.com>") MODULE_DESCRIPTION("USB Ethernet Driver") modinfo 命令输出 license: GPL author: Alice <alice@example.com> description: USB Ethernet Driver

2.5 模块编译与 Makefile

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

# Makefile for hello kernel module
obj-m := hello.o

# 内核源码路径(根据实际环境修改)
KDIR := /lib/modules/$(shell uname -r)/build

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

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

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean
编译与测试步骤:
  1. 将 hello.c 和 Makefile 放在同一目录。
  2. 执行 make 编译,生成 hello.ko。
  3. 执行 sudo insmod hello.ko 加载模块。
  4. 执行 dmesg | tail 查看内核日志输出。
  5. 执行 sudo rmmod hello 卸载模块。
  6. 再次执行 dmesg | tail 查看卸载日志。

2.6 本章小结

  • 内核模块是 Linux 动态扩展机制的核心,允许运行时加载/卸载驱动和功能。
  • module_init() 和 module_exit() 是模块的入口和出口,必须成对出现。
  • MODULE_LICENSE() 决定模块能否使用 GPL-only 内核符号,专有模块受限严重。
  • 作者、描述、版本等信息声明有助于模块管理和维护。
  • 使用 insmod/rmmod/modprobe/lsmod/modinfo 管理模块生命周期。
  • Makefile 通过内核构建系统编译模块,obj-m 指定目标模块名。