ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Linux内核裁剪实战:从全家桶到轻量化,提速70%的完整复盘

2026/9/29 8:22:54 拓冰建站 浏览量
Linux内核裁剪实战:从全家桶到轻量化,提速70%的完整复盘 做 Linux 内核裁剪这件事听起来很高端本质上就是给内核做减法。我去年给一台跑单一业务的 X86 工控机做了一次完整的 linux 内核裁剪把一个 12.8MB 的 Debian 默认 bzImage 一路砍到 5.7MB可加载模块从 5300 多个精简到 84 个开机到登录从 23 秒降到了 6.8 秒。整个过程有惊喜也有翻车今天这篇就把这次内核裁剪的经验完整复盘一下从动机、摸底、逐项取舍到编译启动时踩过的坑以及验证和回滚的方法全部摊开讲。不管你是给嵌入式设备省空间还是给服务器提速这套思路都能直接套用。1. 裁剪动机发行版内核全家桶与我的瘦身目标1.1 发行版内核为什么什么都带发行版内核的设计哲学和定制内核完全不同。Debian、Ubuntu、CentOS 这些发行版要面对的是地球上所有可能的硬件组合所以它们编译内核时的心态是不管用不用得上先全部编进去再说。于是你会在配置里看到几十种文件系统、几百个网卡驱动、各种冷门的工业总线协议——全被编译成模块塞进了/lib/modules。这就像买了台标配车后备箱里永远放着你可能这辈子都用不上的全套工具。平时没啥感觉但对一台用途明确的专用机器来说这种全家桶策略的代价很具体磁盘空间装完内核后/lib/modules里有几千个.ko文件随便就是几百 MB。启动时间内核启动时要扫描总线、探测设备、触发 udev 去匹配 modalias模块越多匹配和加载的耗时越长。内存占用模块的元数据、页表项、以及被自动加载进内存的驱动都会吃掉宝贵的内存。安全面每多一个驱动或协议栈就多一分被攻击或误触发的风险。我手头那台工控机配置是 Intel 赛扬 J4125、4GB 内存、一块 SATA SSD、双千兆网卡业务就是采集串口数据然后通过网络上报固定到不能再固定。这种机器装一个通用发行版内核等于每天穿着全套滑雪装备去上班纯属浪费。1.2 我先量化三个目标再动手裁剪这种事最怕边剪边看随缘优化所以我先定了三个硬指标都是有依据的指标裁剪前目标值判断依据bzImage 镜像体积12.8MB≤ 7MB内核本身应该轻装上阵开机到登录耗时23.1s≤ 10s工控机要快速恢复业务启动后已加载模块数148 个≤ 50 个只保留这台机器实际用到的我特意把已加载模块数而不是可用模块总数作为核心指标因为内核真正占资源的是被加载进内存并在运行的那些模块而不是躺在磁盘上睡觉的。另外我还给自己定了一条安全底线裁剪期间 grub 里永远保留一个已知能正常启动的旧内核入口。后面你会看到这条底线救了我两次。2. 开工前摸底配置快照、硬件清单与模块依赖树2.1 从 /proc/config.gz 拿到完整配置快照裁剪的第一步不是动手删而是先把家底摸清楚。内核编译时如果开启了CONFIG_IKCONFIG_PROC运行中的内核会把完整配置打包在/proc/config.gz里。如果你的内核没开这个选项可以从/boot/config-$(uname -r)拿到一份编译时的默认配置。uname -r # 6.1.0-13-amd64 # 优先从 /proc 导出当前运行内核的真实配置 zcat /proc/config.gz /tmp/current.config # 或者从 /boot 拷贝发行版默认配置 cp /boot/config-$(uname -r) /tmp/current.config # 查看关键选项是否已开启 grep -E CONFIG_IKCONFIG_PROC|CONFIG_BLK_DEV_INITRD /tmp/current.config这份配置快照后面有大用一是作为裁剪的起点二是跟裁剪后的配置做 diff搞清楚自己到底动了哪些选项。我强烈建议你像我一样先把/tmp/current.config存一份千万别直接拿它当.config来用。2.2 用 lspci/lsusb 盘点必须保留的硬件驱动裁剪的本质是按需保留所以你必须先明确这台机器到底有哪些硬件。三个命令就够lspci -nnk # 看 PCI 设备以及当前绑定的内核驱动 lsusb # 看 USB 设备 lshw -short 2/dev/null | head -50 # 综合硬件信息当时我盘点完的结论是SATA 控制器、双网卡RTL8111/8168、USB 控制器、串口板载 8250/16550、核显但机器是纯headless 运行只用串口做 console。对应到内核配置上就是下面这几组必须保留的选项硬件设备对应内核配置建议形式SATA 控制器CONFIG_SATA_AHCI、CONFIG_ATA编入内核 y板载网卡 RTL8168CONFIG_R8169编入内核 yUSB 3.0 控制器CONFIG_USB_XHCI_HCD、CONFIG_USB_STORAGE模块 m板载串口CONFIG_SERIAL_8250、CONFIG_SERIAL_8250_CONSOLE编入内核 y核显可关CONFIG_DRM、CONFIG_FB裁剪掉这里有个关键思路如果这台机器从今往后不会再插拔新硬件那驱动可以大胆裁如果还有可能接新设备就得多留一个心眼。我当时考虑得很清楚这台工控机上线后外设就锁死了所以走的是激进路线。通用服务器别学我老老实实用localmodconfig就好。2.3 顺着 modules.dep 理清模块依赖树模块不是孤立存在的thermal这种看起来人畜无害的模块背后可能挂着一整串依赖。想让裁剪不出乱子必须先看懂模块之间的依赖关系# 看看当前加载了哪些模块按引用次数排序 lsmod | sort -k2 -n # 查看某个模块的依赖链 modprobe --show-depends thermal # insmod /lib/modules/6.1.0-13-amd64/kernel/drivers/thermal/thermal_sys.ko # insmod /lib/modules/6.1.0-13-amd64/kernel/drivers/thermal/thermal_sys.ko.zst # 依赖关系的静态索引 grep thermal /lib/modules/$(uname -r)/modules.depmodules.dep是depmod生成的模块依赖索引modprobe加载一个模块时会先看一眼这个文件把依赖模块按顺序全部拉起来。你裁剪时如果保留了一个模块它的所有依赖模块也必须保留否则运行时会报Unknown symbol或者直接加载失败。另外要明白 udev 的自动加载机制设备插入后内核上报 modaliasudev 根据/lib/modules/.../modules.alias找到对应模块并触发modprobe。所以如果你把某个驱动从配置里摘掉了系统启动时不会报错但设备会一直处于不被驱动的状态表现为网卡不出现、硬盘识别不到这种诡异问题。这也是为什么每次裁剪完我都习惯性地lspci -k看一眼驱动有没有正确绑定上。3. 裁剪三板斧localmodconfig、menuconfig 与 y/m 取舍3.1 第一板斧localmodconfig 自动瘦身摸底做完正式开工。第一步是让内核自己帮你做一次粗剪工具叫localmodconfig逻辑很简单读当前系统lsmod的加载列表配合modules.dep反推凡是没有被加载过的模块对应的配置项一律改成n加载过的保留为m。这一步跑完几千个模块配置基本能砍掉七成以上。cp /tmp/current.config .config LANGC make localmodconfig之所以要加LANGC是因为内核的配置脚本在某些本地化环境下会输出乱码导致交互界面错乱我踩过一次后面就老实了。另外提醒一句跑 localmodconfig 之前先确认这台机器当前状态就是它日常工作的状态。如果平时会插 USB 设备、接调试线、挂移动硬盘这些设备最好提前插上再跑否则这些驱动会被当成没用的东西直接裁掉。如果你是在别的机器上分析模块列表可以手动指定lsmod | awk NR1 {print $1} /tmp/modules.txt make LSMOD/tmp/modules.txt localmodconfigLSMOD指向一个模块名列表文件脚本会按这个列表而不是当前环境来裁剪。这个技巧在批量处理同型号机器时非常好用先在一台标准机上导出模块列表剩下的机器直接套用。3.2 第二板斧menuconfig 里关掉六大类大户localmodconfig只处理了模块的取舍还有大量以y形式编进内核的功能它动不了这就需要人工干预了。我习惯用make menuconfig做精细调整交互界面清晰搜索选项也方便make menuconfig # 需要安装 libncurses-dev否则会报找不到 menuconfig 界面在 menuconfig 里我一共关了六大类东西每一类的理由都给你列清楚第一类用不到的文件系统。这台机器根分区是 ext4我又不需要挂 Windows 盘、玩网络文件系统所以xfS、btrfs、reiserfs、f2fs、ntfs3、nfs、cifs、fuse全部关掉。注意fuse如果关掉某些基于 FUSE 的用户态文件系统比如部分网盘客户端就会失效关之前确认业务没依赖。另外SquashFS 建议保留很多软件包格式和快照机制靠它。第二类无线、蓝牙与多余的网络协议。工控机只用有线网于是CFG80211、MAC80211、BT、WIRELESS_LAN整组关掉。网络协议里我把CAN总线协议、各种冷门NET_VENDOR_*驱动也关了但特意留着 IPv6。有相当多 Linux 程序和服务依赖 IPv6 loopback比如某些容器和调试工具关 IPv6 省不了多少东西却可能让你排查到怀疑人生。第三类声卡、显卡和用不上的输入设备。headless 机器不需要CONFIG_SOUND/CONFIG_SND_*也不需要CONFIG_DRM和CONFIG_FB。但这里有个大坑如果你关掉了所有显示相关驱动而机器又没有串口 console一旦内核启动失败你连错误信息都看不到。所以我的做法是保留串口 console 相关的CONFIG_SERIAL_8250_CONSOLE在 grub 里配置consolettyS0,115200这样哪怕系统起不来也能用串口抓日志。第四类调试与观测设施。这是体积和启动时间的大头。CONFIG_DEBUG_INFODWARF 调试信息、CONFIG_DEBUG_INFO_BTFBTF 类型信息、KPROBES、FTRACE、KGDB、MAGIC_SYSRQ、SCHEDSTATS、PERF_EVENTS这些全是可以关的。但注意两点一是CONFIG_KALLSYMS建议保留否则内核 panic 时打印的是无意义的地址而不是函数名排障难度陡增二是如果你依赖 eBPF 观测工具CONFIG_DEBUG_INFO_BTF不能关BCC/bpftrace 这类工具全靠它加载 eBPF 程序。第五类安全与虚拟化。这台机器跑在隔离的内网段没有多租户需求所以SELinux、AppArmor、KVM、Intel VT这些被我关了。但你得想清楚自己机器的定位如果机器在不可信网络里或者要跑容器安全模块和命名空间千万别乱动。另外CONFIG_IO_URING我强烈建议保留现在 MySQL、Nginx、很多高性能网络库都在用 io_uring关了会让它们的底层 IO 路径崩掉或性能暴跌。第六类各种冷门外设驱动。HID传感器、V4L/DVB多媒体、IRDA红外、PCMCIA卡、各种少见的总线控制器统统关掉。工控机如果接了串口传感器注意保留CONFIG_I2C、CONFIG_SPI、CONFIG_GPIO_SYSFS这些基础总线框架我当时差点误伤 I2C 导致采集板失灵幸好启动后例行dmesg检查时发现报错及时补了回来。menuconfig 改完之后跑一遍make olddefconfig让新配置里没被显式声明的选项回归默认值然后生成一份精简版配置备查make olddefconfig make savedefconfig # 生成的 defconfig 只保留非默认项方便 review 和版本管理3.3 第三板斧内建还是模块的决策逻辑配置界面里每个选项都有三种状态y编进内核镜像、m编译成模块、n直接不编。很多人纠结这里我给出一个简单实用的决策标准内核早期就要用的编成y。根文件系统驱动、磁盘控制器驱动、串口 console、以及你无法容忍晚一步加载的网卡驱动。它们编进镜像是为了在内核还没挂载任何文件系统之前就能工作不依赖 initramfs。可以热插拔、或者到用户态阶段才需要的编成m。比如 USB 存储、输入设备、各种外设驱动。反正有 udev 和 initramfs 兜底模块化不影响使用还能给内核镜像减负。永远用不到的直接n。这才是裁剪的真正含义。这里必须解释一下 initramfs 这个机制它是一个小型的临时根文件系统内核启动早期把initrd.img解压到内存里用它里面预置的驱动和 udev 去识别磁盘然后把根切换到真正的根分区。所以你如果把根文件系统驱动编成了m没关系只要 initramfs 里包含这个模块就能正常启动但如果你图省事把它设成了n那就彻底没救了内核连磁盘都认不出来。我当时为了保险直接把ext4、SATA_AHCI、R8169全部改成y宁可内核镜像大 1MB也不赌 initramfs 的加载时序。4. 编译安装与三次翻车VFS 挂载失败、网卡失联、固件缺失4.1 编译参数与安装的常规动作配置定型后就是编译。Debian 系先装依赖sudo apt install build-essential libncurses-dev flex bison bc libssl-dev libelf-dev dwarves cpiodwarves这个包经常被忽略但内核开了CONFIG_DEBUG_INFO_BTF时编译最后一步需要pahole工具生成 BTF 信息如果没装会直接报错BTF: .tmp_vmlinux.btf: pahole (pahole) is not available。解决办法要么装dwarves要么scripts/config --disable DEBUG_INFO_BTF。我为了给 eBPF 留后路选择了保留 BTF 并装好工具。编译安装的标准流程make -j$(nproc) # 多核并行编译工控机交叉编译时按目标机器核数来 sudo make modules_install # 把模块装到 /lib/modules/6.1.0-xxx/ sudo make install # 安装内核镜像并触发 installkernel 脚本 sudo update-initramfs -c -k $(make kernelrelease) # 务必显式重建 initramfs下面会讲为什么 sudo update-grub补充一个虚拟机场景的小坑如果你是在 VirtualBox 里做内核实验裁剪掉的内核会导致 vboxguest 这类 DKMS 模块需要重新编译头文件没匹配上就会出现kernel driver not installed (rc-1908)一类的错误。解决办法是确认安装了对应当前内核版本的linux-headers并且让 DKMS 重建模块别让它在 grub 列表里带着旧模块反复折腾。4.2 第一次翻车VFS: Unable to mount root fs裁剪后的第一个版本重启我盯着串口输出屏幕赫然出现一行红色 panic[ 1.603212] VFS: Cannot open root device sda2 or unknown-block(8,2): error -6 [ 1.603898] Please append a correct root boot option; here are the available partitions: [ 1.605102] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(8,2)先解释一下-6这个错误码它是-ENXIO意思是设备不存在。这个报错说明 initramfs 已经加载了但它在里面找不到能驱动根分区所在磁盘的模块。我第一反应是ext4被 localmodconfig 干掉了查了一圈发现不是——问题出在我把CONFIG_BLK_DEV_INITRD关掉了也不对initramfs 明明加载了。真正的原因是make modules_install把新模块装到了新路径但update-initramfs没有执行grub 用的还是旧版 initramfs里面根本没有新内核对应的模块而我的ext4还保持m于是 initramfs 里没有 ext4 驱动可以被加载挂载根分区自然失败。这个坑的教训就两句话第一make install之后必须手动确认 initramfs 重建了别指望脚本自动帮你做第二根文件系统和磁盘控制器驱动直接编成y一劳永逸它能让你少掉一半 initramfs 相关的麻烦。我后来把ext4、SATA_AHCI、R8169全改成内建这个问题再没出现过。4.3 第二次翻车远程机器的网卡驱动没了第一次翻车是本地工控机还有串口可以救。但那次教训之后我又去优化一台远程服务器结果犯了个更蠢的错裁剪配置时手滑把网卡驱动从y改成了n旧内核备份在 grub 里虽然留着但新内核一启动网卡直接消失SSH 当场断开机器彻底失联。说实话那一刻我冷汗都下来了。最后是靠机房同事在 IPMI 的 KVM 上进了 grub手动选中旧内核启动才把配置改回来。这个经历让我长记性了提示远程机器裁剪内核之前务必确认有三种以上能摸到这台机器的途径IPMI/BMC 带外管理、物理串口 console、或者至少一个不受内核驱动影响的救援系统。网络驱动和磁盘驱动这类硬件识别相关的配置任何未经测试的修改都可能让你连不上机器。宁可多跑一趟机房也别拿生产服务器的网络开玩笑。4.4 第三次翻车固件加载失败与静默降级第三次翻车主没有 panic比前两次更阴险。裁剪完启动倒是正常系统也起来了但我例行dmesg时看到一堆错误[ 2.891234] r8169 0000:03:00.0: Direct firmware load for rtl_nic/rtl8168h-2.fw failed with error -2 [ 2.891567] r8169 0000:03:00.0: Unable to load firmware rtl_nic/rtl8168h-2.fw这就是典型的静默降级驱动加载了设备也能用但因为没有固件文件网卡可能跑在较低的速率或者某些硬件特性直接缺失而且系统不会因为这个报错就拒绝启动。很多人在内核裁剪时只顾着开关 CONFIG忘了把/lib/firmware里的固件文件一并考虑进去。固件和驱动是两回事驱动是软件代码固件是刷进网卡/显卡/无线模块里的小程序。驱动加载时会从/lib/firmware请求对应的.fw文件。裁剪时你关的是驱动但如果驱动还在固件路径依然需要保留。解决办法通常是把/lib/firmware里对应的固件文件拷贝到新系统的同名目录或者更彻底一点用CONFIG_EXTRA_FIRMWARE把固件直接内嵌进内核镜像代价是镜像会变大。我选择了前者因为镜像体积对我来说更敏感。4.5 调试、回滚与 QEMU 预验证三次翻车之后我总结了一套稳妥的上线流程核心思想是先在隔离环境里验证再上真机。回滚永远走 grubupdate-grub之后开机进 Advanced options 就能看到旧内核入口。我给自己定了个纪律新内核没有稳定运行满一周旧内核入口坚决不删。QEMU 预启动验证是性价比最高的一招。在物理机重启之前先用 QEMU 把新内核和新 initramfs 跑一遍qemu-img create -f qcow2 test-root.qcow2 8G # 把根文件系统克隆到测试镜像里或用现有测试虚机 qemu-system-x86_64 -m 1G \ -drive filetest-root.qcow2,formatqcow2,ifvirtio \ -kernel /boot/vmlinuz-6.1.0-trim \ -initrd /boot/initrd.img-6.1.0-trim \ -append root/dev/vda1 consolettyS0 \ -nographic通过串口控制台观察启动全过程确认能够挂载根分区、网络驱动加载成功、没有固件报错再上真机。这一步能把 80% 的启动问题挡在物理机之外。真机启动后的验证清单也很固定systemd-analyze # Startup finished in 1.2s (kernel) 5.6s (userspace) dmesg | grep -iE fail|error|firmware|warn # 排查任何隐性问题 lspci -k | grep -A2 -E Ethernet|SATA # 确认关键驱动已绑定 find /lib/modules/$(uname -r) -name *.ko* | wc -l # 当前内核实际安装的模块数量5. 最终效果对比与沉淀下来的几条裁剪纪律5.1 裁剪前后的量化对比这套流程走完之后再看各项指标差距非常直观指标裁剪前裁剪后变化bzImage 体积12.8MB5.7MB下降 55%已安装模块文件数5317 个84 个下降 98%启动时已加载模块数148 个39 个下降 74%内核启动耗时4.3s1.2s下降 72%开机到登录总耗时23.1s6.8s下降 71%同负载下空闲内存基准多出约 80MB内存占用显著下降.config 开启项数量47601873下降 61%配置差异可以用内核自带的 diff 脚本快速审计scripts/diffconfig /tmp/current.config .config | head -30 # ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff # 显示所有被改动/新增/删除的配置项我用 git 把每个阶段的.config都提交了一次改了什么、为什么改全部写在 commit message 里。后面机器出问题回溯时能直接看到配置演进历史比自己拍脑门回忆靠谱一万倍。5.2 几条值得长期遵守的裁剪纪律这次裁剪之后我给自己定了几条规矩分享给同样想折腾内核的人第一已知良好的旧内核永远留在 grub 里。新内核没有稳定运行一周以上旧内核的镜像、模块、initramfs 一个都不许删。这是你所有翻车事故的底牌。第二每关一个大类就编译测试一次。别攒到最后一次性验证。先关文件系统试试能不能启动再关网络协议再关驱动逐步推进。一旦出问题你马上知道是上一批改动导致的排查范围小得多。第三用 git 管理 .config配合scripts/diffconfig做差异检查。内核配置就是代码值得被版本化。每次裁剪前把当前配置作为 baseline 提交改完提交一次形成清晰的 changelog。第四想清楚机器的定位再决定裁剪深度。专机专用、外设固定、永不升级硬件的机器才值得激进裁剪。通用的开发机、服务器用localmodconfig做一次基础瘦身就够了没必要追求极限。裁剪的终极目的是让你更懂自己的机器而不是为了一个压缩后的数字去牺牲稳定性。第五远程机器的网络驱动和存储驱动永远编成y。这两类驱动关系到你能不能连上机器、系统能不能找到根分区属于关键时刻绝对不能掉链子的部分不值得为了省几 KB 体积去赌。最后说点个人感受。这次裁剪最大的收获其实不是省下的那 7MB 镜像和 16 秒启动时间而是我把这台机器的硬件栈和内核代码的对应关系彻底摸清了一遍每个设备绑定哪个驱动、每个驱动的配置项长什么样、initramfs 的加载链路里每一步在干什么。折腾完内核我不光会用系统还知道系统底下是怎么运转的。这种感觉比省下的那点资源值钱多了。