本文由 Codex 根据 PXE、Alpine 与 VirtualBMC 实测和讨论过程辅助整理生成。

为了理解裸金属装机流程,我在一台 KVM 宿主机上搭建了最小 PXE 实验:客户端只有 1 vCPU512 MiB 内存和 2 GiB 磁盘,通过网络启动 Alpine,再将系统安装到虚拟磁盘。

最终 qcow2 文件实际占用约 90 MiB,适合资源有限的测试环境。

PXE 启动原理

PXE 不是操作系统安装器。它解决的是:一台没有可用系统的机器,如何通过网络获得并启动下一段程序。

完整链路如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
BIOS/UEFI
↓ 选择网卡启动
PXE/iPXE 固件
↓ DHCP 获取 IP 和启动地址
boot.ipxe
↓ HTTP 下载
Linux kernel + initramfs + modloop

内存中的 Alpine 临时系统
↓ setup-alpine
安装到虚拟磁盘 /dev/vda
↓ 重启
从硬盘启动 Alpine

DHCP:告诉客户端去哪里启动

PXE 客户端首先通过 DHCP 的 Discover、Offer、Request、ACK 获取网络配置。与普通 DHCP 不同的是,服务端还会返回启动文件地址:

1
2
3
IP:192.168.130.110
网关:192.168.130.1
启动文件:http://192.168.130.1:8080/alpine/boot.ipxe

iPXE:下载并启动 Linux

boot.ipxe 类似网络版 GRUB 配置。iPXE 读取脚本后,通过 HTTP 下载内核和 initramfs,再把 CPU 执行权交给 Linux 内核。

传统 PXE 常用 TFTP;本实验的 QEMU 网卡固件已经支持 iPXE,因此直接使用 HTTP,省去了 TFTP。nginx 只负责提供静态文件,并不负责安装系统。

setup-alpine:真正写入磁盘

内核和 initramfs 启动后,Alpine 仍运行在内存中。此时 PXE 的任务已经完成,磁盘尚未安装系统。

真正执行分区、格式化、安装软件包和引导程序的是:

1
setup-alpine

一句话概括:PXE 把安装环境送进内存,安装器再把操作系统写入磁盘。

内核、initramfs 与无盘系统

以下内容用于解释启动原理,不属于本次 Alpine、VirtualBMC 实测范围。

Linux 完整启动链路

无论从本地磁盘还是 PXE 启动,Linux 获得 CPU 控制权后的过程基本相同:

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
上电

BIOS/UEFI:硬件自检并选择启动设备

启动入口
├─ 磁盘:GRUB 等引导程序
└─ 网络:固件 PXE 客户端 → NBP/iPXE

引导阶段
├─ 将 Linux kernel 放入内存
├─ 将 initramfs 放入内存
└─ 传递 kernel command line

Linux 内核
├─ 自解压
├─ 初始化 CPU、内存、中断和调度器
├─ 初始化内置驱动
└─ 解压 initramfs,挂载为临时根目录 /

initramfs 中的 /init(早期 PID 1)
├─ 挂载 /proc、/sys、/dev
├─ 加载磁盘、网卡等内核模块
├─ 识别 RAID、LVM、加密卷或网络根设备
└─ 挂载真正的根文件系统
↓ switch_root
真正根文件系统中的 /sbin/init

systemd、OpenRC 等初始化系统
├─ 挂载其他文件系统
├─ 配置网络
├─ 启动系统服务
└─ 提供控制台或登录界面

传统磁盘启动与 PXE 启动的主要区别只在前半段:

启动方式 内核和 initramfs 的来源 真正的根文件系统可能位于
磁盘启动 GRUB 从本地磁盘读取 本地磁盘
PXE 启动 固件 PXE 客户端加载 iPXE,再由 iPXE 从网络下载 内存、本地磁盘或网络存储

如果 initramfs 已经包含完整用户空间,它可以继续作为最终根文件系统,不一定执行 switch_root。这正是部分 Live、救援和纯内存系统的工作方式。

为什么内核加 initramfs 就能启动 Linux

