3. 内核模块基础
3.1 内核模块概念
Linux 内核是一个单体内核(Monolithic Kernel),所有核心功能(进程调度、内存管理、文件系统、网络协议栈)都运行在内核空间。如果每次添加新功能都需要重新编译整个内核并重启系统,开发和调试效率将极低。
内核模块(Kernel Module) 是 Linux 为解决此问题而设计的动态加载机制。模块是一段可以在运行时被插入内核或从内核移除的代码,它扩展了内核的功能,而无需重新编译内核或重启系统。
核心价值: 模块机制使 Linux 内核具备了“可插拔”的灵活性。驱动开发人员可以独立编译、测试一个模块,加载后立即生效,卸载后释放资源,极大缩短了“修改-编译-测试”的迭代周期。
3.2 模块与驱动的区别
在嵌入式 Linux 开发中,“模块”和“驱动”经常被混用,但它们在概念上有明确区分:
| 对比维度 | 内核模块(Module) | 设备驱动(Driver) |
|---|---|---|
| 定义 | 一段可动态加载/卸载的内核代码 | 操作和管理特定硬件设备的软件实体 |
| 范围 | 更广,可以是驱动、文件系统、协议栈、安全策略等 | 特指与硬件交互的软件层 |
| 关系 | 驱动通常以模块形式存在,但模块不一定是驱动 | 驱动可以编译进内核(built-in),也可以编译为模块 |
| 示例 | hello.ko(仅打印日志)、nf_conntrack.ko(网络连接跟踪) |
i2c_designware.ko(I2C 控制器驱动)、usb_storage.ko(USB 存储驱动) |
一句话总结: 模块是“打包和分发”的形式,驱动是“功能和职责”的内容。在字符设备驱动开发中,我们编写的驱动代码最终会被编译成一个
.ko 模块文件。
3.3 模块生命周期
一个内核模块从加载到卸载,经历四个关键阶段。理解这个生命周期是编写正确模块的基础。
3.4 模块操作命令详解
Linux 提供了一组用户空间工具来管理内核模块。以下是四个最核心的命令:
3.4.1 insmod — 加载模块
insmod 将指定的 .ko 文件插入内核。它不会自动解析依赖关系,如果模块依赖其他模块,需要先手动加载依赖。
# 加载一个简单的字符设备驱动模块
sudo insmod hello_driver.ko
# 加载带参数的模块(模块内部需声明参数)
sudo insmod hello_driver.ko debug=1 name="mydevice"
3.4.2 rmmod — 卸载模块
rmmod 从内核中移除一个已加载的模块。如果模块正在被使用(例如设备文件被打开),卸载会失败。
# 卸载模块(只需模块名,不带 .ko 后缀)
sudo rmmod hello_driver
# 强制卸载(危险,可能导致内核崩溃,仅调试使用)
sudo rmmod -f hello_driver
⚠️ 安全提示: 切勿在生产环境中使用
rmmod -f。强制卸载会跳过模块的清理函数,可能导致内存泄漏、内核 panic 或系统不稳定。
3.4.3 lsmod — 列出已加载模块
lsmod 读取 /proc/modules 文件,以表格形式显示当前内核中所有已加载的模块信息。
$ lsmod
Module Size Used by
hello_driver 16384 0
usb_storage 73728 0
i2c_designware 24576 0
输出列含义:
- Module: 模块名称
- Size: 模块占用的内存大小(字节)
- Used by: 被其他模块引用的次数及引用者列表
3.4.4 modinfo — 查看模块信息
modinfo 从 .ko 文件中提取模块的元数据,包括作者、描述、许可证、参数等。
$ modinfo hello_driver.ko
filename: /home/user/hello_driver.ko
license: GPL
author: Zhang San
description: A simple character device driver example
depends:
name: hello_driver
version: 1.0
parm: debug:Enable debug output (int)
parm: name:Device name string (charp)
3.5 模块依赖与自动加载
实际项目中,一个驱动模块可能依赖多个基础模块(如 i2c-core、gpiolib)。手动管理依赖非常繁琐,因此 Linux 提供了更高级的工具:
| 命令 | 功能 | 与 insmod/rmmod 的关系 |
|---|---|---|
modprobe |
智能加载/卸载模块,自动解析并加载依赖 | 推荐替代 insmod 和 rmmod |
depmod |
生成模块依赖关系文件 modules.dep |
modprobe 依赖此文件工作 |
# 使用 modprobe 加载模块(自动处理依赖)
sudo modprobe hello_driver
# 使用 modprobe 卸载模块(仅当无其他模块依赖时)
sudo modprobe -r hello_driver
3.6 模块的入口与出口函数
每个内核模块都必须声明两个宏:module_init() 和 module_exit(),它们定义了模块生命周期的起点和终点。
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, module loaded!\n");
return 0; // 0 表示成功,负数表示失败
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, module unloaded!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Engineer");
MODULE_DESCRIPTION("A minimal kernel module example");
关键点:
__init标记:初始化函数使用的内存,在模块加载完成后会被内核回收,节省内存。__exit标记:如果模块被编译进内核(built-in),此函数会被忽略,因为 built-in 模块不能被卸载。MODULE_LICENSE("GPL")是必须的,缺少许可证声明会导致内核报“tainted”警告。
3.7 本章小结
- 内核模块 是 Linux 动态扩展内核功能的机制,驱动通常以模块形式存在。
- 模块生命周期 分为加载、初始化、运行、卸载四个阶段,由
module_init和module_exit控制。 - insmod/rmmod/lsmod/modinfo 是模块管理的基础命令,
modprobe是更智能的替代方案。 - 编写模块时,必须包含
<linux/module.h>,并声明许可证、作者等元信息。