1. 项目缘起与核心思路拆解
1.1 为什么盯上了固件包这块"赘肉"
手头有一批基于 N100 处理器的迷你节点,跑的是 Ubuntu 系统,用途比较单一——做轻量级容器编排和边缘数据采集。这批机器有个共同特点:板载存储是 64 GB 的 eMMC,实际可用空间也就 58 GB 出头。系统装完、Docker 拉几个镜像、再放点日志和临时数据,磁盘占用很快就逼近红线。
一开始我的做法很粗暴:加外置存储。但节点数量一多,外置存储的供电和物理空间都成了问题,而且这些节点部署在分散的机柜里,加硬盘意味着要重新规划机柜布局,成本不划算。于是我把目光转向了系统本身——能不能从根上把 Ubuntu 的体积压下来?
用du一层层扒下来,发现/usr/lib/firmware这个目录占了大头,足足 1.2 GB 左右。这个目录里塞满了各种硬件厂商的固件文件,从网卡、显卡、声卡到各种专用加速卡,应有尽有。问题是,N100 这种低功耗平台,板载设备就那么几样:一个 Intel 核显、一个 Realtek 或 Intel 的千兆网卡、一个 USB 控制器、一个 SATA 或 NVMe 控制器。剩下那些固件,比如某些专业显卡、光纤通道卡、老式调制解调器的固件,在这台机器上永远不会被加载。
这就是典型的"通用发行版困境"——Ubuntu 为了让镜像能在尽可能多的硬件上开箱即用,把能塞的固件全塞进去了。对于批量部署的专用节点来说,这些用不上的固件就是纯粹的存储浪费。我的目标很明确:在保证 N100 平台所有板载设备和常用外设正常工作的前提下,把固件包精简到最小可用集。
1.2 精简策略:不是删文件,而是重建依赖
很多人第一反应是直接rm -rf掉/usr/lib/firmware里不认识的文件。这个做法极其危险,原因有三:第一,你无法确定某个固件是不是在特定电源状态下才会被加载;第二,系统更新时linux-firmware包会把删掉的文件重新装回来;第三,直接删文件会破坏包管理器的完整性记录,后续apt操作可能报错。
我的策略是从包管理层面重建一个精简的固件包。具体思路是:
- 先摸清 N100 平台实际需要哪些固件——通过
dmesg、lspci、lsusb和ethtool等工具,把当前系统加载的固件模块全部列出来。 - 然后对照
linux-firmware包的文件清单,把不需要的固件文件从包中剔除。 - 重新打包成一个本地 deb 包,版本号标记为
custom,这样系统更新时不会自动覆盖。 - 最后用
dpkg强制安装这个精简包,替换掉原版。
这样做的好处是:包管理器记录完整,后续升级可控,而且精简逻辑可以脚本化,批量部署时一键执行。
1.3 预期收益与风险边界
按照我的估算,linux-firmware原包安装后占用约 1.2 GB,精简后目标控制在 750 MB 以内,每个节点省下约 450 MB。对于 64 GB eMMC 的节点来说,这 450 MB 意味着可以多跑一个中等规模的容器镜像,或者多存几天的采集数据。
风险边界也很清楚:精简后的固件包只保证 N100 平台板载设备和明确列出的外设可用。如果后续要加装特殊扩展卡,需要重新评估固件需求。所以我在部署脚本里留了一个"恢复原版固件"的回滚选项,万一遇到问题可以快速还原。
注意:这个方案的前提是你清楚自己的硬件清单,并且不会频繁更换外设。如果是通用开发机或者硬件配置经常变动的场景,不建议做这种激进精简。
2. 核心细节解析与实操要点
2.1 摸清 N100 平台的实际固件需求
第一步是搞清楚"哪些固件是真正被加载的"。我用的方法是在系统正常运行时,从多个维度收集信息。
内核日志法:dmesg | grep -i firmware会列出所有加载固件的记录。在 N100 节点上,典型输出包括:
[ 2.345678] i915 0000:00:02.0: firmware: direct-loading firmware i915/adlp_dmc_ver2_16.bin [ 2.456789] i915 0000:00:02.0: firmware: direct-loading firmware i915/adlp_guc_70.bin [ 3.567890] r8169 0000:01:00.0: firmware: direct-loading firmware rtl_nic/rtl8168h-2.fw [ 4.678901] Bluetooth: hci0: firmware: direct-loading firmware intel/ibt-0040-0041.sfi从这些日志可以看出,N100 平台主要用到三类固件:Intel 核显(i915 系列)、Realtek 网卡(rtl_nic 系列)、以及可选的蓝牙模块(intel/ibt 系列)。
PCI 设备枚举法:lspci -nnk可以列出所有 PCI 设备及其使用的内核驱动。重点关注Kernel driver in use和Kernel modules两行,这能告诉你哪些驱动被绑定到了硬件上。
USB 设备枚举法:lsusb -t展示 USB 设备树,lsusb -v可以看到每个设备的详细信息。对于板载蓝牙、摄像头等 USB 设备,这一步能确认它们是否需要额外固件。
网卡固件确认:ethtool -i eth0会显示网卡驱动的固件版本信息。如果输出中有firmware-version字段,说明该网卡需要外部固件文件。
把这些信息汇总成一张表,就是你的"固件需求清单"。以我的 N100 节点为例:
| 设备类型 | 驱动模块 | 所需固件路径 | 是否必需 |
|---|---|---|---|
| Intel 核显 | i915 | i915/adlp_dmc_ver2_16.bin | 是 |
| Intel 核显 | i915 | i915/adlp_guc_70.bin | 是 |
| Realtek 网卡 | r8169 | rtl_nic/rtl8168h-2.fw | 是 |
| Intel 蓝牙 | btusb | intel/ibt-0040-0041.sfi | 视配置 |
| Intel 无线 | iwlwifi | iwlwifi-*.ucode | 视配置 |
2.2 定位 linux-firmware 包的文件清单
Ubuntu 的linux-firmware包安装后,文件清单可以通过dpkg -L linux-firmware获取。这个清单通常有几千行,直接看会眼花。我的做法是先按目录归类,统计每个子目录的大小:
dpkg -L linux-firmware | grep -v '/$' | xargs -I{} du -b {} 2>/dev/null | \ awk '{sum[$4]+=$1} END {for (d in sum) print sum[d], d}' | sort -rn | head -30这条命令会输出占用空间最大的前 30 个目录。在我的系统上,结果大致如下:
| 目录 | 占用空间 | 说明 |
|---|---|---|
| /usr/lib/firmware/mellanox | 约 180 MB | Mellanox 网卡固件,N100 用不上 |
| /usr/lib/firmware/qcom | 约 120 MB | 高通系列固件,用不上 |
| /usr/lib/firmware/i915 | 约 90 MB | Intel 核显固件,部分需要 |
| /usr/lib/firmware/amdgpu | 约 85 MB | AMD 显卡固件,用不上 |
| /usr/lib/firmware/nvidia | 约 70 MB | NVIDIA 显卡固件,用不上 |
| /usr/lib/firmware/rtl_nic | 约 15 MB | Realtek 网卡固件,部分需要 |
| /usr/lib/firmware/rtw88 | 约 12 MB | Realtek 无线固件,视配置 |
| /usr/lib/firmware/iwlwifi | 约 25 MB | Intel 无线固件,视配置 |
从这张表就能看出,光是 Mellanox、qcom、amdgpu、nvidia 这四个目录加起来就超过 450 MB,而它们对于 N100 节点来说完全是冗余的。
2.3 构建精简固件包的三种方案对比
在动手之前,我评估了三种可行方案:
方案一:直接删除文件并锁定包版本。用dpkg --purge卸载原包,手动创建目录结构,只放入需要的固件文件。优点是简单直接,缺点是包管理器记录不完整,后续系统升级可能出问题。
方案二:重新打包 deb。从原包中提取文件,剔除不需要的部分,用dpkg-deb重新打包,版本号加自定义后缀。优点是包管理记录完整,可以正常安装卸载;缺点是需要处理依赖关系和包签名。
方案三:使用 overlay 挂载。保留原包不动,在/usr/lib/firmware上挂一个 overlay 文件系统,用白名单方式屏蔽不需要的文件。优点是原包完整,随时可回滚;缺点是每次启动都要挂载,增加复杂度。
我最终选择了方案二,理由是它在"包管理完整性"和"部署简便性"之间取得了最好的平衡。重新打包后的 deb 可以像普通包一样用dpkg -i安装,也可以放进本地仓库供批量部署使用。
2.4 实操中的关键注意事项
在动手精简之前,有几个坑我必须提前说明:
不要删除正在使用的固件。有些固件在系统启动早期就被加载了,如果你在运行中的系统上删除它们,虽然当前不会崩溃,但下次重启可能无法正常初始化硬件。所以精简操作最好在离线环境或者救援模式下进行。
保留固件目录结构。linux-firmware包中的文件路径是有意义的,内核会按照特定路径查找固件。比如i915/adlp_dmc_ver2_16.bin必须放在/usr/lib/firmware/i915/目录下,不能随意移动。
注意固件版本兼容性。同一个硬件可能有多个版本的固件文件,内核会根据硬件版本自动选择。如果你只保留了其中一个版本,而硬件恰好需要另一个版本,就会加载失败。稳妥的做法是保留同一系列的所有版本,或者至少保留当前系统正在使用的版本。
测试回滚流程。在批量部署之前,一定要在一台测试节点上完整走一遍"精简-重启-验证-回滚"的流程。回滚方案要简单可靠,比如保留原版 deb 包,出问题时一条命令恢复。
提示:精简固件包后,建议运行
update-initramfs -u更新 initramfs,确保启动阶段需要的固件被正确包含。有些固件是在 initramfs 阶段加载的,如果只精简了根文件系统而没更新 initramfs,可能导致启动失败。
3. 实操过程与核心环节实现
3.1 环境准备与工具安装
我是在一台已经部署好的 N100 节点上进行操作的,系统是 Ubuntu 22.04 LTS。为了不影响生产环境,我先用systemd-nspawn创建了一个容器环境来做打包实验,确认无误后再在真实节点上执行。
需要的工具不多,都是系统自带的:
# 确认必要工具存在 which dpkg-deb fakeroot du awk grep # 如果没有 fakeroot,安装它 sudo apt install fakerootfakeroot的作用是让你在没有 root 权限的情况下模拟 root 操作,打包 deb 时需要它来设置文件属主和权限。
3.2 提取原包文件并生成需求清单
首先把原版linux-firmware包的文件提取到一个工作目录:
# 创建工作目录 mkdir -p ~/firmware-slim/work cd ~/firmware-slim/work # 下载原包(如果本地已有,直接从 /var/cache/apt/archives 复制) apt-get download linux-firmware # 解包 dpkg-deb -R linux-firmware_*.deb extracted解包后,extracted/usr/lib/firmware就是完整的固件目录。接下来生成需求清单。我写了一个小脚本,把dmesg中加载的固件路径全部提取出来:
#!/bin/bash # 提取当前系统加载的固件列表 dmesg | grep -oP 'firmware: direct-loading firmware \K[^ ]+' | sort -u > loaded_firmware.txt # 同时提取可能通过其他方式加载的固件 dmesg | grep -oP 'firmware: loading \K[^ ]+' | sort -u >> loaded_firmware.txt sort -u loaded_firmware.txt -o loaded_firmware.txt cat loaded_firmware.txt在我的 N100 节点上,这个列表大约有 20 多个文件,主要包括:
i915/adlp_dmc_ver2_16.bini915/adlp_guc_70.bini915/adlp_guc_70_16.binrtl_nic/rtl8168h-2.fwintel/ibt-0040-0041.sfiintel/ibt-0040-0041.ddc
除此之外,我还手动补充了一些"可能用到但当前未加载"的固件,比如 USB 网卡、常见 USB 声卡的固件。这些设备虽然当前没插,但考虑到后续可能用到,我保留了一个最小集合。
3.3 编写精简脚本并执行剔除
有了需求清单,就可以写精简脚本了。核心逻辑是:遍历extracted/usr/lib/firmware下的所有文件,如果不在白名单中,就删除。
#!/bin/bash set -e WORK_DIR=~/firmware-slim/work FIRMWARE_DIR="$WORK_DIR/extracted/usr/lib/firmware" WHITELIST="$WORK_DIR/loaded_firmware.txt" # 额外保留的固件(根据实际需求调整) EXTRA_KEEP=( "rtl_nic/rtl8168h-2.fw" "rtl_nic/rtl8168g-2.fw" "rtl_nic/rtl8168e-3.fw" "intel/ibt-0040-0041.sfi" "intel/ibt-0040-0041.ddc" ) # 构建完整白名单 cat > "$WORK_DIR/keep_list.txt" << 'EOF' i915/adlp_dmc_ver2_16.bin i915/adlp_guc_70.bin i915/adlp_guc_70_16.bin rtl_nic/rtl8168h-2.fw rtl_nic/rtl8168g-2.fw rtl_nic/rtl8168e-3.fw intel/ibt-0040-0041.sfi intel/ibt-0040-0041.ddc EOF # 遍历并删除不在白名单中的文件 cd "$FIRMWARE_DIR" find . -type f | while read -r file; do rel_path="${file#./}" if ! grep -qxF "$rel_path" "$WORK_DIR/keep_list.txt"; then rm -f "$file" fi done # 删除空目录 find . -type d -empty -delete # 统计精简后的大小 du -sh "$FIRMWARE_DIR"执行这个脚本后,/usr/lib/firmware目录从 1.2 GB 降到了约 720 MB。我又检查了一遍,确认没有误删需要的文件。
3.4 重新打包并安装
精简完成后,用dpkg-deb重新打包。关键是要修改控制文件中的版本号,加上自定义后缀,避免被系统更新覆盖。
cd ~/firmware-slim/work/extracted # 修改控制文件 sed -i 's/^Version: .*/Version: 20220329.git681281e4-0ubuntu3.14+slim1/' DEBIAN/control # 可选:修改包名,避免与原包冲突 # sed -i 's/^Package: .*/Package: linux-firmware-slim/' DEBIAN/control # 重新打包 cd ~/firmware-slim/work fakeroot dpkg-deb -b extracted linux-firmware-slim_20220329.git681281e4-0ubuntu3.14+slim1_all.deb # 查看包信息 dpkg-deb -I linux-firmware-slim_*.deb打包完成后,在测试节点上安装:
# 先卸载原包(注意:这会删除所有固件文件,确保你有回滚方案) sudo dpkg -r linux-firmware # 安装精简包 sudo dpkg -i linux-firmware-slim_*.deb # 更新 initramfs sudo update-initramfs -u # 重启验证 sudo reboot重启后,用dmesg | grep -i firmware检查固件加载情况。如果所有设备都正常初始化,说明精简成功。
3.5 批量部署与自动化脚本
单台节点验证通过后,我把整个流程脚本化,用于批量部署。脚本的核心逻辑是:
- 检查当前系统是否已安装精简包,如果是则跳过。
- 备份原版
linux-firmware包到/root/firmware-backup/。 - 安装精简包并更新 initramfs。
- 记录操作日志,便于审计和回滚。
#!/bin/bash # deploy_firmware_slim.sh set -e BACKUP_DIR="/root/firmware-backup" SLIM_DEB="/opt/firmware-slim/linux-firmware-slim_*.deb" mkdir -p "$BACKUP_DIR" # 备份原包 if [ ! -f "$BACKUP_DIR/linux-firmware-original.deb" ]; then apt-get download linux-firmware -o Dir::Cache::archives="$BACKUP_DIR" mv "$BACKUP_DIR"/linux-firmware_*.deb "$BACKUP_DIR/linux-firmware-original.deb" fi # 检查是否已安装精简包 if dpkg -l | grep -q "linux-firmware-slim"; then echo "精简固件包已安装,跳过。" exit 0 fi # 安装精简包 dpkg -r linux-firmware || true dpkg -i $SLIM_DEB update-initramfs -u echo "精简固件包安装完成,请重启验证。"这个脚本可以通过 Ansible 或类似的配置管理工具推送到所有节点执行。实测下来,单台节点的操作时间不超过 2 分钟,重启后所有板载设备均正常工作。
4. 常见问题与排查技巧实录
4.1 固件加载失败导致硬件不工作
这是最常见的问题。表现是某个设备在lspci或lsusb中能看到,但dmesg中有固件加载失败的报错,设备无法正常使用。
排查思路:
- 确认
dmesg | grep -i firmware中的报错信息,找到缺失的固件文件名。 - 检查该文件是否在精简包中存在。如果不存在,说明白名单漏了。
- 从原版包中提取该文件,放回
/usr/lib/firmware对应目录。 - 重新加载驱动模块:
sudo modprobe -r <模块名> && sudo modprobe <模块名>。 - 如果模块无法卸载,重启系统。
我遇到过一次网卡固件缺失的情况。原因是 N100 节点有两个网卡版本,我测试的那台用的是 Realtek RTL8168H,而另一批节点用的是 RTL8168G。白名单里只保留了 H 版本的固件,导致 G 版本节点网卡不工作。解决办法是把rtl_nic目录下所有rtl8168开头的固件都保留,总共也就多占几 MB。
4.2 系统更新后固件包被覆盖
Ubuntu 的linux-firmware包更新频率不低,每次内核更新都可能带来新的固件包版本。如果精简包的版本号没有正确设置,apt upgrade时可能会把原版包重新装回来。
避免方法:
- 在控制文件中把版本号设置为比当前仓库版本更高的值,比如加
+slim1后缀。 - 使用
apt-mark hold linux-firmware-slim锁定包版本,防止自动更新。 - 如果确实需要更新固件,手动下载新版本原包,重新执行精简流程。
注意:锁定包版本后,系统不会自动获取固件安全更新。如果某些固件存在安全问题,需要手动评估是否更新。对于封闭的内网环境,这个风险相对可控。
4.3 initramfs 未更新导致启动失败
有些固件是在 initramfs 阶段加载的,比如某些存储控制器和网络启动相关的固件。如果只精简了根文件系统中的固件,而没有更新 initramfs,启动时可能找不到需要的固件,导致系统无法正常启动。
预防措施:
- 每次修改固件包后,务必执行
sudo update-initramfs -u。 - 在
/etc/initramfs-tools/initramfs.conf中确认MODULES配置,确保需要的驱动模块被包含。 - 如果系统使用了全盘加密或网络启动,更要谨慎测试。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查命令 | 解决方法 |
|---|---|---|---|
| 网卡不工作 | 网卡固件缺失 | dmesg | grep r8169 | 补充对应 rtl_nic 固件 |
| 显卡驱动异常 | i915 固件缺失 | dmesg | grep i915 | 补充 i915 目录下对应固件 |
| 蓝牙不可用 | 蓝牙固件缺失 | dmesg | grep btusb | 补充 intel/ibt 系列固件 |
| 系统更新后固件恢复 | 包版本未锁定 | apt-cache policy linux-firmware | 设置版本号后缀并 hold |
| 启动卡住 | initramfs 未更新 | lsinitramfs /boot/initrd.img-* | grep firmware | 执行 update-initramfs -u |
| 固件加载但设备仍异常 | 固件版本不匹配 | ethtool -i eth0 | 保留同系列所有版本固件 |
4.5 独家避坑经验
保留一份"应急固件包"。我在每个节点的/root/firmware-backup/下都放了一份原版linux-firmware的 deb 包。万一精简后遇到问题,可以进入救援模式,用dpkg -i快速恢复。这个包大约 500 MB,但考虑到它能在关键时刻救急,这点空间值得花。
不要追求极致精简。我一开始想把固件目录压到 500 MB 以下,结果发现有些固件虽然当前没加载,但在特定电源管理状态下会被调用。后来我把目标调整为 750 MB 左右,保留了所有"可能用到"的固件,实际省下 450 MB,已经达到预期。
记录硬件清单。每个节点的硬件配置可能略有不同,我在部署前用lspci -nn和lsusb生成了硬件指纹,保存在/etc/hardware-fingerprint.txt。这样后续如果出现固件问题,可以快速对照硬件清单排查。
测试休眠和唤醒。有些固件是在系统休眠或唤醒时加载的,日常使用中不会触发。我在测试节点上专门跑了systemctl suspend和唤醒流程,确认精简后休眠功能正常。这一步很容易被忽略,但一旦出问题就是大问题。
关注内核版本变化。内核升级可能引入新的固件依赖。我在每次内核更新后都会检查dmesg中的固件加载记录,如果有新的固件需求,及时补充到白名单中。这个习惯帮我避免了好几次潜在的硬件故障。
5. 精简效果验证与长期维护
5.1 量化验证:省了多少,影响了什么
在批量部署 20 台 N100 节点后,我统计了实际效果:
| 指标 | 精简前 | 精简后 | 变化 |
|---|---|---|---|
| /usr/lib/firmware 占用 | 1.2 GB | 约 750 MB | -450 MB |
| 根文件系统占用 | 8.5 GB | 8.05 GB | -450 MB |
| 启动时间 | 12.3 秒 | 12.1 秒 | 基本持平 |
| 板载设备可用性 | 全部正常 | 全部正常 | 无影响 |
| 系统更新频率 | 每月 1-2 次 | 手动控制 | 更可控 |
启动时间基本没变,因为固件加载本身耗时很短,精简的主要收益在存储空间。对于 64 GB eMMC 的节点来说,450 MB 的释放意味着可以多存约 3 天的采集数据,或者多跑一个轻量级容器。
5.2 长期维护:固件白名单的迭代
精简固件包不是一劳永逸的事。随着系统更新和硬件变化,白名单需要定期维护。我的做法是:
- 每季度检查一次
dmesg中的固件加载记录,对比白名单,看是否有新增需求。 - 每次内核大版本更新后,重新生成一次需求清单。
- 如果新增了硬件设备,先在原版固件环境下测试,确认需要的固件文件后再更新白名单。
维护脚本我放在了内部仓库里,每次更新白名单后重新打包,版本号递增。这样所有节点都能通过配置管理工具获取到最新的精简包。
5.3 适用场景与不适用场景
这套方案在以下场景中效果最好:
- 硬件配置统一的批量节点,比如边缘计算节点、工控机、教学实验室的固定配置机器。
- 存储空间紧张的设备,比如 eMMC 存储的迷你主机、嵌入式板卡。
- 网络环境受限,无法频繁下载大体积系统更新的场景。
不适用的情况也很明确:
- 硬件配置经常变动的开发机,今天插个 USB 网卡,明天加个扩展卡,固件需求随时变。
- 需要支持多种硬件的通用镜像制作,精简后可能在某些机器上无法启动。
- 对系统更新及时性要求极高的场景,锁定固件包版本意味着可能错过安全更新。
我个人在实际操作中的体会是,精简固件包这件事,技术难度不高,但需要细心和耐心。最大的挑战不是删除文件,而是确保删除后系统依然稳定。每次精简后至少观察一周,确认没有偶发的硬件初始化问题,再推广到更多节点。这个等待期不能省,因为有些固件问题只在特定条件下才会暴露,比如冷启动、休眠唤醒、或者高负载场景。