本实验由 iPXE 启动 Alpine 时,至少需要向客户端提供两部分:

  • 内核:初始化 CPU、内存、中断和设备,管理进程与硬件资源
  • initramfs:提供早期用户空间,包括 /init、BusyBox、驱动加载和网络脚本

启动过程是:

1
2
3
4
5
6
7
8
9
10
11
iPXE 将 kernel 和 initramfs 放入内存

跳转执行 Linux 内核

内核解压 initramfs,并将其作为临时根目录 /

执行 /init,成为 PID 1

加载驱动、配置网络、寻找真正的根文件系统

继续运行内存系统,或通过 switch_root 切换到磁盘/网络根文件系统

内核只有管理硬件和执行程序的能力;initramfs 只有文件和启动工具。两者结合,才构成一个可以运行的最小 Linux。它可以只是安装环境,也可以是完全运行在内存里的系统,但不代表操作系统已经安装到磁盘。

initramfs 并非所有 Linux 启动方式都必需。如果根文件系统位于内核可直接访问的设备上,且所需的文件系统与存储驱动已编译进内核,内核也可以根据 root= 参数直接挂载磁盘根文件系统并执行其中的 /sbin/init。本实验需要 initramfs,是因为它要先提供网络启动所需的早期用户空间。

initramfs 为什么不能无限增大

initramfs 没有统一的固定大小上限,实际边界取决于客户端内存和固件、引导器的内存布局。启动时内存中可能同时存在:

1
2
3
4
5
压缩的 initramfs
+ 解压后的内存根文件系统
+ Linux 内核及其数据结构
+ 驱动、网络缓冲区和用户进程
= 启动内存峰值

因此不能只看 initramfs 压缩文件的大小。文件过大时,可能在 iPXE 分配内存、内核解压或用户空间启动阶段失败。

本实验将内容拆分为 vmlinuz-virtinitramfs-virtmodloop-virt 和 HTTP APK 仓库,目的之一就是让 initramfs 只保留早期启动必需内容,额外模块与软件包在网络可用后再按需获取。

网吧无盘 Windows 如何启动

无盘系统通常不会把完整 Windows 和所有游戏一次性复制到内存,而是把服务器上的远程块设备伪装成本地系统盘:

1
2
3
客户机的 C:
= 服务器上的公共只读系统镜像
+ 当前客户机的独立写入层

公共镜像便于统一更新,写时复制层避免多台客户机相互覆盖;服务器 NVMe、内存和客户端内存则缓存热点数据。

Windows 无盘启动主要有两种链路。

第一种由 iPXE 直接连接 iSCSI:

1
2
3
4
BIOS/UEFI → PXE/iPXE → 挂载 iSCSI LUN
→ 读取远程盘上的 Windows Boot Manager
→ Windows 内核和启动级网卡/iSCSI 驱动接管连接
→ 从远程 C: 继续启动

iPXE 脚本示意如下:

1
2
3
#!ipxe
dhcp
sanboot iscsi:192.168.130.1::::iqn.2026-08.local:windows

iPXE 会通过 iBFT 等机制向操作系统描述 iSCSI 启动盘。因此,这条链路不要求先启动 WinPE;远程盘在 Windows Boot Manager 运行前就已经连接。

第二种先启动 WinPE 或厂商的精简引导环境:

1
2
3
4
BIOS/UEFI → PXE → WinPE/厂商引导环境
→ 加载网卡和网络磁盘驱动
→ 连接远程系统盘
→ 链式启动正式 Windows

这种模式下,WinPE 的作用与 Linux initramfs 相似:先提供最小用户空间,获得网络存储能力,再找到真正的系统盘。无论采用哪种方式,关键都不是“必须启动 WinPE”,而是 Windows 第一次需要读取系统盘之前,远程磁盘必须已经可用;Windows 接管后,启动级驱动必须能继续维持连接。

实验架构

1
2
3
4
5
6
7
8
KVM 宿主机
├── libvirt dnsmasq:DHCP 与启动地址
├── nginx :8080:iPXE、内核、initramfs、modloop、APK
└── pxe-alpine VM
├── 1 vCPU
├── 512 MiB RAM
├── 2 GiB qcow2
└── 192.168.130.110

