Linux内核驱动实战 驱动文件操作与VFS层交互 - 第1节

1. Linux内核模块基础

Linux内核模块(Kernel Module)是一段可以在运行时动态加载到内核或从内核卸载的代码片段。它允许在不重新编译整个内核、不重启系统的情况下扩展内核功能。驱动开发者通常以模块形式编写设备驱动,使其具备高度的灵活性和可维护性。

核心优势: 模块化设计使内核保持精简,同时支持按需加载驱动、文件系统、网络协议等扩展功能。

模块与静态编译的对比

对比维度 内核模块(动态) 静态编译(内置)
加载方式 运行时通过 insmod/modprobe 加载 编译时链接进内核镜像
卸载方式 运行时通过 rmmod 卸载 无法卸载,始终存在于内存
内核体积 按需加载,节省内存 所有功能固定占用内存
开发调试 快速迭代,无需频繁重启 每次修改需重新编译并重启
适用场景 驱动、文件系统、网络协议 核心调度、内存管理、必选组件
Linux内核模块加载/卸载流程 用户空间 系统调用层 内核模块加载器 insmod/rmmod sys_init_module 分配内核内存 复制模块代码 执行 module_init() 说明: 模块加载时,内核先分配内存并复制模块二进制,然后解析符号表、执行 module_init() 初始化函数。 卸载时执行 module_exit() 清理函数,释放资源并移除模块。

1.2 模块加载与卸载

Linux提供一组用户态工具和系统调用来管理内核模块的生命周期。驱动开发者必须掌握这些操作,以便在开发和部署阶段灵活控制模块。

常用模块管理命令

命令 功能 典型用法
insmod 加载指定模块文件到内核 insmod hello.ko
rmmod 从内核卸载指定模块 rmmod hello
modprobe 智能加载模块(自动处理依赖) modprobe hello
lsmod 列出当前已加载的所有模块 lsmod
modinfo 查看模块的详细信息 modinfo hello.ko
开发建议: 调试阶段优先使用 insmod 和 rmmod 手动控制;生产环境推荐 modprobe 自动处理模块依赖。

加载与卸载的完整流程

  1. 编译模块: 使用内核构建系统生成 .ko 文件。
  2. 加载模块: 执行 insmod hello.ko,内核调用 module_init 指定的初始化函数。
  3. 验证加载: 执行 lsmod | grep hello 确认模块已加载。
  4. 卸载模块: 执行 rmmod hello,内核调用 module_exit 指定的清理函数。
  5. 验证卸载: 再次执行 lsmod 确认模块已移除。

1.3 module_init / module_exit 宏

这两个宏是内核模块的入口和出口标记。它们告诉内核在加载和卸载模块时应调用哪个函数。理解其底层机制对驱动开发至关重要。

宏定义与作用

宏 原型 作用
module_init() module_init(x) 将函数 x 标记为模块初始化入口,加载时自动调用
module_exit() module_exit(x) 将函数 x 标记为模块清理出口,卸载时自动调用

标准模块代码框架

#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 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("Your Name");
MODULE_DESCRIPTION("A simple hello world kernel module");
关键点:
  • __init 标记的初始化函数在调用后会被内核释放,以节省内存。
  • __exit 标记的清理函数在模块编译进内核时会被忽略。
  • 初始化函数返回非零值表示加载失败,内核会回滚加载操作。
module_init / module_exit 执行时序 insmod hello.ko module_init(hello_init) 模块进入运行态 rmmod hello module_exit(hello_exit) 模块从内核移除 注意: 如果 module_init 返回非0值,内核不会调用 module_exit,因为模块被认为加载失败。 module_exit 必须能够正确清理所有已分配的资源,否则会导致内核内存泄漏。

1.4 LICENSE 声明

每个内核模块必须包含许可证声明,这不仅是法律要求,也直接影响模块可使用的内核符号范围。Linux内核使用GPL兼容许可证来保护开源生态。

常见许可证声明

宏 许可证 符号访问权限
MODULE_LICENSE("GPL") GNU General Public License v2 可访问所有内核符号(包括 EXPORT_SYMBOL_GPL)
MODULE_LICENSE("GPL v2") GPL v2 专用 完全访问权限
MODULE_LICENSE("Dual BSD/GPL") BSD 或 GPL 双许可 完全访问权限
MODULE_LICENSE("Proprietary") 专有许可证 无法访问 EXPORT_SYMBOL_GPL 标记的符号
重要提醒: 如果模块使用非GPL许可证,将无法调用内核中标记为 EXPORT_SYMBOL_GPL 的函数和变量。这可能导致驱动功能受限或无法编译通过。商业驱动通常需要联系内核社区或使用专用接口。

其他常用模块信息宏

MODULE_AUTHOR("Author Name");           // 作者信息
MODULE_DESCRIPTION("Module description"); // 模块功能描述
MODULE_VERSION("1.0");                  // 版本号
MODULE_ALIAS("alias-name");             // 模块别名,用于自动加载
MODULE_DEVICE_TABLE(table);             // 设备ID表,用于热插拔

1.5 完整模块示例与编译

下面给出一个完整的、可直接编译运行的模块示例,包含所有必要元素。

示例代码:hello.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init my_driver_init(void)
{
    printk(KERN_INFO "MyDriver: Initializing hardware...\n");
    // 在这里进行硬件初始化、注册设备、分配资源等操作
    return 0;
}

static void __exit my_driver_exit(void)
{
    printk(KERN_INFO "MyDriver: Shutting down hardware...\n");
    // 在这里进行资源释放、设备注销等清理操作
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Driver Expert");
MODULE_DESCRIPTION("A practical example of kernel module");
MODULE_VERSION("1.0.0");

Makefile 构建脚本

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 查看清理信息。
调试技巧: 使用 dmesg -w 实时监控内核日志,可以立即看到模块加载/卸载时的打印信息。配合 printk 的日志级别(KERN_INFO、KERN_DEBUG等)可以过滤不同重要性的消息。
模块开发完整工作流 编写 .c 源码 编写 Makefile make 编译 insmod 加载 dmesg 验证 rmmod 卸载 dmesg 验证 循环迭代:修改源码 → 重新 make → rmmod 旧模块 → insmod 新模块 → dmesg 查看效果,无需重启系统。

1.6 常见问题与避坑指南

问题 原因 解决方案
insmod 报错 "Invalid module format" 模块编译内核版本与运行内核版本不一致 确保 KDIR 指向当前运行内核的构建目录
rmmod 报错 "Module is in use" 有其他进程或模块正在使用该模块 使用 lsmod 查看依赖,先卸载依赖模块
模块加载后无任何日志输出 printk 日志级别高于当前控制台级别 使用 dmesg 查看所有日志,或降低 printk 级别
编译报错 "implicit declaration of function" 缺少必要的头文件 检查并包含 linux/module.h、linux/kernel.h 等
模块加载后系统崩溃或死机 初始化函数中存在非法内存访问或空指针 使用 BUG_ON() 和 WARN_ON() 辅助调试
安全警告: 内核模块运行在特权级别,任何错误都可能导致系统崩溃、数据损坏或安全漏洞。在开发阶段务必在虚拟机或测试机上验证,切勿直接在生产环境调试未经测试的模块。