简介:面向Linux系统运维与嵌入式开发人员的内核裁剪实操文档,围绕menuconfig逐项演示如何精简内核功能,解决系统体积偏大、启动与响应变慢的问题。文档按步骤清单展开,覆盖代码成熟度选项、General setup、可加载模块、块设备层、处理器类型、电源管理以及抢占式内核等关键配置区域,并结合System V IPC、ACPI、主板按钮事件、内核抢占等概念说明每个选项的取舍依据,避免盲目裁剪导致系统无法启动;同时补充了超线程/多核调度、机器检查异常等进阶判断思路,帮助读者根据硬件与应用场景选择合适的配置策略。文档不仅列出删除项,还解释了为何保留System V IPC与模块加载支持,以及ACPI、抢占式调度对系统响应的影响;读者按步骤操作后可得到体积更小、运行更流畅的精简内核。资源为单个docx文档,大小约80KB,已有237人学习浏览。适合刚接触内核编译的初学者逐步实践,也可作为后续内核配置与性能调优的速查手册。
1. linux内核裁剪:它解决什么,你该不该动这个手
linux内核裁剪的完整过程,本质上不是“删代码”,而是通过 Kconfig 配置体系把用不到的子系统、驱动、文件系统支持关掉,让编出来的内核镜像更小、模块集更干净、启动路径更短,同时缩小攻击面。很多人第一次做裁剪,以为只要打开 make menuconfig 把不认识的选项全部取消就能成功,结果往往是剪完起不来、模块加载报错,甚至配置一改就是半天找不回原因。这篇文章会把具体过程按「准备 → 动手 → 编译 → 排查 → 验证」拆开讲,覆盖常用命令、关键参数和真实会踩的坑,适合三类读者:嵌入式开发者想给板子瘦身,云镜像维护者想缩小内核体积,或者你只是想彻底看懂内核配置体系,这篇也能帮你把黑匣子打开。
裁剪值不值得做,取决于你的交付物。如果你维护的是固定硬件平台,裁剪收益非常明显,镜像体积能缩小一半以上,启动时间能省出可感知的几百毫秒;如果你是做通用发行版内核替代,那裁剪要克制,保留的驱动面反而比体积更重要。动手之前先记住一个判断标准:裁掉一个选项前,先问它被谁依赖,而不是先问它有没有用。
2. 裁剪前把源码树和配置体系摸透:Kconfig、Kbuild 与 .config 的联动
不摸清内核的配置组织方式就直接开裁,后面每一步都是玄学。内核源码里每个子目录下都有一个 Kconfig 文件,它描述了这个目录下有哪些可配置项、依赖关系和默认值;Kbuild 系统在编译时读取你生成的 .config,决定哪些目录会被编进去、哪些文件会被编译成内建还是模块。三者联动,裁剪的本质就是通过修改 .config 来影响 Kbuild 的编译决策。
2.1 Kconfig 语法与菜单树:你在 menuconfig 里看到的每一行来自哪里
打开 make menuconfig 看到的是一个树状菜单,这棵树的每个节点都来自各层目录的 Kconfig 文件。一个典型的配置项长这样:
config SND tristate "Sound card support" depends on SOUND help Say Y here to include support for sound cards.tristate 表示这个选项有三种状态:Y(内建)、M(编译成模块)、N(不编译)。bool 则只有 Y 和 N。depends on 表示依赖项,依赖不满足时这个选项在菜单里不会出现或者直接变灰。select 则是反向依赖,表示选中当前项会自动选上某个其他项,比如某个驱动 select 了某个通用框架。裁剪时最需要留意的就是 select,它会在你没注意的情况下把一堆选项重新打开。
理解 Kconfig 语法不用背,拿一块真实配置来对照就行。你可以在源码根目录执行 make menuconfig 随便点进去看,也可以直接查看某个驱动目录下的 Kconfig 文件。这里给一个积累手感的方法:
# 在源码树内查看某个具体配置项的来源和依赖 grep -rn "config SND" sound/ # 查看一个配置项被哪些地方 select grep -rn "select SND" sound/ drivers/ # 用 menuconfig 的搜索功能直接定位(进入菜单后按 / 输入关键字)搜索功能是裁剪时最高频的操作,没有之一。进入 menuconfig 后按斜杠 / 输入关键词,会列出所有匹配项,并直接告诉你当前值是 Y/M/N、依赖关系、被谁 select,以及选中状态是否有效。学会用 / 搜索,光标上下翻菜单的效率差了不止五倍。
2.2 最小可裁剪的源码准备与配置基线:从 defconfig 出发还是从发行版配置出发
裁剪前要有一个干净的源码树,而不是在之前编译过的目录里乱改。常见做法是先 make distclean 清理,然后确定配置基线。三种基线的选择决定了你后面要走的路:
| 基线来源 | 适用场景 | 优点 | 风险 |
|---|---|---|---|
| 厂商 defconfig(arch/<平台>/configs/ 下的 defconfig) | 嵌入式/开发板 | 配置按硬件裁剪过,体积小 | 可能缺你想要的文件系统或网络功能 |
| 发行版配置(/proc/config.gz 或 /boot/config-*) | x86 桌面/服务器 | 硬件兼容面广,驱动全 | 选项多、裁剪工作量大 |
| tinyconfig | 从零构建极端最小内核 | 体积最小起点 | 需要逐项加功能,调试成本高 |
我一般碰到的裁剪需求,70% 是从厂商 defconfig 起步的,因为你既然要裁剪,说明你手里有一个明确的目标硬件;剩下 30% 是在发行版配置基础上做减法,比如给云镜像瘦身。从发行版配置起步有个好处,它包含了发行版维护者踩过坑后的妥协方案,比如某些固件加载路径、ACPI 处理、USB 支持的组合。直接从 tinyconfig 起手更适合学习,不适合交付。
归档当前使用的配置是裁剪前必须做的一步,保证任何时候能退回起点,这是你的后悔药:
# 把当前运行内核的配置导出为基线存档(有 CONFIG_IKCONFIG 时) zcat /proc/config.gz > /tmp/kernel-baseline.config # 如果 /proc/config.gz 不存在,从 /boot 下取 cp /boot/config-$(uname -r) /tmp/kernel-baseline.config # 把基线配置放到源码树,让 Kbuild 进入已配置状态 cp /tmp/kernel-baseline.config .config make olddefconfigmake olddefconfig 的作用是让新配置对应当前源码树的 Kconfig 规则,自动补全缺失项并重置失效项,相当于把旧配置“翻译”成当前源码树能识别的状态。这个命令在导入外部配置后必须执行,跳过它直接 make 会得到一堆诡异的编译错误,因为它们本来就是配置和源码不匹配的表现。
2.3 配置落盘:.config、auto.conf 与 autoconf.h 三者的关系和备份习惯
裁剪操作最终落在 .config 文件上,但你编译时真正被读的不只是它。Kbuild 会基于 .config 生成 include/config/auto.conf(供 Makefile 读取)和 include/generated/autoconf.h(供 C 源码读取)。所以当你手动改 .config 后,必须重新执行配置同步命令,否则源码里看到的宏和实际配置不一致,编译出来的东西和你想要的对不上。
手动改配置的常用流程是这样:
# 编辑 .config,把 CONFIG_SND 改成 # CONFIG_SND is not set # 然后同步配置,而不是直接 make make olddefconfig # 确认关键配置项确实生效 grep "CONFIG_SND" .config另一个强烈建议养成的习惯是使用 make savedefconfig。这个命令会把当前 .config 中所有非默认值压缩成最小配置,生成的 defconfig 文件小、易读、适合放进版本管理。裁剪前后各保存一份,配合 diff 就能精确看到你改了什么:
# 裁剪完成后生成最小配置 make savedefconfig cp defconfig /tmp/kernel-cropped.defconfig # 和基线对比 diff /tmp/kernel-baseline.defconfig /tmp/kernel-cropped.defconfig有了这份 diff,你就能回答最常被问的问题:“你这次裁剪到底动了什么”。没有它,三个月后回头看自己的裁剪记录,你会对着上千行 .config 无从下手。
3. 动手裁剪:从 menuconfig 到命令行的完整过程
配置体系摸清楚之后,动手裁剪就变成了一个“判断 + 操作”的重复过程。判断靠的是对目标硬件和启动路径的理解,操作靠的是 menuconfig、命令行辅助脚本和配置工具。这一章按执行顺序展开,从第一轮菜单操作到模块收敛再到最终编译验证。
3.1 用 make menuconfig 做第一轮瘦身:必关的子系统与保留项
第一轮裁剪建议直接进菜单操作。启动 menuconfig 前先确认你已经装了 ncurses 开发库,否则界面起不来:
# 进入配置界面,界面操作核心就两个:方向键移动、空格切换状态 make menuconfig进入菜单后,按 / 搜索以下常见可裁剪项,逐个评估:
| 配置项 | 典型场景 | 裁剪建议 |
|---|---|---|
| CONFIG_SND(声卡) | 无音频需求的嵌入式/服务器 | 可关 |
| CONFIG_BT(蓝牙) | 非无线外设场景 | 可关 |
| CONFIG_WLAN(无线网卡) | 只用有线网络 | 可关 |
| CONFIG_INPUT_TOUCHSCREEN | 无触摸屏 | 可关 |
| CONFIG_DRM(显示子系统) | 无显示输出 | 可关 |
| CONFIG_HWMON(硬件监控) | 无传感器需求 | 按需保留 |
第一轮只做“明显的关闭”,不要碰任何带 selected by 的项,也不要试图一次性关掉几十个。每关一个子系统,建议顺手看一眼它下面的依赖说明,特别是 help 文本里有没有其他模块会依赖它的提示。GPU、声卡这类子系统往往会被多媒体框架 select,关掉后反而破坏了你需要保留的视频编解码功能,所以这类项要放到第二轮结合模块列表再处理。
import 保留项也有底线。串口驱动(CONFIG_SERIAL_8250 等)必须保留,这是嵌入式设备最重要的调试通道;ext4、vfat 等目标根文件系统相关配置保留;PCI/ACPI 电源管理基础项保留,关掉它们会让硬件探测顺序变得不可预测。第一轮的目标不是一步到位,而是先把明显冗余拿掉,让 .config 体积下降一个量级。
3.2 用 localmodconfig 快速收敛模块集合:原理与三条适用前提
菜单里手动关选项到后面会越来越累,因为驱动项太多,判断成本高。这时候可以用内核自带的 localmodconfig 来做一次“模块集合收敛”。它的原理很简单:列出当前系统已经加载的所有内核模块集合,把集合内模块对应的配置项保留为 M,集合外看得到但未加载的模块直接关掉。
# 前提:当前系统正好是你目标环境的完整运行状态 # 收集当前已加载模块并生成收敛配置 make localmodconfig这个命令执行后,会在当前 .config 基础上把所有未加载模块对应的选项关掉,操作速度比手动找快几十倍。但它有严格的使用前提,三条缺一不可:
第一,你运行 localmodconfig 的机器必须已经完整跑过所有目标硬件路径。比如你的目标设备有 USB 转串口工具,但你现在的系统从未插过它,那么对应驱动就会被误关。第二,lsmod 输出的模块列表必须能代表正常运行集,如果你在精简系统里执行这个命令,结果会把本应保留的驱动裁掉。第三,模块之间的依赖链会被自动保留,但如果某功能以 Y 方式内建且当前未使用,localmodconfig 不会处理它,你还是得靠 menuconfig。执行完后照例跑 make olddefconfig,再 diff 一次看看收敛范围是否符合预期。
localmodconfig 不是万能药,它是“用运行事实代替人工判断”的加速器。我习惯先手动做一轮明显裁剪,再跑 localmodconfig 兜底,最后回到 menuconfig 检查几个关键项,三层下来配置基本干净了。
3.3 裁剪后的编译与尺寸核对:make 的并行参数和 Image/vmlinux 的体积检查
配置定稿后进入编译阶段。内核编译和普通项目不太一样,先看常用命令:
# 并行编译,-j 参数按 CPU 核数来 make -j$(nproc) bzImage # 单独编译模块,供后续安装到目标环境 make -j$(nproc) modules # 如果你要完整安装到当前系统,才执行 modules_install sudo make modules_install-j 后面的数字不是越大越好。内存小的机器开满核会触发 OOM,实际经验是取 nproc 的一半到三分之二之间更稳妥:比如 8 核机器用 -j6,16 核用 -j10,编译内核时每个编译单元吃内存不小,翻车概率高。编译成功后重点看两个产物:arch/x86/boot/bzImage(x86 平台)是实际引导用的压缩内核,vmlinux 是未压缩的 ELF,前者决定你引导时读多大数据量,后者反映代码段实际规模。
# 查看镜像体积 ls -lh arch/x86/boot/bzImage ls -lh vmlinux # 查看模块集合体积 du -sh /lib/modules/$(uname -r) # 按大小排序,找出最大的模块,判断是否还有裁剪空间 find /lib/modules/$(uname -r) -name '*.ko' -exec ls -lh {} \; | sort -k5 -h | tail -20第一轮裁剪的成果在 bzImage 体积上通常立竿见影,常见情况是从十几 MB 降到 5 MB 左右。模块目录体积则更依赖 localmodconfig 的收敛效果,如果你的基线是发行版全量模块,从几百 MB 降到几十 MB 很正常。如果 bzImage 体积变化不大,说明你关掉的选项大多是模块而不是内建设备,这时候要重点看模块列表,而不是继续盯着镜像体积。
4. 内核裁剪避坑:剪完起不来、模块加载失败与配置失效的排查路径
裁剪最痛苦的不是操作过程,而是剪完之后的启动失败。这类问题没有统一报错,故障现象五花八门,但根因高度集中。这一章把最容易踩的五个坑按现象、原因、解决三个维度拆开,每条都可以直接对着你的 dmesg 排查。
4.1 驱动被剪但设备表还在:启动日志里的 deferred probe 现象与处理
现象:裁剪后的内核能启动,但原来能用的外设不工作,dmesg 里频繁出现 platform ... deferred probe 或 -EPROBE_DEFER 错误。
原因:内核驱动和设备驱动的注册是异步的,某个驱动的 probe 依赖另一个驱动先完成,比如一个 I2C 触摸屏驱动依赖 I2C 控制器先就绪。当你裁剪时把依赖链上游的某个驱动关掉了,下游驱动的 probe 就会反复延迟,最终放弃。配置文件里看不到直接的因果关系,因为它依赖的是设备树或 ACPI 表提供的兼容信息,而不是 Kconfig 的 depends on。
解决:先定位 deferred probe 的具体设备,再反查它需要的上游驱动。以内核日志为例:
# 查看 deferred probe 状态列表 ls /sys/kernel/debug/devices_deferred # 查具体设备的依赖信息(以 i2c 设备为例) dmesg | grep -i "deferred probe" # 到 menuconfig 里搜索对应的上游驱动名,比如 pinctrl、regulator、clock make menuconfig大多数情况下,被裁掉的是某个 pinctrl 或时钟驱动。解决办法不是把整个子系统打开,而是按需打开那个具体的厂商驱动选项。如果你分不清设备对应哪个驱动,可以先用未裁剪的基线内核确认设备节点名,再在裁剪内核里对比缺失的驱动目录。
4.2 模块与内建交叉引用:unknown symbol 与未导出的符号表
现象:模块能编译成功,但 modprobe 时报 unknown symbol 或 -EINVAL,dmesg 里有 module ... has no symbol version 之类的提示。
原因:内核模块几乎都依赖内核导出的符号,比如某个函数只在某个配置项开启时才导出。裁剪时如果关闭了导出该符号的配置项,但保留了依赖它的模块选项,编译阶段不报错,运行加载时符号解析失败。这类问题在 lsmod 收集不完整的 localmodconfig 场景下尤其常见。
解决:用 modinfo 查看模块依赖和符号要求,回到配置里打开对应的符号导出项。
# 查看模块信息和依赖 modinfo <module_name> # 搜索模块引用的关键符号对应的配置项 grep -rn "EXPORT_SYMBOL.*<symbol_name>" /path/to/kernel/source/ # 打开对应配置后重新编译模块 make menuconfig make modules -j$(nproc)判断这类问题时,不要试图手动在 .config 里加回某个 CONFIG_ 项然后直接编译,你必须确保依赖项完整。建议查找该符号所在的 Kconfig 项后,在 menuconfig 里重新打开它并保存,然后跑 make olddefconfig 让依赖自动补全。
4.3 initramfs 与根文件系统不匹配:裁剪后找不到根设备的常见原因
现象:启动卡在 VFS: Unable to mount root fs on unknown-block,或者提示不对根文件系统类型,直接进入 initramfs 紧急 shell。
原因:裁剪时把根文件系统所在磁盘的控制器驱动或文件系统驱动关掉了。常见误伤对象是 NVMe 控制器、SATA AHCI、USB 存储驱动,以及 ext4 之外的根文件系统格式。另一个隐蔽原因是 initramfs 里面存放的模块列表来自旧内核的 modules.dep,新旧内核模块版本对不上,导致 initramfs 加载不到正确驱动。
解决:先确认根文件系统所在设备类型,再检查对应驱动配置:
# 确认根设备是 NVMe 还是 SATA 还是 USB lsblk # 检查关键配置项是否保留 grep -E "CONFIG_NVME|CONFIG_ATA|CONFIG_USB_STORAGE|CONFIG_EXT4_FS" .config # 重新生成 initramfs,让驱动列表和新内核匹配 sudo update-initramfs -u -k <your-kernel-version>如果你用的是 dracut 体系,对应命令是 sudo dracut --force。这一步经常被人忽略,以为裁剪内核跟 initramfs 无关,实际上 initramfs 里打包的模块列表就是按旧内核配置生成的。裁剪后不重建 initramfs,驱动加载顺序和模块集合全部错位,表现就是找不到根设备。
4.4 固件文件缺失:保留驱动但缺少 firmware,设备静默失败
现象:设备在 dmesg 里没有任何报错,但就是不出来,或者明确提示 firmware: failed to load。
原因:有些驱动(特别是网卡、GPU、DSP)在 probe 时需要加载固件文件,固件不在内核源码树里,而在 /lib/firmware 下。裁剪内核时你把 CONFIG_FW_LOADER 关了,或者没把固件文件路径编进内核,驱动就静默失败。反过来,如果驱动被裁剪,固件加载代码还在,日志里也会出现 failed to load 的误导信息。
解决:第一步区分是驱动问题还是固件问题。
# 查看驱动声明的固件文件 modinfo <driver_name> | grep firmware # 检查固件文件是否存在 ls -lh /lib/firmware/<firmware-name> # 如果驱动已经裁剪,先回 menuconfig 确认它的状态 # 如果驱动在但固件缺失,把固件文件放到 /lib/firmware 对应路径内核支持把固件直接编入镜像,对应配置项是 CONFIG_EXTRA_FIRMWARE。嵌入式裁剪场景我喜欢在 .config 里把目标固件路径写死,这样可以减少对根文件系统的依赖。但要注意,EXTRA_FIRMWARE 只在开启 CONFIG_FW_LOADER 时生效,两者必须同时保留。
4.5 配置碎片化:没有版本管理,裁剪就是一次性操作
现象:裁剪到第三轮时,发现第一轮关掉的某个配置又莫名其妙地变回了 Y,整个 .config 混乱到无法维护。
原因:配置之间通过 select、condition 等隐式依赖互相牵连,你在 menuconfig 里关闭一个项,可能顺手把另一处依赖它的选项也改变了,而你根本没有察觉。更常见的是,手动编辑 .config 后没跑 make olddefconfig,第二次 make menuconfig 时被 Kbuild 静默修正。
解决:把配置当作代码来管。每次有效裁剪后做三件事:备份 .config、生成 savedefconfig、记录 diff。一套顺手的方式是建一个配置管理目录:
mkdir -p /tmp/kernel-config-history # 每次裁剪后的固定动作 cp .config /tmp/kernel-config-history/config-$(date +%Y%m%d-%H%M%S) make savedefconfig cp defconfig /tmp/kernel-config-history/defconfig-$(date +%Y%m%d-%H%M%S)有了这些存档,任何一轮裁剪出错都能快速二分定位:先对比最近两次 defconfig 的差异,看是哪个配置项导致的行为变化,再决定回退还是修正。我自己吃过一次大亏,裁剪到第三轮时一次误操作把 CONFIG_DEVTMPFS 关了,系统起来后 /dev 下面全是空的,排查了大半天才通过 defconfig 对比找到原因。从那之后固定执行这个备份流程,再没在配置回滚上浪费过时间。
5. 裁剪的验证与进阶:启动时间、体积回归和自动瘦身手段
验证比裁剪本身更重要。裁剪的效果不是“能起来”就算成功,而是要有可量化的指标,还要有对比基线。三类指标最值得记录。
第一个是镜像体积。bzImage 大小直接体现在引导加载时间上,vmlinux 大小反映了代码段规模。第二个是模块集合体积和数量,这决定了根文件系统(特别是 initramfs)里要放多少东西。第三个是启动耗时,用 systemd-analyze 或内核 printk 时间戳来测:
systemd-analyze systemd-analyze blame | head -20 # 更细的时段分布,看内核初始化时间占比 dmesg | grep "kernel booted"把裁剪前后的三组数据记下来,才是这次工作的完整交付物。光说“我剪小了”没有说服力,拿出体积、模块数和启动时间的变化,项目评审才过得去。
配置审计用内核自带的 diffconfig 脚本最方便。它比普通 diff 更懂配置语义,能自动把同一配置项的新旧值合并成有效变更行:
# 对比两个配置文件,只显示有变化的项 scripts/diffconfig /tmp/kernel-baseline.config .config进阶手段方面,如果你想把裁剪玩到极致,可以从 tinyconfig 起步做白名单式配置:先全部关掉,再把目标硬件和文件系统需要的项逐个加回去。这条路前期成本高,但产物最干净,适合学习 Kconfig 依赖关系和做最小系统的场景。内核源码里还有 scripts/kconfig/merge_config.sh,适合把碎片化的配置片段合并进基线配置里做自动化裁剪流程,我一般配合脚本做每日构建时的配置回归测试。
另一个常被忽略的优化是内核压缩方式。CONFIG_KERNEL_XZ 比默认的 gzip 通常能再缩小 20% 到 30%,代价是启动解压时间变长,没有通用最优解,压到你的板子能接受的平衡点即可。
我做内核裁剪这几年,最大的教训就是开头那句判断标准:先查依赖,再动选项,裁剪不是删文件,是给内核做减法手术,每一刀都得知道它切在哪里。养成裁剪前归档、裁剪后对比的习惯之后,这套流程基本不再翻车。希望帮到你,下次剪内核,先 save 一份 defconfig 再动手。
本文还有配套的精品资源,点击获取