为避免影响现有网络,DHCP 仅绑定到独立的 virbr-pxe 网桥。

操作过程

1. 创建隔离 PXE 网络

创建 pxe-lab.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<network>
<name>pxe-lab</name>
<forward mode='nat'/>
<bridge name='virbr-pxe' stp='on' delay='0'/>
<ip address='192.168.130.1' netmask='255.255.255.0'>
<dhcp>
<range start='192.168.130.100' end='192.168.130.200'/>
<host mac='52:54:00:13:00:10'
name='pxe-alpine'
ip='192.168.130.110'/>
<bootp file='http://192.168.130.1:8080/alpine/boot.ipxe'/>
</dhcp>
</ip>
</network>

启用网络:

1
2
3
virsh net-define pxe-lab.xml
virsh net-autostart pxe-lab
virsh net-start pxe-lab

2. 准备 Alpine 启动文件

nginx 在 8080 端口暴露 /var/lib/ironic/httpboot。创建目录并下载 Alpine virt netboot 文件:

1
2
3
4
5
6
7
mkdir -p /var/lib/ironic/httpboot/alpine
cd /var/lib/ironic/httpboot/alpine

base=https://dl-cdn.alpinelinux.org/alpine/v3.24/releases/x86_64/netboot-3.24.1
curl -fLO "$base/vmlinuz-virt"
curl -fLO "$base/initramfs-virt"
curl -fLO "$base/modloop-virt"

三份文件合计约 43 MiB

  • vmlinuz-virt:Linux 内核
  • initramfs-virt:启动早期的内存根文件系统
  • modloop-virt:Alpine 内核模块

3. 编写 iPXE 脚本

创建 /var/lib/ironic/httpboot/alpine/boot.ipxe

1
2
3
4
5
6
7
8
9
10
11
12
13
#!ipxe
dhcp

set base-url http://192.168.130.1:8080/alpine

kernel ${base-url}/vmlinuz-virt \
console=tty0 console=ttyS0,115200 \
modules=loop,squashfs,sd-mod,virtio_pci,virtio_blk,virtio_net \
alpine_repo=https://dl-cdn.alpinelinux.org/alpine/v3.24/main \
modloop=${base-url}/modloop-virt

initrd ${base-url}/initramfs-virt
boot

先确认 HTTP 文件可访问:

1
2
curl http://192.168.130.1:8080/alpine/boot.ipxe
curl -I http://192.168.130.1:8080/alpine/vmlinuz-virt

本次实验中公网仓库较慢,因此后来把安装需要的约 48 个基础包缓存到 nginx,并将 alpine_repo 改为:

1
http://192.168.130.1:8080/alpine/main

使用本地仓库时,应答文件中的 APKREPOSOPTS 也要改成同一地址。

不要直接把 Alpine virt ISO 当作完整安装仓库,它可能缺少 linux-virtsyslinux 等磁盘安装包。

4. 创建最小虚拟机

先创建稀疏磁盘:

1
2
qemu-img create -f qcow2 \
/var/lib/libvirt/images/pxe-alpine.qcow2 2G

虚拟机关键配置如下:

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
<domain type='kvm'>
<name>pxe-alpine</name>
<memory unit='MiB'>512</memory>
<vcpu>1</vcpu>
<os>
<type arch='x86_64' machine='pc'>hvm</type>
<boot dev='network'/>
<boot dev='hd'/>
</os>
<devices>
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2'/>
<source file='/var/lib/libvirt/images/pxe-alpine.qcow2'/>
<target dev='vda' bus='virtio'/>
</disk>
<interface type='network'>
<mac address='52:54:00:13:00:10'/>
<source network='pxe-lab'/>
<model type='virtio'/>
</interface>
<serial type='pty'/>
<console type='pty'/>
<graphics type='vnc' port='-1' autoport='yes' listen='127.0.0.1'/>
</devices>
</domain>

