本文由 Codex 根据 PXE、Alpine 与 VirtualBMC 实测和讨论过程辅助整理生成。
为了理解裸金属装机流程,我在一台 KVM 宿主机上搭建了最小 PXE 实验:客户端只有 1 vCPU、512 MiB 内存和 2 GiB 磁盘,通过网络启动 Alpine,再将系统安装到虚拟磁盘。
最终 qcow2 文件实际占用约 90 MiB,适合资源有限的测试环境。
PXE 启动原理
PXE 不是操作系统安装器。它解决的是:一台没有可用系统的机器,如何通过网络获得并启动下一段程序。
完整链路如下:
1 | BIOS/UEFI |
DHCP:告诉客户端去哪里启动
PXE 客户端首先通过 DHCP 的 Discover、Offer、Request、ACK 获取网络配置。与普通 DHCP 不同的是,服务端还会返回启动文件地址:
1 | IP:192.168.130.110 |
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 | 上电 |
传统磁盘启动与 PXE 启动的主要区别只在前半段:
| 启动方式 | 内核和 initramfs 的来源 | 真正的根文件系统可能位于 |
|---|---|---|
| 磁盘启动 | GRUB 从本地磁盘读取 | 本地磁盘 |
| PXE 启动 | 固件 PXE 客户端加载 iPXE,再由 iPXE 从网络下载 | 内存、本地磁盘或网络存储 |
如果 initramfs 已经包含完整用户空间,它可以继续作为最终根文件系统,不一定执行 switch_root。这正是部分 Live、救援和纯内存系统的工作方式。
为什么内核加 initramfs 就能启动 Linux
本实验由 iPXE 启动 Alpine 时,至少需要向客户端提供两部分:
- 内核:初始化 CPU、内存、中断和设备,管理进程与硬件资源
- initramfs:提供早期用户空间,包括
/init、BusyBox、驱动加载和网络脚本
启动过程是:
1 | iPXE 将 kernel 和 initramfs 放入内存 |
内核只有管理硬件和执行程序的能力;initramfs 只有文件和启动工具。两者结合,才构成一个可以运行的最小 Linux。它可以只是安装环境,也可以是完全运行在内存里的系统,但不代表操作系统已经安装到磁盘。
initramfs 并非所有 Linux 启动方式都必需。如果根文件系统位于内核可直接访问的设备上,且所需的文件系统与存储驱动已编译进内核,内核也可以根据 root= 参数直接挂载磁盘根文件系统并执行其中的 /sbin/init。本实验需要 initramfs,是因为它要先提供网络启动所需的早期用户空间。
initramfs 为什么不能无限增大
initramfs 没有统一的固定大小上限,实际边界取决于客户端内存和固件、引导器的内存布局。启动时内存中可能同时存在:
1 | 压缩的 initramfs |
因此不能只看 initramfs 压缩文件的大小。文件过大时,可能在 iPXE 分配内存、内核解压或用户空间启动阶段失败。
本实验将内容拆分为 vmlinuz-virt、initramfs-virt、modloop-virt 和 HTTP APK 仓库,目的之一就是让 initramfs 只保留早期启动必需内容,额外模块与软件包在网络可用后再按需获取。
网吧无盘 Windows 如何启动
无盘系统通常不会把完整 Windows 和所有游戏一次性复制到内存,而是把服务器上的远程块设备伪装成本地系统盘:
1 | 客户机的 C: |
公共镜像便于统一更新,写时复制层避免多台客户机相互覆盖;服务器 NVMe、内存和客户端内存则缓存热点数据。
Windows 无盘启动主要有两种链路。
第一种由 iPXE 直接连接 iSCSI:
1 | BIOS/UEFI → PXE/iPXE → 挂载 iSCSI LUN |
iPXE 脚本示意如下:
1 | #!ipxe |
iPXE 会通过 iBFT 等机制向操作系统描述 iSCSI 启动盘。因此,这条链路不要求先启动 WinPE;远程盘在 Windows Boot Manager 运行前就已经连接。
第二种先启动 WinPE 或厂商的精简引导环境:
1 | BIOS/UEFI → PXE → WinPE/厂商引导环境 |
这种模式下,WinPE 的作用与 Linux initramfs 相似:先提供最小用户空间,获得网络存储能力,再找到真正的系统盘。无论采用哪种方式,关键都不是“必须启动 WinPE”,而是 Windows 第一次需要读取系统盘之前,远程磁盘必须已经可用;Windows 接管后,启动级驱动必须能继续维持连接。
实验架构
1 | KVM 宿主机 |
为避免影响现有网络,DHCP 仅绑定到独立的 virbr-pxe 网桥。
操作过程
1. 创建隔离 PXE 网络
创建 pxe-lab.xml:
1 | <network> |
启用网络:
1 | virsh net-define pxe-lab.xml |
2. 准备 Alpine 启动文件
nginx 在 8080 端口暴露 /var/lib/ironic/httpboot。创建目录并下载 Alpine virt netboot 文件:
1 | mkdir -p /var/lib/ironic/httpboot/alpine |
三份文件合计约 43 MiB:
vmlinuz-virt:Linux 内核initramfs-virt:启动早期的内存根文件系统modloop-virt:Alpine 内核模块
3. 编写 iPXE 脚本
创建 /var/lib/ironic/httpboot/alpine/boot.ipxe:
1 | #!ipxe |
先确认 HTTP 文件可访问:
1 | curl http://192.168.130.1:8080/alpine/boot.ipxe |
本次实验中公网仓库较慢,因此后来把安装需要的约 48 个基础包缓存到 nginx,并将 alpine_repo 改为:
1 | http://192.168.130.1:8080/alpine/main |
使用本地仓库时,应答文件中的 APKREPOSOPTS 也要改成同一地址。
不要直接把 Alpine virt ISO 当作完整安装仓库,它可能缺少 linux-virt、syslinux 等磁盘安装包。
4. 创建最小虚拟机
先创建稀疏磁盘:
1 | qemu-img create -f qcow2 \ |
虚拟机关键配置如下:
1 | <domain type='kvm'> |
保存为 pxe-alpine.xml,然后启动:
1 | virsh define pxe-alpine.xml |
5. 将 Alpine 安装到磁盘
进入内存中的 Alpine 后,可执行 setup-alpine 交互式安装。为了减少重复输入,本实验使用应答文件:
1 | KEYMAPOPTS=none |
将文件保存为 nginx 可访问的 /var/lib/ironic/httpboot/alpine/answers,再从 Alpine 控制台执行:
1 | ERASE_DISKS=/dev/vda \ |
参数含义:
-m sys:将系统安装到磁盘-s 0:不创建 swap-k virt:安装虚拟机内核/dev/vda:安装目标磁盘-e:root 密码为空,仅适用于隔离实验
ERASE_DISKS=/dev/vda会清空目标磁盘,务必确认它是新建的实验盘。
6. 改为硬盘优先启动
看到以下提示后,说明磁盘安装完成:
1 | Installation is complete. Please reboot. |
关闭虚拟机,将启动顺序改为:
1 | <boot dev='hd'/> |
重新启动:
1 | virsh shutdown pxe-alpine |
如果控制台出现以下内容,说明已经从磁盘启动:
1 | Welcome to Alpine Linux 3.24 |
使用 VirtualBMC 模拟裸金属 IPMI
PXE 链路验证完成后,可以在同一台宿主机上运行 VirtualBMC,把 pxe-alpine 暴露成一个支持 IPMI 的虚拟裸金属节点:
1 | ipmitool |
VirtualBMC 不提供 DHCP,也不传输启动文件。它只把 IPMI 电源和启动设备命令转换成 libvirt 操作。
1. 安装并托管 VirtualBMC
为了不污染系统 Python,把 VirtualBMC 3.3.0 安装到独立 venv:
1 | python3 -m venv /opt/virtualbmc |
使用 systemd 托管 vbmcd:
1 | [Unit] |
2. 为虚拟机创建 BMC
本实验只监听回环地址,避免把弱实验密码暴露到外部网络:
1 | /opt/virtualbmc/bin/vbmc add pxe-alpine \ |
实测监听状态:
1 | UNCONN 0 0 127.0.0.1:6230 0.0.0.0:* users:(("vbmcd",...)) |
3. IPMI 电源控制实测
1 | ipmitool -I lanplus -H 127.0.0.1 -p 6230 \ |
本次结果:
1 | Chassis Power is on |
IPMI 与 virsh domstate pxe-alpine 的开关机状态一致。
4. IPMI 强制下一次从 PXE 启动
1 | before=$(grep -c 'GET /alpine/boot.ipxe' /var/log/nginx/access.log) |
先完整关机,再设置 PXE 并开机,才能确保运行中的 VM 重新执行固件启动流程。本次 nginx 请求数增加 1,随后日志证明新一轮 PXE 下载完成:
1 | NEW_PXE_REQUESTS=1 |
5. 恢复磁盘启动时必须冷启动
1 | ipmitool -I lanplus -H 127.0.0.1 -p 6230 \ |
这里不能只执行 power reset。VirtualBMC 3.3.0 通过 defineXML() 修改 libvirt 的持久化 XML;运行中的 QEMU 仍保留旧的 live XML。完整关机再开机后,QEMU 才会读取新的启动设备。
实测最终状态:
1 | Chassis Power is on |
验收结果
| 检查项 | 结果 |
|---|---|
| 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>,保留配置但停止实例 |