PXE 启动 archlinux

本文最后更新于 2024年11月17日 凌晨

最近想整个 PXE 启动的 archlinux 环境,启动之后像 archiso 一样,跑在内存里面。

实现思路

PXE 启动 grub, 然后走 http 下载 kernel 和 initramfs, 在 initramfs 启动过程中下载 rootfs,最后 switch_root 到 rootfs。

  1. 首先,我们需要制作一个可以启动的 initramfs 和 rootfs, 可以参考制作自定义的 archlinux initramfs 与 rootfs。此处不再赘述。
  2. 然后我们需要架设一个 http 服务器,因为我们的 grub 启动后,打算让它走 http 去加载内核 与 initramfs, 最后 initramfs 走 http 加载rootfs。可以用 darkhttp 或者 python 的 http.server 模块都行,此处不再赘述。
  3. 我们还需要架设一个 tftp 服务器,给 pxe 下发 grub 引导文件。具体分成三种情况,x86_64-efi, i386-pc,arm64-efi. 其中 arm64-efi 和 x86_64-efi 差不多,本文略过arm64-efi 的情况。(其实是没有机器用来测试)。

制作 grub 引导文件

i386-pc 的情况

首先是 i386-pc 的情况,先创建一个文件grub.cfg.pre.bios,内容如下:

1
2
set prefix=(http,$net_pxe_next_server)/boot/grub
configfile /boot/grub/grub.cfg

它的作用是告诉 grub 去 http 服务器上获取 grub.cfg。然后执行下面的命令创建 grubpxe.0, 其中 -c grub.cfg.pre.bios 会把我们刚刚创建的 grub.cfg.pre.bios 当成一个早期配置文件进行嵌入。然后执行下面的命令生成 pxe 引导文件 grubpxe.0。 注意这里 -p '' 建议设置为空,不然会影响启动速度。

1
2
3
grub-mkimage --format=i386-pc-pxe --output=grubpxe.0 \
-c grub.cfg.pre.bios \
-p '' net pxe http tftp linux progress configfile

x86_64-efi 的情况

假设我们用 UEFI 的 PXE 启动方式,x86_64-efi 的情况下我们可以不使用 grub-mkimage 而是使用 grub-mkstandalone
先创建一个 grub.cfg.pre.uefi 文件。

1
2
3
eval "set _net_http_server=\${net_${net_default_interface}_next_server}"
set prefix=(http,${_net_http_server})/boot/grub
configfile $prefix/grub.cfg

然后生成引导文件 grubpxe.efi

1
2
3
grub-mkstandalone -O x86_64-efi -o grubpxe.efi \
"boot/grub/grub.cfg=grub.cfg.pre.uefi" \
--modules="efinet http net configfile eval echo linux progress" --locales="en@quot" --themes=''

然后执行下面的命令创建 grubpxe.efi, 其中 --modules= 指定预加载的模块,--locales 指定包含的 locale, 建议只指定需要的,不然生成的产物会很大。你还可以通过指定 --fonts='' 来进一步减小大小(2M)。

当然,也可以使用 grub-mkimage 来制作引导文件。这种情况下需要写死 grub.cfg.pre.uefi,原因暂时不明,可能漏了什么模块。

1
2
3
eval "set _net_http_server=\${net_efinet0_next_server}"
set prefix=(http,${_net_http_server})/boot/grub
configfile $prefix/grub.cfg

然后执行

1
2
grub-mkimage --format=x86_64-efi --output=grubpxe.efi -c grub.cfg.pre.uefi   \
-p '/boot/grub' net tftp http efinet configfile eval echo linux

HTTP BOOT 的情况

同样先新建一个文件 grub.cfg.pre.http, 注意这里我们手动执行了 net_dhcp 并且硬编码了一个 efinet0

1
2
3
4
net_dhcp
eval "set _net_http_server=\${net_efinet0_dhcp_next_server}"
set prefix=(http,${_net_http_server})/boot/grub
configfile ${prefix}/grub.cfg