保存为 pxe-alpine.xml,然后启动:

1
2
3
virsh define pxe-alpine.xml
virsh start pxe-alpine
virsh console pxe-alpine

5. 将 Alpine 安装到磁盘

进入内存中的 Alpine 后,可执行 setup-alpine 交互式安装。为了减少重复输入,本实验使用应答文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
KEYMAPOPTS=none
HOSTNAMEOPTS=pxe-alpine
DEVDOPTS=mdev
INTERFACESOPTS="auto lo
iface lo inet loopback

auto eth0
iface eth0 inet dhcp
"
DNSOPTS=none
TIMEZONEOPTS=UTC
PROXYOPTS=none
APKREPOSOPTS="https://dl-cdn.alpinelinux.org/alpine/v3.24/main"
USEROPTS=none
SSHDOPTS=none
NTPOPTS=none
DISKOPTS="-m sys -s 0 -k virt /dev/vda"
LBUOPTS=none
APKCACHEOPTS=none

将文件保存为 nginx 可访问的 /var/lib/ironic/httpboot/alpine/answers,再从 Alpine 控制台执行:

1
2
3
ERASE_DISKS=/dev/vda \
setup-alpine -e \
-f http://192.168.130.1:8080/alpine/answers

参数含义:

  • -m sys:将系统安装到磁盘
  • -s 0:不创建 swap
  • -k virt:安装虚拟机内核
  • /dev/vda:安装目标磁盘
  • -e:root 密码为空,仅适用于隔离实验

ERASE_DISKS=/dev/vda 会清空目标磁盘,务必确认它是新建的实验盘。

6. 改为硬盘优先启动

看到以下提示后,说明磁盘安装完成:

1
Installation is complete. Please reboot.

关闭虚拟机,将启动顺序改为:

1
2
<boot dev='hd'/>
<boot dev='network'/>

重新启动:

1
2
3
virsh shutdown pxe-alpine
virsh edit pxe-alpine
virsh start pxe-alpine

如果控制台出现以下内容,说明已经从磁盘启动:

1
2
Welcome to Alpine Linux 3.24
pxe-alpine login:

使用 VirtualBMC 模拟裸金属 IPMI

PXE 链路验证完成后,可以在同一台宿主机上运行 VirtualBMC,把 pxe-alpine 暴露成一个支持 IPMI 的虚拟裸金属节点:

1
2
3
4
5
6
7
ipmitool
↓ IPMI / UDP 6230
VirtualBMC
↓ libvirt API
pxe-alpine VM

PXE 或虚拟磁盘

VirtualBMC 不提供 DHCP,也不传输启动文件。它只把 IPMI 电源和启动设备命令转换成 libvirt 操作。

1. 安装并托管 VirtualBMC

为了不污染系统 Python,把 VirtualBMC 3.3.0 安装到独立 venv:

1
2
python3 -m venv /opt/virtualbmc
/opt/virtualbmc/bin/pip install virtualbmc

使用 systemd 托管 vbmcd

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=VirtualBMC daemon
After=network.target libvirtd.service
Requires=libvirtd.service

[Service]
Type=simple
ExecStart=/opt/virtualbmc/bin/vbmcd --foreground
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target

2. 为虚拟机创建 BMC

本实验只监听回环地址,避免把弱实验密码暴露到外部网络:

1
2
3
4
5
6
7
8
9
/opt/virtualbmc/bin/vbmc add pxe-alpine \
--username admin \
--password '<实验密码>' \
--address 127.0.0.1 \
--port 6230 \
--libvirt-uri qemu:///system

/opt/virtualbmc/bin/vbmc start pxe-alpine
/opt/virtualbmc/bin/vbmc list

实测监听状态:

1
UNCONN 0 0 127.0.0.1:6230 0.0.0.0:* users:(("vbmcd",...))

3. IPMI 电源控制实测

1
2
3
4
5
6
7
8
ipmitool -I lanplus -H 127.0.0.1 -p 6230 \
-U admin -P '<实验密码>' power status

ipmitool -I lanplus -H 127.0.0.1 -p 6230 \
-U admin -P '<实验密码>' power soft

