构建一个可通过 wget | dd 直接写入块设备的极简 Linux 发行版,核心挑战在于打造一个足够小巧但功能完备的 initramfs。原始 NixOS 救援镜像高达 477MB,经过系统性的裁剪优化后,最终可压缩至约 6.1MB。这一过程涉及对系统依赖的深度梳理、对启动流程的精确控制,以及对每个组件存在价值的重新审视。

initramfs 的本质与精简逻辑

initramfs 本质上是一个打包在 cpio 归档文件中的微型根文件系统。Linux 内核在启动时将其解压到 tmpfs 中,然后执行其中的 /init 脚本作为 PID 1 进程。这个临时文件系统的主要职责是:在真实根分区可用之前,提供必要的工具和驱动来完成系统启动。

传统发行版的 initramfs 包含大量用于磁盘检测、文件系统检查、LVM 加密卷解锁、网络配置等功能的脚本和二进制文件。以 Debian 为例,其默认 initramfs 约为 35MB,NixOS 日常使用版本约为 19MB。然而,这些功能对于一个只需完成「从网络下载镜像并写入磁盘」这一单一任务的启动环境而言,几乎全部可以移除。

精简的核心逻辑是重新审视每个软件包的存在必要性。如果某个组件的功能在目标场景中不会被用到,就可以将其从构建配置中剔除。这包括但不限于:文件系统检查工具(fsck 系列)、加密相关库、LVM/RAID 管理工具、网络管理服务(NetworkManager)、包管理工具(nix、apt)等。

驱动加载的工程实践

由于极简 initramfs 不包含 udev 服务,需要手动完成硬件驱动的加载过程。驱动加载的目标是确保系统能够识别存储设备和网络接口。

存储设备的识别依赖于 SCSI 子系统。现代 Linux 中,无论 SATA、NVMe 还是 USB 存储设备,最终都通过 SCSI 驱动层呈现为 /dev/sdX/dev/nvmeXn1 设备。加载存储驱动需要两个步骤:首先加载 SCSI 核心模块,然后加载具体的设备驱动。

# 加载 SCSI 磁盘层
modprobe sd_mod 2>/dev/null
modprobe sr_mod 2>/dev/null  # 用于 CD/DVD

网络设备的识别则更为复杂。现代系统普遍使用 PCI 总线连接网卡,驱动通过 PCI modalias 自动匹配是较为可靠的方式。具体做法是遍历 /sys/bus/pci/devices/*/modalias 文件,将读取到的模块别名字符串传递给 modprobe 命令。

# 遍历 PCI 设备并加载对应驱动
for f in /sys/bus/pci/devices/*/modalias; do
    modprobe -b "$(cat $f)" 2>/dev/null
done

# USB 设备需要在 USB 主机控制器加载后重新扫描
for f in /sys/bus/usb/devices/*/modalias; do
    modprobe -b "$(cat $f)" 2>/dev/null
done

部分网络驱动依赖特定的协议层模块。例如,某些内核配置中需要显式加载 af_packet 模块才能提供原始套接字支持,这对后续的 DHCP 客户端运行至关重要。

网络配置与 DHCP 客户端

initramfs 环境中的网络配置完全在用户空间完成。Linux 内核本身不提供任何网络功能,IP 地址的获取完全依赖用户空间的 DHCP 客户端。Busybox 内置的 udhcp 客户端是轻量级的选择。

# 启用网络接口并启动 DHCP 客户端
ip link set eth0 up
udhcpc --script /etc/dhcpevent.sh &
udhcpc6 --script /etc/dhcpevent.sh &  # IPv6 支持

需要注意的是,DHCP 客户端的启动与网络接口的就绪之间存在竞态条件。脚本需要在尝试网络访问前等待 DHCP 过程完成。一个常用的模式是轮询测试网络连通性:

until wget --spider "$src"; do
    echo "等待网络就绪..."
    sleep 5
done

特殊文件系统挂载

Linux 系统中 /dev/proc/sys 三个目录并非自然存在,而是由 init 进程负责挂载的虚拟文件系统。devtmpfs 提供设备节点文件系统,procfs 提供进程和系统信息接口,sysfs 提供内核对象层次结构。

# 挂载必要的虚拟文件系统
mkdir -p /dev /proc /sys
mount -t devtmpfs devtmpfs /dev
mount -t proc procfs /proc
mount -t sysfs sysfs /sys

这些文件系统是后续驱动加载和系统查询的基础。devtmpfs 会在内核设备初始化时自动创建设备节点,而 procfs 和 sysfs 则提供了查询系统状态的接口。

镜像构建工具链

使用 Nix 构建 initramfs 可以利用其声明式的依赖管理。通过 pkgs.makeInitrdNG 函数可以打包自定义的 initramfs。然而,直接使用 NixOS 模块系统生成的 initramfs 体积庞大,原因是 NixOS 默认包含完整的 systemd 和大量模块。

更高效的做法是完全绕过 NixOS 模块系统,直接在 Nix 表达式中声明所需的组件。这包括:Linux 内核(bzImage)、Busybox(提供基础命令和 udhcp)、glibc 共享库、以及自定义的 init 脚本。

构建结果的典型目录结构如下:根目录包含指向 nix store 中实际二进制文件的符号链接(遵循 FHS 标准),init 脚本位于根目录,必要的配置文件(如 DHCP 事件脚本)位于 etc 目录。

精简效果与关键参数

通过系统性裁剪,可实现显著的体积缩减:禁用 Nix 移除约 121MB,使用 lib.mkForce 强制只包含必要包减少约 28MB,切换到 systemd-minimal 减少约 100MB,禁用各类系统服务再减少数十 MB。

最终 6.1MB 的 initramfs 包含:约 4MB 的 Busybox 及相关工具链、内核模块目录、以及必要的网络配置脚本。这个体积甚至小于多数发行版的内核镜像,适合通过网络快速分发。

需要注意的是,精简后的 wget 实现默认不验证 SSL 证书,这是体积与安全之间的典型权衡。对于概念验证类应用可以接受,生产环境则需要重新编译 Busybox 并启用 TLS 支持。

资料来源:本文技术实现细节参考 astrid.tech 上关于极简 Linux 发行版构建的系列博客,作者完整记录了从 292MB 压缩至 6.1MB 的全过程,并开源了相关构建脚本。