然后执行下面的命令创建 grubpxe-http.efi, 其中 --modules= 指定预加载的模块,--locales 指定包含的 locale, 建议只指定需要的 locale,不然生成的产物会很大。你还可以通过指定 --fonts='' 来进一步减小大小(2M)。

1
2
3
grub-mkstandalone -O x86_64-efi -o grubpxe-http.efi \
"boot/grub/grub.cfg=grub.cfg.pre.http" \
--modules="efinet http net tftp linux progress configfile" --locales="en@quot" --themes=''

grub-mkimage 的版本

1
2
grub-mkimage --format=x86_64-efi --output=grubpxe-http.efi -c grub.cfg.pre.http   \
-p '/boot/grub' net tftp http efinet configfile eval linux

架设 dhcp 与 tftp 服务器。

本文选用了 dnsmasq, 你可以可选用 dhcpcd.
本文使用的配置文件如下。其实设置了三种启动方式,X86-64_EFI_HTTP (UEFI 的http boot), efi-x86_64 pxe 和 bios pxe. http boot的情况下,dhcp 服务器直接告诉 qemu 去拿 http://192.168.122.1/boot/grub/grubpxe-http.efi 这个文件启动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
port=10053
interface=wlp4s0
interface=virbr0
bind-interfaces
dhcp-range=192.168.122.50,192.168.122.150,12h
dhcp-range=10.0.2.50,10.0.2.150,12h
dhcp-range=192.168.2.1,proxy
dhcp-option-force=209,archiso_pxe.cfg
dhcp-option-force=210,
dhcp-match=set:X86-64_EFI_HTTP,option:client-arch,16
dhcp-userclass=set:iPXE,iPXE
dhcp-boot=tag:X86-64_EFI_HTTP,http://192.168.122.1/boot/grub/grubpxe-http.efi,192.168.122.1,192.168.122.1
dhcp-match=set:efi-x86_64,option:client-arch,7
dhcp-match=set:efi-x86_64,option:client-arch,9
dhcp-match=set:efi-x86,option:client-arch,6
dhcp-match=set:bios,option:client-arch,0
dhcp-boot=tag:efi-x86_64,grubpxe.efi,192.168.122.1,192.168.122.1
dhcp-boot=tag:efi-x86,efi32/syslinux.efi
dhcp-boot=tag:bios,grubpxe.0,,192.168.122.1
enable-tftp
tftp-root=/tmp/x64/pxe
tftp-no-blocksize
log-dhcp

资源目录结构

对下面的目录结构进行一个简单说明:

  1. archiso/arch 是从 archlinux iso 解压的资源,未做任何更改(可以用自定义的initramfs 和rootfs 替换对应文件)。
  2. archiso/boot/grub 下面的文件由 grub-mknetdir --net-directory=. --subdir=/boot/grub -d /usr/lib/grub/x86_64-efi 生成, UEFI 和 legacy 各一份。grubpxe.0grubpxe.efi 是由上面的命令生成,并复制到 /tmp/x64/pxe 下面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/tmp/x64/pxe
├── archiso
│   ├── arch
│   │   ├── boot
│   │   │   ├── amd-ucode.img
│   │   │   ├── intel-ucode.img
│   │   │   ├── licenses
│   │   │   └── x86_64
│   │   ├── grubenv
│   │   ├── pkglist.x86_64.txt
│   │   ├── version
│   │   └── x86_64
│   │   ├── airootfs.sfs
│   │   ├── airootfs.sfs.bak
│   │   ├── airootfs.sfs.cms.sig
│   │   └── airootfs.sha512
│   ├── boot
│   │   └── grub
│   │   ├── fonts
│   │   ├── grub.cfg
│   │   ├── grub.cfg.pre.bios
│   │   ├── grub.cfg.pre.http
│   │   ├── grub.cfg.pre.uefi
│   │   ├── grubenv
│   │   ├── grubpxe.0
│   │   ├── grubpxe.efi
│   │   ├── grubpxe-http.efi
│   │   ├── i386-pc
│   │   ├── locale
│   │   ├── themes
│   │   └── x86_64-efi
│   ├── shellia32.efi
│   └── shellx64.efi
├── archlinux-2023.12.01-x86_64.iso
├── dnsmasq.conf
├── grubpxe.0 -> archiso/boot/grub/grubpxe.0
├── grubpxe.efi -> archiso/boot/grub/grubpxe.efi
├── grubpxe-http.efi -> archiso/boot/grub/grubpxe-http.efi
├── run.sh
└── uefi.log