ipmitool -I lanplus -H 127.0.0.1 -p 6230 \
-U admin -P '<实验密码>' power on

本次结果:

1
2
3
4
5
Chassis Power is on
Chassis Power Control: Soft
Chassis Power is off
Chassis Power Control: Up/On
Chassis Power is on

IPMI 与 virsh domstate pxe-alpine 的开关机状态一致。

4. IPMI 强制下一次从 PXE 启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
before=$(grep -c 'GET /alpine/boot.ipxe' /var/log/nginx/access.log)

ipmitool -I lanplus -H 127.0.0.1 -p 6230 \
-U admin -P '<实验密码>' power off

ipmitool -I lanplus -H 127.0.0.1 -p 6230 \
-U admin -P '<实验密码>' chassis bootdev pxe

ipmitool -I lanplus -H 127.0.0.1 -p 6230 \
-U admin -P '<实验密码>' power on

for n in $(seq 1 40); do
after=$(grep -c 'GET /alpine/boot.ipxe' /var/log/nginx/access.log)
[ "$after" -gt "$before" ] && break
sleep 1
done

echo $((after - before))

先完整关机,再设置 PXE 并开机,才能确保运行中的 VM 重新执行固件启动流程。本次 nginx 请求数增加 1,随后日志证明新一轮 PXE 下载完成:

1
2
3
4
5
NEW_PXE_REQUESTS=1
GET /alpine/boot.ipxe 200
GET /alpine/vmlinuz-virt 200
GET /alpine/initramfs-virt 200
GET /alpine/modloop-virt 200

5. 恢复磁盘启动时必须冷启动

1
2
3
4
5
6
7
8
ipmitool -I lanplus -H 127.0.0.1 -p 6230 \
-U admin -P '<实验密码>' chassis bootdev disk

ipmitool -I lanplus -H 127.0.0.1 -p 6230 \
-U admin -P '<实验密码>' power off

ipmitool -I lanplus -H 127.0.0.1 -p 6230 \
-U admin -P '<实验密码>' power on

这里不能只执行 power reset。VirtualBMC 3.3.0 通过 defineXML() 修改 libvirt 的持久化 XML;运行中的 QEMU 仍保留旧的 live XML。完整关机再开机后,QEMU 才会读取新的启动设备。

实测最终状态:

1
2
3
4
Chassis Power is on
Boot Device Selector : Force Boot from default Hard-Drive
<boot dev='hd'/>
NEW_PXE_REQUESTS=0

验收结果

检查项 结果
DHCP 获取 192.168.130.110
iPXE 脚本与启动文件 HTTP 下载成功
Alpine 安装环境 启动成功
磁盘安装 /dev/vda 安装成功
重启 从硬盘进入 Alpine
资源 1 vCPU / 512 MiB / 2 GiB
qcow2 实际占用 约 90 MiB
SSH、NTP、普通用户 未安装
VirtualBMC 服务 systemd 运行并启用开机启动
IPMI 电源状态、软关机、开机 与 libvirt 状态一致
IPMI bootdev pxe 冷启动后成功进入 PXE
PXE 文件下载 boot.ipxe、kernel、initramfs、modloop 均返回 200
IPMI bootdev disk 冷启动后恢复磁盘,未产生新 PXE 请求

排障速查

现象 优先检查
客户端拿不到 IP 网桥、libvirt DHCP
已拿到 IP,但 nginx 无请求 DHCP 启动地址、iPXE HTTP 支持
只请求了 boot.ipxe 脚本语法、内核 URL
文件返回 200,但无内核日志 内核、initramfs、启动参数
Alpine 已启动,但磁盘为空 setup-alpine 尚未执行或失败
IPMI 无响应 vbmcd、实例状态、UDP 监听地址和端口
bootdev 返回成功但查询仍是旧值 对比 virsh dumpxml--inactive,通过完整关机开机加载持久化 XML
旧虚拟 BMC 持续报错 对不存在的 domain 执行 vbmc stop <domain>,保留配置但停止实例

参考资料