1. 为什么我要啃 debian-installer 这块硬骨头
第一次接触 debian-installer 的人,十有八九是被同一个场景逼过来的:想给一批机器做自动化装机,或者想定制一个属于自己的安装镜像,结果发现网上能找到的资料要么是"点下一步"的图形化安装教程,要么是直接甩给你一堆 udeb 包名让你自己悟。真正能把 d-i 的启动链路、组件加载机制、preseed 配置逻辑讲清楚的内容少得可怜。
debian-installer(后面我统一简称 d-i)是 Debian 系发行版的核心安装器,它不是一个单一程序,而是一整套由上百个 udeb 包组成的模块化系统。你在安装界面看到的每一个步骤——选语言、分区、装基础系统、配 apt 源——背后都是独立的组件在跑。理解这套机制的价值在于:你能精确控制安装过程的每一个环节,而不是被安装器的默认行为牵着走。
这篇文章适合三类人看。第一类是想做批量自动化部署的运维,需要靠 preseed 把装机这件事彻底脚本化;第二类是想定制安装镜像的开发者,需要往 initrd 里塞自己的组件或者替换默认行为;第三类是单纯想搞明白"Linux 安装器到底怎么工作的"技术爱好者。不管你是哪一类,我都会从启动链路开始,一层层拆到组件加载和 preseed 的实战细节,尽量把每个"为什么这么设计"讲透。
先说一个我踩过的坑作为引子。早期我以为 d-i 就是个普通的安装程序,改改配置文件就能定制。结果第一次尝试往 initrd 里加组件时,发现整个启动流程卡在"detect hardware"阶段不动了,日志里全是 udeb 加载失败的报错。后来才明白,d-i 的组件加载是有严格依赖顺序的,你塞进去的东西如果依赖没满足,整个 anna(the anna installer,负责组件加载的核心程序)就会卡住。这个教训让我意识到,玩 d-i 必须先把它的架构搞清楚,否则就是盲人摸象。
2. d-i 的启动链路:从引导到 anna 接管
2.1 三种引导方式的差异与选择
d-i 支持多种引导方式,最常见的是 BIOS 引导、UEFI 引导,以及网络引导(PXE)。这三种方式在 d-i 的视角下,最终都会走到同一个地方:加载内核和 initrd,然后把控制权交给 initrd 里的 init 进程。但它们的差异体现在引导介质和参数传递上。
BIOS 引导走的是传统的 MBR 路径,引导程序通常是 isolinux 或 syslinux。UEFI 引导则用 GRUB 或 EFI stub,引导分区的格式和文件布局完全不同。PXE 引导更特殊,它通过网络从 TFTP 服务器拉取内核和 initrd,适合无盘工作站或者批量部署场景。
我实测下来,如果你只是做本地装机,用 UEFI + GRUB 的组合最省心,因为 GRUB 的配置灵活,调试信息也清晰。但如果你要做 PXE 批量部署,就得额外注意 DHCP 的 next-server 和 filename 参数配置,这两个参数配错了,机器连内核都拉不下来。
提示:不管用哪种引导方式,内核命令行参数都是关键。d-i 的行为很大程度上由这些参数决定,比如
auto=true会跳过大部分交互,priority=critical只显示关键问题。这些参数后面会详细讲。
2.2 initrd 里到底装了什么
initrd(或者叫 initramfs)是 d-i 的核心载体。它本质上是一个压缩的 cpio 归档,里面包含了 d-i 运行所需的最小根文件系统。你把这个归档解开,会看到熟悉的目录结构:/bin、/sbin、/lib、/etc,但里面的内容和你平时用的 Debian 系统完全不是一回事。
d-i 的 initrd 里装的主要是 udeb 包解压出来的文件。udeb 是 "micro deb" 的缩写,是 Debian 包的一种精简形式,去掉了文档、man page 等非必要内容,只保留运行所需的二进制和库。initrd 里的 init 进程是 busybox 提供的,它负责挂载必要的文件系统、加载内核模块,然后启动 d-i 的主程序。
这里有个容易混淆的点:initrd 里的组件并不是一次性全部加载的。d-i 采用按需加载的策略,初始 initrd 只包含最基础的组件,比如内核模块加载器、文件系统驱动、网络基础组件。其他组件(比如分区工具、文件系统格式化工具、apt 配置工具)都是后续通过 anna 从安装介质或网络源里动态加载的。这个设计的好处是 initrd 可以保持很小,缺点是如果组件源配置错了,安装过程就会卡在半路。
2.3 anna 的工作机制与组件依赖
anna 是 d-i 里负责组件加载的核心程序,名字来源于 "ANother NAsty acronym"。它的工作逻辑其实不复杂:读取一个组件列表,按顺序从安装介质里找到对应的 udeb 包,解压并安装到运行环境中,然后执行组件的 postinst 脚本。
但这里有个关键细节:组件之间是有依赖关系的。比如分区工具 partman 依赖 libparted 相关的库,网络配置工具 netcfg 依赖网卡驱动和 DHCP 客户端。anna 在加载组件时会检查依赖,如果依赖没满足,它会尝试先加载依赖组件。如果依赖组件找不到,整个加载过程就会失败。
我踩过的那个坑就出在这里。当时我往 initrd 里塞了一个自定义组件,但这个组件依赖一个没被包含的库,anna 加载时直接报错退出,导致安装流程卡死。后来我的做法是:先用udpkg -c检查组件的依赖关系,确保所有依赖都在组件列表里,或者手动把依赖包也塞进去。
组件列表通常放在/var/lib/dpkg/status或者安装介质上的udeb.list文件里。你可以通过修改这个列表来控制哪些组件被加载。对于定制镜像来说,这是最直接的干预点。
3. preseed 实战:把交互式安装变成全自动
3.1 preseed 文件的加载时机与优先级
preseed 是 d-i 实现自动化安装的核心机制。它的本质是一个预置答案的文件,d-i 在安装过程中遇到需要用户输入的问题时,会先去 preseed 文件里找答案,找到了就直接用,找不到才弹交互界面。
preseed 文件的加载时机很关键。它可以在内核命令行里通过preseed/file=参数指定,也可以放在安装介质上的特定路径(比如/preseed.cfg),还可以通过网络从 HTTP 服务器拉取。不同加载方式的优先级不同,内核命令行指定的优先级最高,会覆盖其他来源的同名配置。
我一般推荐把 preseed 文件放在安装介质上,同时在内核命令行里指定路径。这样做的好处是离线也能用,不依赖网络。如果你要做 PXE 批量部署,那就把 preseed 放在 HTTP 服务器上,通过preseed/url=参数指定,这样改配置不用重新打包镜像。
注意:preseed 文件的语法是
d-i <question> <type> <value>的形式。比如d-i debian-installer/locale string en_US就是设置语言。类型必须匹配,string 类型的值你写成 boolean 会直接报错。
3.2 分区配置的坑与正确写法
分区是 preseed 里最容易出问题的部分。d-i 的分区配置通过partman相关的 question 来控制,语法比较绕,我第一次写的时候反复试了七八次才跑通。
最简单的写法是用partman-auto/disk指定磁盘,然后用partman-auto/method指定分区方法(regular、lvm、crypto),最后用partman-auto/choose_recipe选择预定义的分区方案。但预定义方案往往不满足需求,这时候就得自己写 recipe。
Recipe 的语法是这样的:<size> <priority> <filesystem> <mountpoint>。比如500 100 ext4 /boot表示创建一个 500MB、优先级 100、ext4 格式、挂载到 /boot 的分区。优先级决定了分区创建的先后顺序,数字越大越先创建。最后一行通常用1 1 linux-swap这种写法来分配剩余空间。
我踩过的坑是:recipe 里的 size 单位默认是 MB,但如果你写100%就表示占用剩余空间的百分比。有一次我写了个100%的分区放在中间,结果后面的分区全都没空间创建了。正确的做法是把百分比分区放在最后一行。
3.3 网络与 apt 源的自动化配置
网络配置在 preseed 里相对简单,但有几个细节容易忽略。netcfg/enable必须设为 true,否则网络配置会被跳过。netcfg/choose_interface可以指定网卡,用auto让它自动选择。如果是 DHCP,netcfg/get_ipaddress这些就不用填;如果是静态 IP,那就得把 IP、网关、DNS 都配全。
apt 源的配置通过mirror/country、mirror/http/hostname、mirror/http/directory这几个 question 来控制。我一般会把mirror/country设为manual,然后直接指定 hostname 和 directory,这样能精确控制用哪个源。
有个坑是:如果你用的源需要特定的 apt 配置(比如代理或者特定的 release 名称),光靠 preseed 的 mirror 配置可能不够。这时候可以用preseed/late_command在安装后期执行自定义脚本,直接往/target/etc/apt/sources.list里写内容。late_command 是 preseed 里最强大的功能之一,几乎所有 preseed 覆盖不到的配置都能靠它搞定。
4. 定制 initrd:往安装器里塞私货
4.1 解包与重新打包 initrd 的正确姿势
定制 initrd 的第一步是把它解开。d-i 的 initrd 通常是 gzip 压缩的 cpio 归档,解包命令是zcat initrd.gz | cpio -idmv。解完之后你会看到一个完整的目录树,这就是 d-i 的运行环境。
重新打包的时候要注意:cpio 的归档格式必须和原来一致,否则引导程序可能认不出来。我一般用find . | cpio -o -H newc | gzip -9 > ../initrd.gz这个命令,-H newc指定用 newc 格式,这是 Linux 内核推荐的 initramfs 格式。
有个细节是文件权限。initrd 里的文件权限必须正确,特别是可执行文件和目录。如果你在解包和打包过程中用了错误的 umask,可能会导致权限丢失,引导时直接报 "permission denied"。我的做法是解包前先记录原始权限,打包后用cpio -tv对比一下。
4.2 添加自定义 udeb 组件的完整流程
往 initrd 里加自定义组件,本质上是把你的程序打包成 udeb 格式,然后放到组件列表里让 anna 加载。udeb 的打包和普通 deb 类似,但控制文件里要加上XC-Package-Type: udeb这一行。
打包完成后,把 udeb 放到 initrd 的/var/cache/anna/目录下(或者安装介质上的对应位置),然后修改组件列表文件,把你的组件加进去。组件列表的格式是每行一个包名,anna 会按顺序加载。
这里有个经验:如果你的组件需要在特定阶段执行,可以在组件的 postinst 脚本里判断当前安装阶段。d-i 会把当前阶段信息放在环境变量里,比如$DEBCONF_DB_REPLACE或者通过 debconf 的 question 来查询。我一般会在 postinst 里加日志输出,方便调试。
4.3 组件加载失败的排查链路
组件加载失败是定制 initrd 时最常见的问题。排查链路我总结成三步:先看 anna 的日志,再看组件的依赖,最后看文件权限。
anna 的日志通常在/var/log/syslog或者直接输出到控制台。如果日志里说 "cannot find package",那就是组件列表里的包名写错了,或者 udeb 文件没放对位置。如果说 "dependency not satisfied",那就是依赖没满足,用udpkg -c检查一下依赖关系。
文件权限问题比较隐蔽。有一次我打包 udeb 时用了 root 权限,解压出来的文件属主是 root,但 d-i 运行时的用户可能不是 root,导致组件执行失败。后来我统一在打包时用--owner=0 --group=0参数,确保文件属主是 root,这样就不会有权限问题。
5. 那些文档里不会写的实操心得
5.1 调试 d-i 的几种有效手段
调试 d-i 最直接的手段是看日志。d-i 会把安装过程的所有输出写到/var/log/下的几个文件里,其中syslog最全,debug里有更详细的调试信息。你可以在安装界面按 Alt+F2 切到控制台,用tail -f /var/log/syslog实时看日志。
另一个手段是在内核命令行里加debug参数,这会让 d-i 输出更详细的调试信息。如果问题出在组件加载阶段,加anna/verbose=true能让 anna 输出每个组件的加载过程。
还有个技巧是用shell参数。在内核命令行里加shell,d-i 会在安装开始前给你一个 shell,你可以手动检查环境、挂载文件系统、查看日志。这个手段在排查启动阶段的问题时特别有用。
5.2 版本差异带来的兼容性问题
d-i 的版本和 Debian 的版本是绑定的。Debian 11 用的 d-i 和 Debian 12 用的 d-i 在组件列表、preseed 语法、甚至 anna 的行为上都可能有差异。我遇到过最坑的一次是:在 Debian 11 上跑通的 preseed 文件,拿到 Debian 12 上直接报错,原因是某个 question 的名字变了。
所以我的建议是:preseed 文件和定制脚本一定要和 d-i 版本绑定。你可以在 preseed 文件开头加个版本检查,或者在 late_command 里根据/etc/debian_version做条件判断。另外,升级 d-i 版本时,一定要重新测试所有 preseed 配置,不要想当然地认为向后兼容。
5.3 性能与体积的平衡取舍
定制 initrd 时,体积和功能是一对矛盾。你塞进去的组件越多,initrd 越大,引导时间越长。但组件太少,又可能满足不了安装需求。
我的经验是:只把安装必需的组件放进 initrd,其他组件通过 anna 从安装介质或网络源动态加载。比如分区工具、文件系统格式化工具这些是必需的,但像某些不常用的文件系统驱动、额外的语言包,完全可以放到安装介质上按需加载。
另外,initrd 的压缩算法也影响体积和引导速度。gzip 压缩率高但解压慢,xz 压缩率更高但解压更慢,lz4 压缩率低但解压快。我一般用 gzip,因为它在兼容性和性能之间比较平衡。如果你的硬件性能足够,可以试试 xz,能省不少空间。
6. 从一次真实定制需求看 d-i 的扩展边界
6.1 需求拆解:定制镜像到底要改什么
假设你有一个需求:做一个定制安装镜像,要求安装时自动分区、自动配置网络、自动装一批特定软件包,并且安装完成后自动执行一些初始化脚本。这个需求拆解下来,涉及 d-i 的四个扩展点:preseed 配置、组件定制、late_command 脚本、以及安装后的首次启动脚本。
preseed 负责自动化安装过程,late_command 负责安装后的配置,首次启动脚本负责系统第一次启动时的初始化。这三个环节串起来,就是一个完整的自动化装机链路。
我实际做这个需求时,最大的挑战不是写配置,而是调试。因为安装过程是一次性的,出了问题只能重来。后来我的做法是:先在虚拟机里跑,用快照功能保存每个阶段的进度,出问题就回滚到上一个快照,这样能大幅缩短调试周期。
6.2 扩展点的选择与组合策略
d-i 的扩展点很多,但不是每个需求都需要全部用上。我的策略是:能用 preseed 解决的,就不写脚本;能用 late_command 解决的,就不改 initrd。因为改 initrd 的成本最高,调试也最麻烦。
preseed 能覆盖大部分安装配置需求,包括分区、网络、apt 源、软件包选择。late_command 能覆盖安装后的配置需求,包括写配置文件、装额外软件、执行初始化脚本。只有当你需要改变安装器本身的行为时,才需要改 initrd 或者加自定义组件。
这个策略的核心逻辑是:尽量在高层做扩展,避免动底层。因为底层改动的影响面大,调试成本高,而且容易和 d-i 的版本升级冲突。
6.3 验证与回滚:定制镜像的测试方法
定制镜像的测试必须覆盖完整安装流程。我的测试清单包括:引导是否正常、组件加载是否完整、preseed 是否生效、分区是否正确、网络是否通、软件包是否装全、late_command 是否执行、首次启动脚本是否运行。
测试环境我推荐用虚拟机,因为快照和回滚方便。物理机测试虽然更真实,但出问题后恢复成本高。如果必须用物理机,那就准备一块可擦写的硬盘,每次测试前先擦干净。
回滚策略也很重要。定制镜像一旦发布,如果发现问题,回滚方案是什么?我的做法是保留上一个稳定版本的镜像,新版本先在小范围测试,确认没问题再全量推送。另外,preseed 文件和 late_command 脚本要版本化管理,方便追溯和回滚。
7. 我在这条路上踩过的几个典型坑
7.1 组件依赖循环导致的死锁
有一次我加了一个自定义组件,这个组件依赖 A,A 又依赖 B,B 反过来依赖我的组件,形成了一个循环依赖。anna 在加载时陷入了死循环,安装界面卡在 "loading components" 不动。
排查这个问题的关键是看 anna 的日志。日志里会显示它尝试加载的组件顺序,如果看到同一个组件被反复加载,那就是循环依赖了。解决方法是打破循环,把公共依赖抽出来单独加载,或者调整组件的加载顺序。
这个坑让我明白一个道理:d-i 的组件依赖不是随便设计的,每个组件的依赖关系都有其道理。加自定义组件时,一定要先理清依赖链,避免引入循环。
7.2 preseed 语法错误导致的静默失败
preseed 文件里如果有一行语法错误,d-i 不一定会报错,而是会静默忽略这一行,然后弹交互界面让你手动输入。这种静默失败最坑,因为你以为配置生效了,结果安装到一半停下来等你输入。
我后来养成了一个习惯:preseed 文件写完后,用debconf-set-selections命令先验证一遍。这个命令会检查语法,有错误会直接报出来。另外,安装时加priority=critical参数,这样只有关键问题才会弹交互界面,非关键问题如果 preseed 没配,会直接用默认值。
7.3 initrd 打包格式错误引发的引导失败
initrd 打包时如果格式不对,引导程序会直接报 "invalid ramdisk" 或者 "not a valid initramfs"。我遇到过两次,一次是 cpio 格式用错了,一次是压缩算法不匹配。
排查这个问题的关键是看引导程序的报错信息。如果是格式问题,报错通常很明确。解决方法是确保打包命令和原始 initrd 的格式一致。你可以用file initrd.gz查看原始 initrd 的格式,然后照着打包。
还有个细节是:initrd 的文件名和引导配置里的文件名必须一致。我有一次改了 initrd 的文件名,但忘了改 GRUB 配置,结果引导时找不到文件,直接进了 GRUB rescue 模式。
8. 关于 d-i 后续可以深入的方向
d-i 的扩展能力远不止我上面讲的这些。如果你已经跑通了基本的定制流程,可以往这几个方向深入:一是研究 d-i 的 debconf 数据库机制,理解 question 的注册和回答流程,这能让你更精确地控制安装行为;二是研究 d-i 的硬件检测机制,理解它如何识别硬件并加载对应的驱动,这对定制特定硬件的安装镜像很有用;三是研究 d-i 的网络安装机制,理解它如何从网络源加载组件和软件包,这对做网络批量部署很关键。
我个人在实际操作中的体会是:d-i 的学习曲线陡峭,但一旦理解了它的架构,后面的定制就是按图索骥。最怕的是一上来就改配置,不改架构,出了问题也不知道从哪查。所以我的建议是:先花时间把启动链路和组件加载机制搞清楚,再动手做定制。这样你踩的坑会少很多,调试效率也会高很多。
最后分享一个小技巧:d-i 的源码是可以直接看的,apt-get source debian-installer就能拉到。源码里的build/目录有打包脚本,debian/目录有控制文件,doc/目录有开发文档。遇到搞不明白的行为,直接翻源码比在网上搜答案靠谱得多。