4、内核裁剪基础:内核模块与内建的区别、文件系统驱动裁剪、网络协议栈裁剪、内存管理选项裁剪、调度器选择
内核裁剪这事儿,说白了就是给你的硬件平台「量身定做」一套操作系统内核。我做了这么多年BSP,见过太多人直接拿个通用内核就往上怼,结果不是镜像太大放不下,就是跑起来卡得要命。今天咱们就把内核裁剪的几个核心点掰开揉碎了讲清楚。
核心原则:内核裁剪不是盲目删代码,而是理解每个选项背后的代价,然后做出取舍。
4.1 内核模块 vs 内建:选哪个?
这是最基础也最容易纠结的问题。我刚开始做BSP时,习惯把所有驱动都编成模块,觉得这样灵活。后来发现,在嵌入式场景下,模块化并不总是最优解。
| 特性 | 内建(built-in) | 模块(module) |
|---|---|---|
| 加载方式 | 随内核一起启动 | 运行时动态加载/卸载 |
| 镜像大小 | 较大(所有代码都在zImage里) | 较小(模块单独存放) |
| 启动速度 | 快(无需额外加载步骤) | 慢(需文件系统支持,加载耗时) |
| 内存占用 | 常驻内存,不可回收 | 按需加载,可卸载释放内存 |
| 调试便利性 | 需重新编译烧写整个内核 | 单独编译模块,insmod/rmmod即可 |
| 适用场景 | 根文件系统必需的驱动、启动早期设备 | 外设驱动、调试阶段、可选功能 |
我的建议:
- 根文件系统驱动(如存储控制器、MMC/SD卡驱动)必须内建。否则内核启动到挂载根文件系统时,驱动还没加载,直接panic。
- 网络协议栈、调度器、内存管理这些核心子系统,必须内建。它们不是「可选功能」。
- 外设驱动(Wi-Fi、蓝牙、音频Codec、传感器)做成模块。我在项目中遇到过,把Wi-Fi驱动内建后,每次改个参数都要重烧整个内核,太痛苦了。
小技巧:调试阶段多用模块,量产阶段把稳定驱动内建。这样可以兼顾开发效率和最终性能。
4.2 文件系统驱动裁剪
文件系统这块,很多人容易犯「贪多」的毛病。我记得有个项目,同事把ext2/3/4、btrfs、xfs、f2fs全编进去了,结果内核镜像大了快2MB。对于只有8MB Flash的IoT设备来说,这简直是灾难。
怎么选?
- 只保留你需要的文件系统类型。 比如你的根文件系统是squashfs(只读压缩),那就只保留squashfs支持,其他全关掉。
- 注意文件系统特性选项。 比如ext4的加密、日志、扩展属性,如果不用就关掉。这些选项会引入额外的代码路径和内存开销。
- 网络文件系统(NFS/CIFS) 调试阶段很有用,量产时建议关掉。我曾经在量产固件里忘了关NFS客户端,结果被安全团队通报了漏洞。
避坑指南:我曾经在裁剪文件系统驱动时,把「内核自动挂载根文件系统」需要的驱动也删了。结果内核启动到一半,直接打印「VFS: Unable to mount root fs」。排查了半天才发现,是忘了把对应的块设备驱动内建。记住:根文件系统驱动必须内建,且必须正确配置。
4.3 网络协议栈裁剪
网络协议栈是内核里最庞大的子系统之一。你想想看,一个完整的TCP/IP协议栈,加上IPv6、IPsec、Netfilter、Bridge、VLAN、MPLS……全开的话,光代码量就够你喝一壶的。
裁剪思路:
- IPv6: 如果你的设备只跑IPv4网络,果断关掉IPv6。很多IoT设备根本不需要IPv6,开着只会增加内存占用和攻击面。
- Netfilter/iptables: 如果设备不需要防火墙功能,关掉。我见过一个智能家居网关,明明没有防火墙需求,却开着完整的Netfilter,白白浪费了几百KB内存。
- 网络设备驱动: 只保留你硬件上有的网卡驱动。比如你的板子用的是DM9000,那就只开DM9000驱动,其他全关。
- 协议选项: TCP拥塞控制算法,默认有cubic、reno、bbr等。如果你不需要BBR,只保留cubic就够了。
经验之谈:网络协议栈裁剪时,建议先开一个最小配置(只保留IPv4 + TCP/UDP + 你需要的网卡驱动),然后逐步添加功能。每加一个选项,都编译一次,看看镜像大小和内存占用变化。这样你能清楚知道每个选项的代价。
4.4 内存管理选项裁剪
内存管理是内核的「心脏」,但很多选项其实跟你的硬件平台无关。比如,你的板子只有64MB内存,那就不需要支持「大内存页(HugeTLB)」或「内存热插拔」。
常见可裁剪选项:
| 选项 | 说明 | 裁剪建议 |
|---|---|---|
| HugeTLB | 大内存页支持 | 内存小于256MB时建议关闭 |
| Memory hotplug | 内存热插拔 | 嵌入式设备几乎用不到,关闭 |
| Transparent Hugepage | 透明大页 | 对实时性有影响,建议关闭 |
| CMA(Contiguous Memory Allocator) | 连续内存分配器 | 如果不用摄像头/DMA,可以关闭 |
| Memory compaction | 内存压缩 | 内存紧张时有用,但会增加CPU开销 |
| OOM killer | 内存耗尽时杀进程 | 建议保留,但可以调整触发阈值 |
我的做法: 先看硬件规格书,确定内存大小和特性。然后对照内核的Memory Management选项,把明显用不到的关掉。比如,我做过一个基于Cortex-M4的RTOS+Linux双系统项目,内存只有32MB,我直接把HugeTLB、Memory hotplug、Transparent Hugepage全关了,内核镜像小了将近300KB。
4.5 调度器选择
调度器决定了系统如何分配CPU时间。Linux内核支持多种调度器,但嵌入式场景下,不是所有调度器都合适。
主流调度器对比:
| 调度器 | 特点 | 适用场景 |
|---|---|---|
| CFS(完全公平调度器) | 默认调度器,公平分配CPU时间 | 通用嵌入式系统 |
| RT(实时调度器) | 支持FIFO/RR优先级调度 | 需要实时响应的系统 |
| Deadline调度器 | 基于截止时间的调度 | 多媒体、工业控制 |
| BFS/MuQSS | 桌面交互优化 | 嵌入式场景不推荐 |
我的选择:
- 大多数嵌入式Linux设备,用CFS就够了。它稳定、成熟、文档多。
- 如果设备有实时性要求(比如电机控制、音频处理),建议开启RT调度器,并把关键线程设置为SCHED_FIFO或SCHED_RR。
- Deadline调度器虽然强大,但配置复杂。我只有在做工业相机项目时才用过,普通场景没必要碰。
注意: 开启RT调度器后,一定要小心优先级反转问题。我曾经在一个项目中,把高优先级线程设置为SCHED_FIFO,结果它占着CPU不放,导致低优先级的中断处理线程饿死。后来加了优先级继承锁才解决。
4.6 本章知识体系总览
下面这张图,把内核裁剪的核心逻辑串起来了。你可以把它当作裁剪时的「检查清单」。
这张图把内核裁剪分成了五个维度:模块与内建的选择、文件系统驱动、网络协议栈、内存管理、调度器。每个维度都有明确的裁剪方向和注意事项。我个人习惯在开始裁剪前,先画一张类似的图,把硬件需求和内核选项对应起来,这样不容易漏掉关键配置。
最后说一句:内核裁剪不是一锤子买卖。硬件升级、需求变更、安全补丁,都会让你重新审视之前的裁剪决策。保持配置文件的版本管理,每次裁剪都记录原因和效果,这样后续维护时才不会抓瞎。