Skip to content

从 QEMU 到 VMware:磁盘镜像迁移实战

上一篇让最小 Linux 脱离了 QEMU 的保姆模式,做了一块包含 GRUB、内核和根文件系统的自引导磁盘。这篇把同一块磁盘搬到 VMware Workstation 里——看起来只是格式转换,实际上踩了好几个坑:磁盘格式、控制器驱动、终端设备、内核日志刷屏。每个都值得单独说说。

格式转换

QEMU 使用 raw 格式(逐字节磁盘映像),VMware 使用 VMDK 格式。qemu-img 可以直接转换:

bash
qemu-img convert -f raw -O vmdk ~/build/minilinux.img ~/build/minilinux.vmdk
  • -f raw:输入格式
  • -O vmdk:输出格式

VMDK 不只是换了个扩展名——它有自己的元数据头,描述磁盘几何结构、虚拟硬件信息等。raw 格式的优势是通用(任何工具都能直接读写),VMDK 的优势是支持稀疏分配(磁盘镜像文件大小 ≤ 实际使用量,而不是始终等于磁盘容量)。

转换后可以把 VMDK 复制到 Windows 侧:

bash
cp ~/build/minilinux.vmdk /mnt/c/Users/<用户>/Desktop/minilinux.vmdk

在 VMware 中创建虚拟机

  1. VMware Workstation → 新建虚拟机 → 自定义
  2. 客户机操作系统选 Linux → Other Linux 5.x kernel 64-bit
  3. 内存 128 MB 即可
  4. 硬盘选择 使用现有虚拟磁盘 → 选择桌面上的 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 控制器驱动的编译状态:

bash
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 驱动:

bash
grep CONFIG_ATA_PIIX .config
CONFIG_ATA_PIIX=y

改成 IDE 后磁盘会被识别为 /dev/sda,和 QEMU 一致。

B. 重编译内核加上 SCSI 驱动

bash
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=ttyS0

Linux 内核的 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,但没用。原因是时序问题

  1. rcS 执行 clear,屏幕确实清了
  2. 但内核有些驱动是延迟初始化的(deferred probe),clear 之后内核又往屏幕打了新日志
  3. 新日志覆盖了 clear 的效果

解决办法是从源头禁止内核继续打日志,然后对每个终端分别清屏:

bash
dmesg -n 1
printf "\033c" > /dev/tty0 2>/dev/null
printf "\033c" > /dev/ttyS0 2>/dev/null
  • dmesg -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

bash
#!/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:

bash
# 挂载
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 设备会持有文件锁

跨平台兼容性清单

如果你要让同一个磁盘镜像在不同虚拟机里都能用:

检查项QEMUVMwareVirtualBox
磁盘格式raw / qcow2vmdkvdi / vmdk
默认磁盘控制器IDE (ata_piix)SCSI (LSI Logic)SATA (AHCI)
需要的内核驱动CONFIG_ATA_PIIX=yCONFIG_SCSI_SYM53C8XX_2=y 或改用 IDECONFIG_SATA_AHCI=y
图形终端tty0(-display gtk)或 ttyS0(-nographic)tty0tty0
格式转换命令N/Aqemu-img convert -O vmdkqemu-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——纯手工从源码搭建。

最后更新于:

Hosted by GitHub Pages