一、内核模块初探:什么是内核模块?为什么需要模块化?
大家好,欢迎来到这门课。我是你们的老朋友,一个在 Linux 内核和嵌入式领域摸爬滚打了十几年的工程师。
今天咱们聊聊内核模块。说白了,内核模块就是一段可以在系统运行时,动态加载到内核里去的代码。它不像传统的驱动程序那样,必须把代码写死在内核里,然后重新编译整个内核才能生效。
我刚开始接触 Linux 内核时,最头疼的就是每次改个驱动都要重新编译内核,然后重启机器。那会儿我还在用一台老旧的奔腾机器,编译一次内核要等将近一个小时。后来知道了内核模块这个东西,感觉就像发现了新大陆——改完代码,make modules,然后 insmod 加载,几秒钟就搞定了。
1.1 什么是内核模块?
内核模块,英文叫 Loadable Kernel Module (LKM)。它本质上是一个目标文件(.ko 文件),包含了可以在内核空间运行的代码和数据。
你可以把它想象成乐高积木。内核是底座,模块就是一块块积木。你需要什么功能,就插上对应的积木。不需要了,随时拔下来。
核心特征:
- 动态加载/卸载:运行时通过
insmod加载,rmmod卸载 - 内核空间运行:拥有最高权限,可以访问所有硬件和内存
- 无需重新编译内核:模块与内核版本需匹配,但不需要重新编译整个内核
- 独立文件:每个模块是一个独立的 .ko 文件
举个例子,你写了一个简单的 "Hello World" 模块:
// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Kernel Module!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel Module!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module");
编译后生成 hello.ko,然后:
# insmod hello.ko # 加载模块
# rmmod hello.ko # 卸载模块
# dmesg | tail # 查看内核日志
嗯,就这么简单。但别小看这个例子,它包含了内核模块的所有核心要素:入口函数、出口函数、许可证声明。
1.2 为什么需要模块化?
你可能会问:为什么不直接把所有功能都编译进内核?非要搞个模块出来?
原因其实很现实。我当年在一个嵌入式项目里,需要支持几十种不同的外设。如果全部编译进内核,内核镜像会变得非常庞大。而且,很多外设用户根本用不到,白白浪费了内存。
模块化带来的好处,我总结了几点:
| 优势 | 说明 | 我的亲身经历 |
|---|---|---|
| 减小内核体积 | 只加载需要的功能,内核镜像更小 | 嵌入式设备 Flash 只有 4MB,模块化是唯一选择 |
| 加快开发速度 | 修改模块代码后,无需重启系统 | 调试驱动时,一天要加载卸载上百次模块 |
| 按需加载 | 系统启动时只加载必要模块,其他按需加载 | USB 设备插入时自动加载对应驱动模块 |
| 方便升级 | 单独升级某个模块,不影响整个内核 | 安全补丁只需替换有漏洞的模块 |
| 节省内存 | 不用的模块可以卸载,释放内存 | 服务器上卸载不用的文件系统模块 |
小提示: 我个人习惯在开发阶段把所有驱动都做成模块。等稳定了,再把那些必须的、频繁使用的驱动编译进内核。这样既保证了开发效率,又保证了生产环境的稳定性。
1.3 模块与内建驱动的对比
这里有个关键问题:什么时候用模块,什么时候用内建驱动?
内建驱动,就是直接编译进内核镜像(vmlinuz)里的驱动。它们在系统启动时就被加载,永远存在于内核中。
我画了一张图,帮你直观理解两者的区别:
这张图很直观。左边是内建驱动,右边是模块。你想想看,如果你的网卡驱动是内建的,那系统启动时就必须加载它。但如果你的网卡是 USB 外接的,可能一个月才用一次,做成模块显然更合理。
1.4 什么时候用模块?什么时候用内建?
我根据自己的经验,给你一个实用的选择指南:
避坑指南: 我曾经在一个嵌入式路由器项目里,把根文件系统(ext4)的驱动做成了模块。结果系统启动时,内核找不到根文件系统,直接 panic 了。后来才意识到,根文件系统驱动必须是内建的,否则内核无法挂载根分区。
具体选择标准:
- 必须内建的情况:
- 根文件系统驱动(ext4、btrfs 等)
- 内存管理相关代码
- 进程调度器核心
- 系统启动时必须的硬件驱动(如串口)
- 适合模块的情况:
- 大多数外设驱动(网卡、声卡、显卡)
- 文件系统(如 NTFS、FAT 等非根文件系统)
- 网络协议(如 IPsec、Netfilter)
- 调试工具和测试代码
说白了,判断标准就一条:系统启动时没有它能不能活? 如果能,就做成模块;如果不能,就内建。
1.5 模块的加载与卸载机制
最后,简单说一下模块是怎么工作的。当你执行 insmod hello.ko 时,内核会做以下几件事:
- 文件读取:从文件系统读取 .ko 文件到内核空间
- 符号解析:解析模块中引用的内核符号(函数、变量)
- 内存分配:为模块分配内核内存空间
- 执行初始化:调用
module_init指定的函数 - 注册模块:将模块信息注册到内核模块链表
卸载时,rmmod 会调用 module_exit 函数,然后释放内存。
小技巧: 我调试模块时,经常用 modprobe 代替 insmod。因为 modprobe 会自动处理模块依赖关系。比如你加载一个 USB 驱动模块,它会自动先加载 USB 核心模块。