引子
最近需要使用 PXE 启动 Linux 系统来做一些自动化工作,所以学习了一下 (i)PXE 相关的知识,这里做一个记录。
什么是 PXE
如果你经常调整启动项,你会在系统的 BIOS 菜单中找到一个叫做 PXE 的东西。它的全名是 Preboot Execution Environment,中文翻译为“预启动执行环境”。简单来说它是一种使用网口(网卡)来启动系统的机制,它让电脑无需依赖本地的存储设备(硬盘,U盘等)就能启动。
可以想象,即使本地磁盘中没有安装系统,使用 PXE 也能引导系统启动。这种机制在批量化安装服务器等设备中很常见。
PXE 与 iPXE
PXE 机制起初由 Intel 在 1980 年左右设计,如今使用的 PXE 已经是经过很多次完善后的版本,与之前使用的方案也有了很大的区别。即使如此,对于这样一个古老的协议,为了保证简洁和兼容性,它也有一定的历史包袱。
为了扩展 PXE 的功能,又出现了 gPXE 和 iPXE。不过前者在 2010 年已经凉凉了,目前只剩下 iPXE。
Linux 启动的流程
想要正确理解并配置 PXE 就需要对 Linux 的启动流程有一个大致的了解。现在,绝大多数发行版默认(推荐)的启动方法主要依赖两个文件:
- linux 内核文件
- initramfs 文件
下面,我们来完整地说一下硬盘启动流程。当你按下电脑的电源按钮后:
- 硬件会加载 BIOS,它会对硬件系统进行自检并初始化,BIOS 会根据启动顺序寻找对应的硬件设备
- 不论磁盘是 MBR 还是 GPT 分区 BIOS 会根据分区类型采用对应的方法,寻找磁盘中系统的 Bootloader(如GRUB)并加载
- 用户可以在 Bootmanager (图形化界面)中选择需要启动的系统选择好后调用 Bootloader 进行启动系统(你可以把 manager 和 loader 想象成一个程序)
- 接着 Bootloader 加载 linux 内核文件和 initramfs,内核接管 BIOS 的工作,对电脑系统进行自检,挂载 initramfs 并借助 initramfs 中的工具挂载模块、调用工具
- 真正的 root 目录被挂载,系统根据等级,依次自启动服务和应用
从上述流程可以看出,其实 PXE 的启动流程就是把“步骤2”和“步骤3”的从“硬盘”中寻找替换为从“网络”中寻找并获得对应文件。
sequenceDiagram participant Hardware participant BIOS participant Disk participant BootManager participant BootLoader participant Kernel participant Initramfs participant RootFS Hardware->>BIOS: 通电自检(POST) BIOS->>Disk: 按启动顺序读取设备 alt MBR分区 BIOS-->>Disk: 读取MBR引导代码 else GPT分区 BIOS-->>Disk: 读取ESP分区的Bootloader end Disk-->>BIOS: 返回Bootloader位置 BIOS->>BootManager: 启动图形化选择界面 BootManager->>User: 显示系统选项 User->>BootManager: 选择操作系统 BootManager->>BootLoader: 调用对应Bootloader BootLoader->>Disk: 加载kernel和initramfs Disk-->>BootLoader: 返回文件数据 BootLoader->>Kernel: 移交控制权(带启动参数) Kernel->>Hardware: 硬件检测和初始化 Kernel->>Initramfs: 解压临时根文件系统 Initramfs->>Kernel: 加载必要驱动/工具 Kernel->>RootFS: 挂载真正的根目录 RootFS-->>Kernel: 返回挂载状态 Kernel->>Systemd: 启动初始化进程 Systemd->>Services: 按运行级别启动服务
initrd/initramfs 与 kernel
initramfs 是 initrd 的继任者,绝大多数发行版都用上了 initramfs
。不过它们的功能是一致的:帮助内核启动系统。那么内核为什么需要“帮助”呢?
其实早些年,Linux 内核是不需要“帮助”的,内核本身就能完成启动功能。但是随着硬件和软件的发展,内核所需要支持的设备和功能越来越多,体积也逐渐膨胀。因为 Linux 系统使用的场景众多,内核需要各种不同的功能。
为了避免内核的体积不断膨胀,内核会加载一系列用不到的功能导致时间加长,以及用户需要定制内核的麻烦事,现代发行版大多使用模块(module)的方式来给给内核提供功能。当用户需要某个功能时,挂载对应模块即可。这样,这个内核文件就会变得比较小巧。
但是过于精简又丧失了很多功能:例如读写 SATA 接口的磁盘。所以内核需要借助 initramfs 来挂载读写磁盘的 module。同时 initramfs 中的工具也可以辅助内核完成一些内核自己完成比较困难的操作。例如使用 LVM,LUKS 等这些功能。
如果你编译过 openWRT 你可能会知道一些功能既可以编译为模块,也可以编译进内核。其实即使是现在,你依旧可以定制一个无需 initramfs 的内核,该内核专为你这个设备使用。支持该功能的主流的发行版是 Gentoo。有兴趣的话可以尝试一下。
总的来说 initramfs 是现代发行版不可或缺的一部分。就算是之前提到的 Gentoo,依旧建议使用模块来提高兼容性,因为像 LVM 这类功能不仅需要内核支持还需要对应的工具,没有 initramfs 大概率是不行的。下面是在我电脑上使用 lsmod
命令列出的当前加载模块的部分输出:
> lsmod
Module Size Used by
rfcomm 102400 4
xt_tcpudp 16384 0
xt_mark 12288 2
xt_conntrack 12288 2
xt_MASQUERADE 16384 4
xt_set 24576 0
ip_set 69632 1 xt_set
nft_chain_nat 12288 6
nf_nat 61440 2 nft_chain_nat,xt_MASQUERADE
nf_conntrack 204800 3 xt_conntrack,nf_nat,xt_MASQUERADE
nf_defrag_ipv6 24576 1 nf_conntrack
nf_defrag_ipv4 12288 1 nf_conntrack
xt_addrtype 12288 4
nft_compat 24576 12
xfrm_user 73728 1
xfrm_algo 16384 1 xfrm_user
tun 69632 2
nf_tables 385024 205 nft_compat,nft_chain_nat
uhid 28672 2
......
initrd 和 initramfs 的历史
initrd (Initial RAM Disk):是早期的解决方案。它实际上是一个压缩的文件系统镜像,通常是`ext2`格式。它由引导加载程序(如GRUB)加载到内存中。内核启动后,将其解压并挂载为一个临时的根文件系统(一个`ramdisk`设备,如`/dev/ram0`),用于执行初始化任务(加载模块、挂载真正的根文件系统等)。它的缺点是作为一个块设备,效率相对较低,且大小固定。
initramfs (Initial RAM File System): 在 2002 年 1 月提出到从 2002 年 11 月 4 日发布的 2.5.46
内核版本引入,并于 2.6.x
成为标准。它不是一个块设备镜像,而是一个 cpio 归档文件,通常还会用`gzip`压缩(所以扩展名是`.gz`)。内核启动时,直接将这个归档解压到一个特殊的`tmpfs`文件系统中。`tmpfs`非常高效,因为它直接在内存中管理文件和目录结构,不需要块设备仿真层。它的大小是动态的,更灵活,性能更好。
虽然 initramfs 已经取代了 initrd 相当长的时间,但是 initrd 这个名字在引导加载程序的配置文件、安装程序、文档以及用户的认知中存在了非常长的时间。所以现在还在使用 initrd 这个名字。不过,即使名字一样,不过辨别它们非常简单:一个是 cpio 格式的归档文件,通常经过 gz 压缩,一个是磁盘镜像。
Initramfs 的内部
Linux 文件里有 Linux 系统的内核,Initramfs 文件是一个 cpio 格式的归档文件,这个 cpio 和 tar 是一类东西。注意,cpio 和 tar 一样,是归档,而非压缩,两个格式都不会对文件进行压缩。不过现代大部分发行版采用 gzip 对该文件压缩来更高效地存储它,可以理解为 initrd.cpio.gz
。解压后使用 cpio -i -vd < initrd
命令可以打开该归档文件。
打开这个归档文件可以发现它具备基础的 Linux 目录结构
tree -L 1
.
├── bin
├── dev
├── etc
├── init
├── lib
├── lib64
├── media
├── mnt
├── proc
├── run
├── sbin
├── sys
├── tmp
├── usr
└── var
它包含了一些必要的内核模块,主要是硬件驱动模块。基础的一些命令行工具,例如 echo
, ln
, mkdir
, cp
, mv
等,在 initramfs 中是可以手动执行这些工具的。不过它里面的这些工具和启动系统后用的工具是有很大不同的:这些工具采用静态编译,它们不依赖任何动态库,同时它们是精简后的版本,在编译时很多功能被省去了。在很多发行版中,一个常见的做法是使用 busybox 项目来提供这些工具。
虽然现在 initramfs 基本完全替代了所有 Linux 系统的 initrd。但是 initrd 这个名字被保留了下来你还能看到很多地方有 initrd.gz 这样命名的文件。
PXE 干了什么
回到 PXE 中,前面介绍了 Linux 是如何启动的,现在来详细看看 PXE 是如何替代“从磁盘中寻找 Bootloader”的。由于 UEFI 的普及,现在 bootloader 的定义边界正在变得模糊,不过只需要记住它指的是一类程序,这里写一下定义:
引导加载程序(Bootloader)是计算机启动过程中,位于固件与操作系统内核之间的关键程序,负责加载内核映像到内存并移交控制权,从而启动操作系统。
如果想使用 PXE 那你必须有一个支持 PXE 启动的网卡。PXE 的相关程序存储在你的网卡上,在网卡的 ROM 里。那么 PXE 是如何寻找到前面说到的内核和 initramfs 两个文件呢?实际上它需要两个服务协议:
- DHCP 动态主机配置协议
- TFTP 简单文件传输协议
PXE 启动的流程
现在详细看看 PXE 部分启动的过程。对于其它的启动流程之前已经介绍过,这里不再赘述。
- 网卡作为客户端,直接发出广播,寻找网络上的 DHCP 服务
- DHCP 服务收数据,把下面这些内容发给客户端
- 给网卡分配的 IP 地址
- TFTP 服务器的 IP 地址
- TFTP 中 Bootloader 的文件名例如
pxelinux.0
或grubx64.efi
- 根据配置不同可能还包含其它信息,如 DNS 服务器地址等
- 网卡收到上面的数据,连接 TFTP 服务器,下载获得 Bootloader
- 用户选择启动项,Bootloader 配置中记录了 kernel 和 initramfs 的文件名以及内核选项
- 网卡从 TFTP 服务器上下载两个对应文件,放入内存
sequenceDiagram participant Client participant DHCP participant TFTP Note over Client: 启动PXE ROM Client->>DHCP: DHCPDISCOVER (广播+PXE标识Option 60) DHCP->>Client: DHCPOFFER (含Next-Server IP/TFTP配置) Client->>DHCP: DHCPREQUEST (确认租约) DHCP->>Client: DHCPACK (最终确认) alt 获取Bootloader Client->>TFTP: GET pxelinux.0 (或grubx64.efi) TFTP->>Client: 传输Bootloader else 失败重试 Client->>TFTP: 重试请求 end alt 获取配置文件 Client->>TFTP: GET pxelinux.cfg/<MAC/IP>/default TFTP->>Client: 传输配置文件 end loop 获取系统文件 Client->>TFTP: GET kernel/vmlinuz TFTP->>Client: 传输内核 Client->>TFTP: GET initrd.img TFTP->>Client: 传输initramfs end Note over Client: 开始引导操作系统
以上就是通常 PXE 启动的大致流程。
搭建一个 PXE 服务器
搭建一个 PXE 服务器主要需要两个步骤:
- 获得支持网络启动的文件
- 使用对应软件搭建 PXE 环境
获得网络启动文件
首先,并不是所有系统和光盘镜像都支持网络启动。相反,如果不做网络启动的配置,大部分系统并不能直接从网络启动。例如:
- 对于 Debian
- Debian installer ISO 不直接支持网络启动
- Debian 提供的
netboot.tar.gz
文件里包含可用于网络启动的文件
- 对于 Arch Linux
- Arch ISO 仅支持 legacy 启动
- Arch 的 Netboot 页面也提供了网络启动所需的文件
不同的发行版可以参照他们对应的文档,或者你可以构建自己的支持启动的镜像。
以上这些内容主要针对的是安装环境,如果你需要无盘启动(Diskless)那么你还需要把系统安装到 ISCSI 等服务中并提供必要的配置。Diskless 不仅需要提供 linux 内核文件和 initramfs,还需要告诉系统该去哪里寻找 rootfs。本文主要针对 PXE 启动,对于 Diskless 的内容请参考对应发行版的文档。
搭建 PXE 服务
根据上面的介绍想要使用 PXE 就需要一个 DHCP 服务和一个 TFTP 服务。有两种常见的解决方法:
- 使用 dnsmasq,它是一个比较全功能的 DHCP DNS TFTP 程序,使用一个程序就能实现这些功能
- 使用 isc-dhcp-server 和 tftpd-hpa 一个是 DHCP 服务,一个是 TFTP 服务
值得一提的是 dnsmasq 支持 DHCP 代理模式,如果你的网络中有一个 DHCP 服务器了,借助该模式可以让网络中的现有 DHCP 服务和 dnsmasq 一同工作。不过我在使用时有部分情况在 UEFI 下还是有兼容性问题。这里就以 dnsmasq 直接当作网络中的 DHCP 服务器为例,给出一个最简化的配置,其它内容具体配置参见文档。
# 监听的网口
interface=eth1
# DHCP 服务分配地址范围
dhcp-range=172.16.0.100,172.16.0.200,12h
# 默认启动文件,TFTP 服务地址默认为本机地址
dhcp-boot=pxelinux.0
# 启动内置 TFTP 服务器
enable-tftp
# TFTP 服务的目录
tftp-root=/var/tftp
# legacy 启动需要的 pxelinux.0 文件
pxe-service=x86PC, "PXELINUX (BIOS)", "pxelinux.0"
# UEFI 启动需要的 grubx64.efi 文件
pxe-service=X86-64_EFI,"PXE (UEFI)","grubx64.efi"
这样需要 PXE 所需要的服务就搭建完成了。启动 dnsmasq 后。插好网线,在支持 PXE 的硬件设备上从 PXE 启动,就可以看到设备能够自动加载你指定的系统了。你可以使用:
sudo dnsmasq --no-daemon -C /etc/dnsmasq.conf --log-dhcp --log-debug
这个命令启动 dnsmasq 看详细输出来排查错误。
iPXE
iPXE 对 PXE 进行扩展和提升,最主要的是文件传输过程。
TFTP 是一个古老的协议,它使用 UDP 传输,它有自己的机制来校验传输完整性。它也是一个十分轻量级的协议。起初它有比较大协议缺陷,后经修复。由于它的设计,它的传输效率是比低的,通常不可能跑满现在的内网带宽。其次,一个问题是你无法看到文件的传输进度。TFTP 也无法像 FTP 那样列出目录,因此你需要指定启动文件。TFTP 不支持加密传输,它只建议在内部网络中使用。PXE 还有一个问题就是不能灵活得选择多个启动系统,虽然也能设置,但是比较麻烦。
基于此,iPXE 在兼容 PXE 的 TFTP 传输的同时,扩展了传输方式,可以使用 HTTP 以及 iSCSI 和 FC 等从 HTTP 或 SAN 网络中启动。因此 iPXE 还可以在广域网,而非只在局域网中工作。此外它也可以通过 WIFI 来启动。
除此之外 iPXE 带来了命令行,对应的脚本,更易于编辑、美化后的菜单来支持启动不同的系统等功能。
怎么用上 iPXE
前文提到 PXE 程序在网卡的 ROM 里。因此最直接的方式是刷写网卡的固件,或者直接买一个支持 iPXE 的网卡。不过很多消费级设备是不支持 iPXE 的。用上 iPXE 的另一种方法是使用 chainload,链式加载。
这个过程的原理为:
- 让设备依旧以 PXE 的方式启动
- 但 DHCP 服务器告诉客户端启动 iPXE 对应的内核文件
- 设备接收并启动 iPXE 的相关文件进入 iPXE 环境
- iPXE 根据配置或 DHCP 服务器的进一步回复启动对应环境或显示启动菜单
注意,如果不做特殊的配置,DHCP 服务器不知道当前设备是 PXE 环境还是 iPXE 环境。在步骤 4 中,iPXE 依旧在发布广播,并等待 DHCP 服务器的回复来下载启动镜像。此时 DHCP 服务器并不知道自己在给哪个环境发送 iPXE 程序路径,iPXE 也并不知道接收到的程序是 iPXE 本身。因此这就会造成 iPXE 自己本身无限循环启动下去。
解决这一问题主要是两种思路:
- 配置 DHCP 服务器,利用保留的 option 175,在第二次发来请求时恢复正确的文件
- 配置 iPXE 环境,让它直接启动指定系统或显示菜单而不是等待 DHCP 服务器的回复
对于 dnsmasq,默认的配置文件提供的解决思路为:
# Boot for iPXE. The idea is to send two different
# filenames, the first loads iPXE, and the second tells iPXE what to
# load. The dhcp-match sets the ipxe tag for requests from iPXE.
#dhcp-boot=undionly.kpxe
#dhcp-match=set:ipxe,175 # iPXE sends a 175 option.
#dhcp-boot=tag:ipxe,http://boot.ipxe.org/demo/boot.php
#dhcp-boot=pxelinux.0,pxeserver,192.168.2.21
# Encapsulated options for iPXE. All the options are
# encapsulated within option 175
#dhcp-option=encap:175, 1, 5b # priority code
#dhcp-option=encap:175, 176, 1b # no-proxydhcp
#dhcp-option=encap:175, 177, string # bus-id
#dhcp-option=encap:175, 189, 1b # BIOS drive code
#dhcp-option=encap:175, 190, user # iSCSI username
#dhcp-option=encap:175, 191, pass # iSCSI password
其它的配置方法可以查看 iPXE chainload 的文档。
不过,还有一种“解决方法”是在 iPXE 从 DHCP 服务器获得启动文件之前按下 ctrl + B 来打断启动流程,进入 iPXE 的命令行,然后使用 iPXE 命令来启动。
总结
如上,本文的目的主要是针对启动的原理进行解释,帮助读者更好地理解 Linux 的网络启动机制,至于具体的使用方法,命令内容并没有做提及,如果你感兴趣可以在它们的文档找到这些东西的使用方法并搭建自己的服务。