4. 系统裁剪优化方法论

内核模块裁剪,说白了就是给Linux内核「瘦身」。我刚开始做嵌入式项目时,总觉得内核编译出来多大都无所谓。直到有一次,一个Flash只有8MB的设备,光内核就占了6MB,应用根本塞不进去。嗯,从那以后,我再也不敢忽视模块裁剪了。

这一章,我们重点解决三个问题:哪些模块可以删?怎么删?删完之后怎么配置?

4.1 模块依赖分析——先搞清楚谁依赖谁

裁剪之前,你得先知道内核里都有什么。我见过不少工程师,上来就对着menuconfig一顿乱删,结果编译出来的内核启动到一半就panic了。为什么?因为模块之间有依赖关系,你删了一个,另一个可能就废了。

4.1.1 查看当前内核的模块列表

如果你手头有一个正在运行的内核,可以用以下命令查看已加载的模块:

# 查看已加载模块
lsmod

# 查看模块详细信息
modinfo <module_name>

# 查看模块依赖关系
modprobe --show-depends <module_name>

我个人习惯先用lsmod扫一遍,看看哪些模块是系统启动时就加载的。然后针对每个模块,用modinfo看它的描述、作者、依赖项。这样心里就有底了。

4.1.2 分析模块依赖树

模块依赖不是线性的,而是一棵树。举个例子,你用了USB存储设备,它依赖USB核心驱动、SCSI子系统、文件系统驱动。删掉任何一个,USB存储就用不了。

我建议你用下面这个脚本,把依赖关系导出来看看:

#!/bin/bash
# 导出模块依赖关系
for mod in $(lsmod | awk 'NR>1 {print $1}'); do
    echo "=== $mod ==="
    modprobe --show-depends $mod 2>/dev/null
    echo ""
done

输出结果会告诉你每个模块依赖了哪些其他模块。把这些信息整理成一张表,你就知道哪些模块是「根节点」,哪些是「叶子节点」。叶子节点通常可以放心删,根节点要谨慎。

核心原则:先删叶子节点,再动根节点。删之前,确认这个模块对应的硬件在你的板子上不存在。

4.2 删除无用驱动与子系统——该扔的就扔

嵌入式设备的硬件是固定的。你的板子上没有Wi-Fi芯片,那Wi-Fi驱动就是废的;没有PCIe接口,那PCIe子系统就是累赘。我见过一个项目,内核里编译了40多个网卡驱动,实际只用了一个。你说这得多浪费?

4.2.1 识别无用驱动

怎么判断哪些驱动是无用的?我总结了三个方法:

  1. 看硬件手册——你的板子上有哪些外设,芯片手册里写得清清楚楚。对照着删。
  2. 看设备树——设备树里enable了哪些设备,对应的驱动才需要保留。
  3. 看启动日志——内核启动时会打印drivers/hid、usbcore之类的信息,没出现的子系统基本可以删。

举个例子,我曾经接手过一个项目,内核里编译了CONFIG_TOUCHSCREEN相关的所有驱动,但产品根本没有触摸屏。删掉之后,内核体积直接小了200KB。200KB在嵌入式设备里,可能就是多放一个功能模块的空间。

4.2.2 常见可裁剪的子系统

下面这张表是我在实际项目中总结的,大部分嵌入式设备都可以放心裁剪的子系统:

子系统 裁剪条件 典型节省空间
Sound card drivers 设备无音频输出 300KB - 500KB
GPU/DRI drivers 无显示输出 500KB - 1MB
Network drivers 只保留用到的网卡驱动 200KB - 800KB
USB subsystem 无USB接口 400KB - 600KB
Input subsystem 无键盘/鼠标/触摸 100KB - 200KB
Bluetooth subsystem 无蓝牙模块 200KB - 400KB
Wireless (Wi-Fi) 无Wi-Fi模块 300KB - 500KB
Filesystem drivers 只保留用到的文件系统 100KB - 300KB

注意:裁剪之前,一定要确认你的设备确实不需要这些功能。我曾经因为删了USB子系统,结果发现调试口是USB转串口,导致无法调试。嗯,那次折腾了我一整天。

4.3 配置内核模块为built-in或module

删完无用驱动之后,剩下的模块怎么配置?是编译进内核(built-in),还是编译成可加载模块(module)?这个问题没有标准答案,但有几个原则可以参考。

4.3.1 built-in vs module 的选择策略

我个人的经验是:启动必需的模块编译进内核,非必需的编译成模块。具体来说:

  • built-in(Y):根文件系统驱动、存储控制器驱动、串口驱动、中断控制器驱动、定时器驱动。这些模块在启动初期就需要,不能等文件系统加载后再去加载模块。
  • module(M):网卡驱动、音频驱动、USB设备驱动、输入设备驱动。这些模块可以在系统启动后按需加载,减少内核镜像体积。
  • 不编译(N):前面分析出来的无用驱动和子系统。