grub.cfg 文件

1
2
3
4
5
6
7
8
timeout=3
default=0
menuentry networkboot {
set root=(http,192.168.122.1:80)
insmod progress
linux /arch/boot/x86_64/vmlinuz-linux archisobasedir=arch archiso_http_srv=http://192.168.122.1/ ip=dhcp
initrd /arch/boot/x86_64/initramfs-linux.img
}

启动服务

dhcp 服务: 在/tmp/x64/pxe执行 sudo dnsmasq -C dnsmasq.conf -d
HTTP服务: 在 /tmp/x64/pxe/archiso 执行 sudo python3 -m http.server 80

一些小技巧

  1. 可以在grub.cfg.pre.* 中的开头加入 debug=all, 并在virtmanager 中给 qemu 虚拟机添加控制台和串口(输出到文件),就可以在本地看启动日志,根据 grub 的失败日志来进行调整。
  2. grub-mkimage 制作出来的引导文件不需要加载 /boot/grub/下面的其他文件(i386-pc, x86_64-efi文件夹下的mod),因此注意将需要打的mod 在mkimage 命令中打进去(除非你在grub.cfg 中显示insmod)。
  3. grub-mkstandalone 中需要添加部分模块(如 linux configfile ) 到 --modules= 参数中让它预加载,不然会通过http去资源目录下获取。

启动耗时比较

记录了从virt-manager 按下 reset 到启动完成,提示login的时间。均未开启 debug

  • uefi+http boot + mkimage: 38.83s
  • uefi+http boot + mkstandalone: 38.6s.
  • uefi+ mkimage+ 单文件 38.88s
  • uefi+ mkstandalone: 38.70s
  • bios+ mkimage+ 单文件(无i386-pc文件夹):25.32s
  • bios+ mkimage+ 多文件(有i386-pc文件夹,有无不影响启动):25.25s (和单文件其实没差异)

结语

本文提出了一种通过 pxe/http 引导grub来启动内存系统的方案,该方案除了拉取 grub 核心引导文件是通过 tftp 以外,后续所有请求都是通过 http 完成,避免了 tftp 丢包的问题。但是本文也存在以下待进一步完善的问题:

  1. UEFI情况下 grub.cfg.pre.* 硬编码了 efinet0, 应该用 $net_default_interface 替代。
  2. UEFI HTTP BOOT 的情况下,需要手动 net_dhcp 否则不会自动加载网络,如果能成功加载网络,则grub.cfg.pre.http 只需要写一行 configfile ${cmdpath}/grub.cfg 即可,我之前成功过,但是目前无法复现。
  3. 我们在使用 grub-mkimage 的时候会将一行模块打到 grub 引导文件中,这会导致引导文件变大,从而拖慢启动速度,但是不包含进去又会导致从http 服务器下载,也会拖慢启动速度,本文并没有进一步比较两种方案的好坏并找到一个建议的方案。

参考资料

  1. Preboot Execution Environment
  2. PXE network boot methods
  3. dnsmasq PXE server
  4. Grub standalone

PXE 启动 archlinux
https://blog.askk.cc/2023/12/24/archlinux-pxeboot/
作者
sukanka
发布于
2023年12月24日
许可协议