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 本章知识体系总览

下面这张图,把内核裁剪的核心逻辑串起来了。你可以把它当作裁剪时的「检查清单」。

内核裁剪知识体系 内核裁剪核心决策树 内核裁剪 模块 vs 内建 内建:启动必需、常驻内存 模块:按需加载、调试灵活 文件系统驱动 只保留需要的FS类型 关闭不需要的特性选项 网络协议栈 IPv6/Netfilter按需关闭 只保留需要的网卡驱动 内存管理选项 HugeTLB/热插拔/CMA 按硬件规格裁剪 调度器选择 CFS通用 / RT实时 Deadline高级场景 目标:最小镜像 + 最优性能 + 稳定运行

这张图把内核裁剪分成了五个维度:模块与内建的选择、文件系统驱动、网络协议栈、内存管理、调度器。每个维度都有明确的裁剪方向和注意事项。我个人习惯在开始裁剪前,先画一张类似的图,把硬件需求和内核选项对应起来,这样不容易漏掉关键配置。

最后说一句:内核裁剪不是一锤子买卖。硬件升级、需求变更、安全补丁,都会让你重新审视之前的裁剪决策。保持配置文件的版本管理,每次裁剪都记录原因和效果,这样后续维护时才不会抓瞎。