从 QEMU 到 VMware:磁盘镜像迁移实战
上一篇让最小 Linux 脱离了 QEMU 的保姆模式,做了一块包含 GRUB、内核和根文件系统的自引导磁盘。这篇把同一块磁盘搬到 VMware Workstation 里——看起来只是格式转换,实际上踩了好几个坑:磁盘格式、控制器驱动、终端设备、内核日志刷屏。每个都值得单独说说。
格式转换
QEMU 使用 raw 格式(逐字节磁盘映像),VMware 使用 VMDK 格式。qemu-img 可以直接转换:
qemu-img convert -f raw -O vmdk ~/build/minilinux.img ~/build/minilinux.vmdk-f raw:输入格式-O vmdk:输出格式
VMDK 不只是换了个扩展名——它有自己的元数据头,描述磁盘几何结构、虚拟硬件信息等。raw 格式的优势是通用(任何工具都能直接读写),VMDK 的优势是支持稀疏分配(磁盘镜像文件大小 ≤ 实际使用量,而不是始终等于磁盘容量)。
转换后可以把 VMDK 复制到 Windows 侧:
cp ~/build/minilinux.vmdk /mnt/c/Users/<用户名>/Desktop/minilinux.vmdk在 VMware 中创建虚拟机
- VMware Workstation → 新建虚拟机 → 自定义
- 客户机操作系统选 Linux → Other Linux 5.x kernel 64-bit
- 内存 128 MB 即可
- 硬盘选择 使用现有虚拟磁盘 → 选择桌面上的
minilinux.vmdk
直接启动试试。
第一个坑:磁盘控制器
启动后内核 panic:
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)unknown-block(0,0) 说明内核完全找不到磁盘——主设备号和次设备号都是 0。
原因:VMware 默认使用 LSI Logic Parallel(或 BusLogic)SCSI 控制器挂载硬盘。我们编译内核时用的是 defconfig,SCSI 控制器驱动的编译状态:
grep -E 'CONFIG_SCSI_BUSLOGIC|CONFIG_SCSI_SYM53C8XX_2|CONFIG_FUSION' .config# CONFIG_SCSI_BUSLOGIC is not set
# CONFIG_SCSI_SYM53C8XX_2 is not set
# CONFIG_FUSION is not set全部 not set。内核没有 VMware SCSI 控制器的驱动,所以完全看不到磁盘。
解决方案
有两条路:
A. 在 VMware 里改用 IDE 控制器(推荐,不用重编译内核)
VMware 虚拟机设置 → 硬盘 → 高级 → 虚拟设备节点改为 IDE 而不是 SCSI。
IDE 控制器在 VMware 里模拟的是 PIIX4 芯片组,而 defconfig 已经编译了对应的 ata_piix 驱动:
grep CONFIG_ATA_PIIX .configCONFIG_ATA_PIIX=y改成 IDE 后磁盘会被识别为 /dev/sda,和 QEMU 一致。
B. 重编译内核加上 SCSI 驱动
cd ~/build/linux-6.18.19
sed -i 's/# CONFIG_SCSI_BUSLOGIC is not set/CONFIG_SCSI_BUSLOGIC=y/' .config
sed -i 's/# CONFIG_SCSI_SYM53C8XX_2 is not set/CONFIG_SCSI_SYM53C8XX_2=y/' .config
make -j$(nproc) bzImage然后重新复制内核到磁盘镜像并转换 VMDK。这条路更"正确"但更麻烦。
第二个坑:终端设备
改成 IDE 后内核不再 panic,磁盘被正确识别和挂载。但启动完成后——没有 shell。屏幕停在内核日志的最后几行,看不到交互提示符。
这是 console 参数的问题。我们的 grub.cfg 里写的是:
console=tty0 console=ttyS0Linux 内核的 console 规则:最后一个 console= 参数成为 /dev/console。这里 ttyS0 是最后一个,所以 /dev/console = ttyS0(串口)。
BusyBox init 默认在 /dev/console 上开 shell。但 VMware 的图形窗口连接的是 tty0(VGA 显示器),不是 ttyS0(串口)。shell 输出到了看不见的串口,VGA 显示器只能看到内核日志。
解决方案
不改 console 参数,而是在 inittab 里在两个终端上各开一个 shell:
tty0::respawn:-/bin/sh
ttyS0::respawn:-/bin/sh这样不管哪个终端可见,都有 shell:
- VMware 图形窗口 → 看到
tty0上的 shell - QEMU
-nographic→ 看到ttyS0上的 shell - 如果某个终端设备不存在,BusyBox 会打开失败并自动忽略
这比根据虚拟机类型修改 grub.cfg 更优雅——系统对运行环境完全无感。正式的 Linux 发行版用 getty 实现同样的效果:在每个 TTY 上运行一个 getty 实例,负责显示登录提示符。
第三个坑:内核日志刷屏
改完终端后,VMware 里能看到 shell 了——但需要按一下 Enter 才能看到提示符。屏幕被内核启动日志填满了,shell 提示符被挤到了看不见的地方。
第一反应是在 rcS 启动脚本里加 clear,但没用。原因是时序问题:
- rcS 执行
clear,屏幕确实清了 - 但内核有些驱动是延迟初始化的(deferred probe),
clear之后内核又往屏幕打了新日志 - 新日志覆盖了
clear的效果
解决办法是从源头禁止内核继续打日志,然后对每个终端分别清屏:
dmesg -n 1
printf "\033c" > /dev/tty0 2>/dev/null
printf "\033c" > /dev/ttyS0 2>/dev/nulldmesg -n 1:设置内核 console loglevel 为 1(只有 KERN_EMERG 级别的消息才输出到屏幕),从此刻起内核不再往屏幕打常规日志printf "\033c" > /dev/tty0:对 VGA 终端执行 VT100 重置清屏printf "\033c" > /dev/ttyS0:对串口终端执行 VT100 重置清屏
为什么要分别重定向?因为 rcS 脚本的标准输出走 /dev/console,而 /dev/console 只对应最后一个 console= 参数指定的设备(这里是 ttyS0)。只写 printf "\033c" 的话,只清了串口终端,VGA 上的内核日志还在。
顺序很重要:先 dmesg -n 1 堵住内核日志,再清屏。反过来的话,清屏和禁日志之间可能有新日志溜进来。日志并没有丢失——在 shell 里随时可以用 dmesg 查看全部启动日志。
完整配置
三个文件的最终状态:
/boot/grub/grub.cfg
set timeout=3
set default=0
menuentry "Mini Linux (BusyBox + Custom Kernel 6.18.19)" {
linux /boot/vmlinuz root=/dev/sda1 console=tty0 console=ttyS0
}/etc/inittab
::sysinit:/etc/init.d/rcS
tty0::respawn:-/bin/sh
ttyS0::respawn:-/bin/sh
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r/etc/init.d/rcS
#!/bin/sh
mount -t devtmpfs none /dev 2>/dev/null
mount -t proc none /proc
mount -t sysfs none /sys
dmesg -n 1
printf "\033c" > /dev/tty0 2>/dev/null
printf "\033c" > /dev/ttyS0 2>/dev/null
for tty in /dev/tty0 /dev/ttyS0; do
[ -e "$tty" ] && {
echo "" > "$tty"
echo "=====================================" > "$tty"
echo " Mini Linux (BusyBox + Custom Kernel)" > "$tty"
echo " Kernel: $(uname -r)" > "$tty"
echo " Boot took $(cut -d' ' -f1 /proc/uptime) seconds" > "$tty"
echo "=====================================" > "$tty"
echo "" > "$tty"
}
done磁盘更新流程
每次修改磁盘内容后,需要重新转换 VMDK:
# 挂载
sudo losetup --show -fP ~/build/minilinux.img
sudo mount /dev/loop0p1 ~/build/mnt
# 修改文件...
# 卸载
sudo umount ~/build/mnt
sudo losetup -D
# 转换
qemu-img convert -f raw -O vmdk ~/build/minilinux.img ~/build/minilinux.vmdk注意事项:
qemu-img要求没有其他进程使用镜像文件。如果报Failed to get "write" lock,用fuser ~/build/minilinux.img找出占用进程并结束losetup -D断开所有 loop 设备后再转换,否则 loop 设备会持有文件锁
跨平台兼容性清单
如果你要让同一个磁盘镜像在不同虚拟机里都能用:
| 检查项 | QEMU | VMware | VirtualBox |
|---|---|---|---|
| 磁盘格式 | raw / qcow2 | vmdk | vdi / vmdk |
| 默认磁盘控制器 | IDE (ata_piix) | SCSI (LSI Logic) | SATA (AHCI) |
| 需要的内核驱动 | CONFIG_ATA_PIIX=y | CONFIG_SCSI_SYM53C8XX_2=y 或改用 IDE | CONFIG_SATA_AHCI=y |
| 图形终端 | tty0(-display gtk)或 ttyS0(-nographic) | tty0 | tty0 |
| 格式转换命令 | N/A | qemu-img convert -O vmdk | qemu-img convert -O vdi |
最省事的方案:所有虚拟机都用 IDE 控制器 + inittab 双终端 shell + dmesg -n 1 清屏。这样一套配置到处跑。
本系列回顾
从第一篇到现在,我们从零构建了一个完整的 Linux 系统:
| 篇目 | 做了什么 | 产物 |
|---|---|---|
| WSL 内核编译 | 验证工具链,理解编译流程 | 41 MB bzImage(allmodconfig) |
| QEMU 内核编译 | defconfig 精简内核 | 14 MB bzImage |
| BusyBox 静态编译 | 构建用户空间工具 | 2.4 MB busybox |
| QEMU 最小系统 | initramfs + 直接加载启动 | 1.3 MB initramfs.cpio.gz |
| GRUB 可引导磁盘 | 自引导磁盘 + 持久化存储 | 128 MB minilinux.img |
| QEMU 到 VMware | 跨平台迁移 + 驱动适配 | minilinux.vmdk |
从 make defconfig 到在 VMware 里看到 shell 提示符,整个过程用到的外部依赖只有 Linux 源码、BusyBox 源码和 GRUB 源码。没有发行版安装器、没有 debootstrap、没有 Docker——纯手工从源码搭建。