举个例子,我曾经做一个路由器项目,内核镜像必须控制在2MB以内。我把文件系统驱动(ext4、jffs2)和网络协议栈编译进内核,把无线网卡驱动、USB驱动、LED驱动编译成模块。最终内核镜像1.8MB,模块包500KB,完美达标。

4.3.2 实际操作:修改内核配置

在menuconfig里,每个选项有三种状态:

  • <*> 或 [*]:编译进内核(built-in)
  • <M> 或 [M]:编译成模块(module)
  • < > 或 [ ]:不编译

我建议你按以下步骤操作:

  1. 执行make menuconfig进入配置界面。
  2. 先全局搜索(按/键)你确定要保留的驱动,确保它们被正确配置。
  3. 然后逐级进入各个子系统,把不需要的驱动设为N。
  4. 对于不确定的驱动,先设为M,编译成模块。如果启动后发现不需要,再删掉。
  5. 保存配置,退出。

小技巧:配置完成后,执行make savedefconfig生成精简的defconfig文件。下次编译时直接使用这个文件,省去重复配置的麻烦。

4.4 实战:模块依赖分析与裁剪流程

说了这么多,我们来走一遍完整的流程。假设你有一个基于ARM Cortex-A7的嵌入式设备,Flash空间有限,需要裁剪内核。

4.4.1 第一步:获取当前配置

# 备份当前配置
cp .config .config.backup

# 查看当前配置中哪些模块被编译了
grep '=y' .config | wc -l
grep '=m' .config | wc -l

我习惯先看看总数。如果=y的选项超过2000个,那肯定有很多可以删的。

4.4.2 第二步:分析硬件需求

对照硬件手册,列出你的设备需要的驱动列表:

  • CPU核心:ARM架构、SMP(多核)
  • 存储:MMC/SD卡控制器、NAND Flash控制器
  • 网络:以太网MAC、PHY芯片
  • 串口:UART
  • 其他:GPIO、I2C、SPI、看门狗

把这些驱动记下来,其他的都可以考虑裁剪。

4.4.3 第三步:逐级裁剪

进入menuconfig,从Device Drivers开始,逐级检查:

Device Drivers --->
    [ ] Network device support --->
        [ ] Ethernet driver support --->
            <*>  你的网卡驱动
            < >  其他网卡驱动(全部设为N)
    [ ] Input device support --->
        < >  Mouse interface
        < >  Touchscreen
        <*>  GPIO buttons(如果设备有物理按键)
    [ ] Sound card support --->
        < >  ALSA(如果设备无音频)

每删完一个子系统,保存配置,然后编译一次。我建议你每删一个子系统就编译一次,这样如果出问题了,能快速定位是哪个子系统导致的。

4.4.4 第四步:验证裁剪结果

编译完成后,检查内核镜像大小和模块数量:

# 查看内核镜像大小
ls -lh arch/arm/boot/zImage

# 查看编译出的模块数量
find . -name '*.ko' | wc -l

# 查看模块总大小
du -sh .

如果内核镜像还是太大,继续回到第三步,进一步裁剪。

我的经验:一个典型的嵌入式Linux内核,经过裁剪后,zImage可以控制在1.5MB到3MB之间。如果超过5MB,说明裁剪得还不够彻底。

4.5 模块依赖关系图

下面这张图展示了模块依赖分析的核心流程,以及built-in/module的选择策略。你可以把它当作裁剪时的参考路线图。

内核模块裁剪决策流程 获取当前内核配置 分析硬件需求与设备树 模块依赖关系分析 判断模块是否必需 否 是 设为 N(不编译) 判断是否启动必需 否 是 设为 M(模块) 启动必需:文件系统、存储、串口等 非必需:网卡、音频、USB等

这张图的核心逻辑很简单:先分析依赖,再判断是否必需,最后决定配置方式。照着这个流程走,基本不会出错。

4.6 避坑指南

裁剪内核模块这件事,说难不难,说简单也不简单。我踩过不少坑,分享几个给你:

坑1:删了启动必需的模块

我曾经把CONFIG_EXT4_FS编译成模块,结果根文件系统是ext4格式的,内核启动时找不到文件系统,直接panic。解决方案:根文件系统驱动必须编译进内核。

坑2:依赖链没分析全

有一次我删了CONFIG_USB_SUPPORT,以为设备没有USB接口。结果发现板子上的调试串口是通过USB转串口芯片连接的,删掉之后连控制台都进不去。解决方案:删之前,用modprobe --show-depends查清楚依赖链。

坑3:模块加载顺序问题

有些模块之间有加载顺序要求。比如usb_storage必须在usbcore之后加载。如果你把依赖模块编译成模块,一定要在init脚本里处理好加载顺序,或者用modprobe自动处理依赖。

嗯,说了这么多,其实核心就一句话:裁剪之前想清楚,裁剪之后验证好。别急着一次删太多,慢慢来,每删一个子系统就编译测试一次。这样即使出问题,也能快速定位。