KVM QEMU运行硬盘中的系统 最近老师购入一台工作站,老师要做一些分子动力学的仿真计算和深度学习,同时老师要我做这台机器的管理员,我自己可以在上面做一些我比较有兴趣的工作,比如灵巧手和机器人相关的仿真强化学习,或者一些用于当前项目的深度学习工作。因为老师用的 Ansys 和我使用的 SolidWorks 、AutoDesk 等软件只有Windows版本,而做分子动力学的 VASP 又只有 Linux 版本,所以购买的工作站就安装了双系统。同时,为了能够尽可能地使机器长时间开机,便于远程连接或长时间计算,我决定在Linux中通过 KVM 和 QEMU 启动硬盘中的Windows, 以此实现Windows和Linux两个系统的软件能够同时使用。
设备配置
CPU:单颗 Intel Xeon Scalable Platinum 8488C CPU,最新 Products formerly Sapphire Rapids 架构,共48个计算物理核心,96个线程
RAM:三星服务器 128G DDR5 ECC 5600 GHZRegistered Shared Memory
GPU:NVIDIA RTX5090 32G显存
硬盘:1块1TB,一块8TB
双系统安装 工作站发来前,已经由厂家安装了 Windows10 和提前列出的Windows平台软件,所以我们只需要安装 Linux 即可。本来我的计划是安装 Ubuntu22,但是最终还是选择了更加熟悉的 ArchLinux 。因为厂家已经安装了Windows平台的一些软件,为了方便,我们就不删掉他们,而是直接在QEMU里面加载Windows。
KVM 基于内核的虚拟机 (英语:K ernel-based V irtual M achine,缩写为KVM )是一种用于Linux内核 中的虚拟化基础设施,可将Linux内核转化为一个虚拟机监视器 。KVM于2007年2月5日被并入Linux 2.6.20核心中。KVM需要支持硬件虚拟化拓展 特性的处理器。
详细介绍可以参考 Wikipedia/KVM
Linux系统启动后,查看设备可以看到有一个 kvm
设备:
为了能够让当前用户使用 kvm,需要将当前用户添加进 kvm
组
1 2 3 4 sudo usermod -aG kvm $(whoami ) id uid=1000(username) gid=1000(username) groups =1000(username),967(docker),992(kvm),998(wheel)
QEMU Installation QEMU (Quick Emulator)是一款免费开源模拟器,由法布里斯·贝拉 (Fabrice Bellard)等人编写。其与Bochs ,PearPC 类似,但拥有高速(配合KVM )、跨平台的特性。
详细介绍可查看 Wikipedia/QEMU
Arch Linux 安装 QEMU 命令:
1 2 paru extra/qemu-full -S paru extra/edk2-ovmf -S
直接安装全部软件包即可
Configuration 由于默认 QEMU 连接的是用户模式的机器,而我们把Windows运行在系统层级,以便所有用户均可访问,所以需要配置一下,让用户能够默认访问系统的 QEMU
1 2 3 4 mkdir -pv ${HOME} /.config/libvirt/cat >> ${HOME} /.config/libvirt/libvirt.conf << EOF uri_default = "qemu:///system" EOF
Libvirt Installation libvirt 是一套用于管理硬件虚拟化 的开源 API 、守护进程 与管理工具。[3] 此套组可用于管理KVM 、Xen 、VMware ESXi 、QEMU 及其他虚拟化技术。libvirt内置的API广泛用于云解决方案开发中的虚拟机监视器 编排层(Orchestration Layer)。
安装命令:
1 2 paru extra/libvirt -S paru extra/virt-manager -S
Configuration 需要把用户添加进 libvirt
组,同时启动 libvirtd
系统服务:
1 2 sudo systemctl enable --now libvirtd sudo usermod -aG libvirt $(whoami )
只有配置的管理员用户需要添加到 libvirt
组,最终连接的其他普通用户不需要添加到该组。
准备工作 Windows 10 ISO 为了防止虚拟机出现问题,损坏Windows的启动,我们在虚拟机中不使用 Windows 的 EFI, 只使用Windows的C,D盘,并重新生成一个新的 EFI 用于在虚拟机中启动 Windows。
这个新的 EFI 需要使用 Windows 的一个工具生成,该工具在 Windows ISO 中有,所以我们从 Windows 网站下载一个 Windows 10 的启动镜像。
下载地址
硬盘线性阵列排布 介绍 由于Arch Linux 安装时,在 1T 的硬盘分出一个分区用来安装系统,从 8T 硬盘分出一个分区用来存储软件、数据和用户目录,而同一块设备 只能有一方在使用,因此我们需要手动重新排列分区,以分离各个分区,防止两方共同使用同一设备。我们可以使用 losetup
和 mdadm
重新排列硬盘分区成一个虚拟设备,并让虚拟机使用这个虚拟设备进行启动。
losetup命令用来设置循环设备,查看回环设备的状态。循环设备可把文件虚拟成区块设备,籍以模拟整个文件系统,让用户得以将其视为硬盘驱动器,光驱或软驱等设备,并挂入当作目录来使用。 原文链接:https://www.linuxcool.com/losetup
mdadm命令来自英文词组multiple devices admin的缩写,其功能是管理RAID设备。作为Linux系统下软RAID设备的管理神器,mdadm命令可以进行创建、调整、监控、删除等全套管理操作。 原文链接:https://www.linuxcool.com/mdadm
原始的硬盘排布为:
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 sudo fdisk -l /dev/nvme0n1 /dev/sda Disk /dev/nvme0n1: 931.51 GiB, 1000204886016 bytes, 1953525168 sectors Disk model: Samsung SSD 980 1TB Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 16384 bytes / 131072 bytes Disklabel type : gpt Disk identifier: 5E0BA9DE-7C92-478F-ADE5-F178A7B6506B Device Start End Sectors Size Type /dev/nvme0n1p1 2048 206847 204800 100M EFI System /dev/nvme0n1p2 206848 239615 32768 16M Microsoft reserved /dev/nvme0n1p3 239616 1322747224 1322507609 630.6G Microsoft basic data /dev/nvme0n1p4 1951893504 1953521663 1628160 795M Windows recovery environment /dev/nvme0n1p5 1322747904 1323747327 999424 488M EFI System /dev/nvme0n1p6 1323747328 1325893631 2146304 1G EFI System /dev/nvme0n1p7 1325893632 1951893503 625999872 298.5G Linux root (x86-64) Partition table entries are not in disk order. Disk /dev/sda: 7.28 TiB, 8001563222016 bytes, 15628053168 sectors Disk model: HGST HUS728T8TAL Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 4096 bytes I/O size (minimum/optimal): 4096 bytes / 4096 bytes Disklabel type : gpt Disk identifier: DAB5F193-5F0A-4513-AE7C-FC6AC785D92D Device Start End Sectors Size Type /dev/sda1 34 32767 32734 16M Microsoft reserved /dev/sda2 32768 4890632191 4890599424 2.3T Microsoft basic data /dev/sda3 4890632192 11333083135 6442450944 3T Linux filesystem /dev/sda4 11333083136 15628053134 4294969999 2T Linux filesystem Partition 1 does not start on physical sector boundary.
其中 /dev/nvme0n1p1
是 Windows 实际的 EFI 分区,我们保留不动,防止操作失误而导致无法启动。/dev/nvme0n1p3
是Windows的 C盘,/dev/sda2
是 Windows 的D盘。
根据 GPT 分区规范,我们可以创建一个新的线性阵列作为虚拟设备
但是!!! 沟槽的厂家给 8T 硬盘第一个分区没有对齐扇区!!!
这种情况会导致后面分区工具自动计算比较麻烦,一般分区工具像是cfdisk
会自动对齐扇区,导致我们线性阵列分区与实际分区的扇区错位,导致分区不可挂载。所以我们最终实际建立的线性阵列为
图中,C盘D盘以外的区域均为我们自己创建的全新的扇区。
准备扇区 假设qemu虚拟机的名称为 windows10
,为了方便管理,我们把所有相关文件放在机器的文件夹中。首先创建文件夹
1 2 3 4 mkdir -pv /etc/libvirt/hooks/qemu.d/windows10/cd /etc/libvirt/hooks/qemu.d/windows10/mkdir -pv md0 md1 cd md0
有上图,我们可以计算 Windows 的 EFI 分区和 GPT 分区表保留的分区共101M,C盘后面也应该留1M保留分区,于是我们可以使用 dd
创建这些扇区:
1 2 3 sudo dd if =/dev/zero of=loop-efi0 bs=1M count=101 sudo dd if =/dev/zero of=loop-efi1 bs=1M count=1 ls -lah
输出应该为
1 2 3 4 5 6 7 8 9 10 11 101+0 records in 101+0 records out 105906176 bytes (106 MB, 101 MiB) copied, 0.0641618 s, 1.7 GB/s 1+0 records in 1+0 records out 1048576 bytes (1.0 MB, 1.0 MiB) copied, 0.00157278 s, 667 MB/s total 102M drwxr-xr-x 1 root root 36 Jun 20 01:04 . drwxr-xr-x 1 root root 80 Jun 20 01:00 .. -rw-r--r-- 1 root root 101M Jun 20 01:04 loop-efi0 -rw-r--r-- 1 root root 1.0M Jun 20 01:04 loop-efi1
创建启动线性阵列的脚本 start-md0.sh
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 cat << EOF | sudo tee start-md0.sh > /dev/null #!/usr/bin/env bash WIN_PART=/dev/nvme0n1p3 # Windows C盘设备 EFI_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)" # 获取到这个脚本的路径 VDEV=/dev/md0 # 要创建的路径,应该符合 POSIX 规范,以数字结尾 # 检查是否已经运行了该脚本 if [[ -e \$VDEV ]]; then echo "\$VDEV already exists" > /dev/kmsg 2>&1 exit 1 fi # 检查 Windows 分区是否被mount,防止被两方同时使用 if mountpoint -q -- "\$WIN_PART"; then echo "Unmounting \$WIN_PART..." > /dev/kmsg 2>&1 umount \$WIN_PART fi modprobe loop modprobe linear eval "EFI_DIR=\$EFI_DIR" # 立即解析路径变量 LOOP0=\$(losetup -f "\$EFI_DIR/loop-efi0" --show) # 1M Reserved LOOP1=\$(losetup -f "\$EFI_DIR/loop-efi1" --show) # 1M reserved mdadm --build --verbose \$VDEV --chunk=512 --level=linear --raid-devices=3 \$LOOP0 \$WIN_PART \$LOOP1 chown \$USER:disk \$VDEV echo "\$LOOP0 \$LOOP1" > "\$EFI_DIR/.win10-loop-devices" EOF sudo chmod +x ./start-md0.sh sudo ./start-md0.sh ls /dev/md*
输出应该是
1 2 mdadm: array /dev/md0 built and started. /dev/md0
再创建一个脚本用来停止线性阵列:
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 cat << EOF | sudo tee stop-md0.sh > /dev/null #!/usr/bin/env bash EFI_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)" VDEV=/dev/md0 # 停止 RAID 设备 if [[ -e "\$VDEV" ]]; then mdadm --stop "\$VDEV" else echo "Device \$VDEV not found. Maybe it's already stopped." fi # 卸载 loop 设备 LOOP_DEV_FILE="\$EFI_DIR/.win10-loop-devices" if [[ -f "\$LOOP_DEV_FILE" ]]; then xargs -r losetup -d < "\$LOOP_DEV_FILE" rm -f "\$LOOP_DEV_FILE" else echo "Loop device list \$LOOP_DEV_FILE not found." fi EOF sudo chmod +x ./stop-md0.sh sudo ./stop-md0.sh ls /dev/md*
输出应该是
1 2 mdadm: stopped /dev/md0 zsh: no matches found: /dev/md*
此时,我们需要参照 fdisk
输出的扇区信息,对 /dev/md0
进行分区
1 2 3 4 5 6 7 8 9 10 11 12 13 14 sudo parted /dev/md0 (parted) unit s (parted) mktable gpt (parted) (parted) mkpart primary fat32 2048 206847 (parted) mkpart primary ntfs 206848 1322714456 (parted) set 1 boot on (parted) set 1 esp on (parted) set 2 msftdata on (parted) name 1 EFI (parted) name 2 Windows (parted) quit ls /dev/md0*
此时应该多出分区的设备
1 /dev/md0 /dev/md0p1 /dev/md0p2
此时确认一下分区是否正确。如果分的扇区正好对应了 Windows C盘的起始和结束扇区,那么不需格式化即可直接挂载。
1 2 3 sudo mount /dev/md0p2 /mnt ls /mntsudo umount /mnt
应该是能够正常挂载读取的。如果挂载失败,大概率是扇区计算错误,重新使用 parted
或 cfdisk
进行分区
格式化 EFI 1 2 paru core/dosfstools -S sudo mkfs.msdos -F 32 -n EFI /dev/md0p1
按照上述步骤继续操作,可以继续为 D 盘创建线性阵列。完成后应该是这样 (我最终是把两块硬盘上保留分区也添加了进来,但实际上保留分区是不需要的)
1 2 ls /dev/md*/dev/md0 /dev/md0p1 /dev/md0p2 /dev/md0p3 /dev/md0p4 /dev/md1 /dev/md1p1
启动 libvirt
打开 virt manager
,在首选项打开 XML 编辑
创建虚拟机 使用 ISO 文件创建虚拟机,并配置适当的CPU与内存,不需要为虚拟机启用存储!! 因为我们直接使用了硬盘。
最后在安装前自定义设置
自定义设置
新建存储,设置为我们创建的线性阵列。可以把两个阵列都添加上去。
之后就可以启动虚拟机了。进入虚拟机后,不要进行安装操作 ,否则有可能会覆盖系统安装!!!
在这个界面使用 Shift + F10 打开 CMD
在 CMD 中输入命令
1 2 3 4 5 6 7 diskpart DISKPART> list disk DISKPART> select disk 0 # 选择/dev/md0在VM中对应的硬盘 DISKPART> list volume # 在分区列表中记下EFI分区序号 DISKPART> select volume 2 # 选择EFI分区 DISKPART> assign letter=Z # 为EFI分区分配驱动器号(Z:) DISKPART> exit
如果这一步找不到正确的硬盘(Disk)或正确的卷(Volume),大概率还是扇区分配不正确 ,回去重新计算,并尝试挂载
最后将驱动器C:中的系统启动信息写入到驱动器Z:(EFI分区):
1 bcdboot C:\Windows /s Z: /f ALL /v
你需要观察上述 diskpart
中的 list volume
输出,根据分卷大小来确认Windows系统卷标。或者使用 dir
命令检查硬盘分区的内容,来确定 Windows 系统盘的分卷。
命令执行完成后,可以进入 Z:
检查内容,但是注意不要修改,否则要重新运行上述命令!
之后就可以退出安装程序,关闭虚拟机,进入虚拟机详情,修改启动顺序。
之后启动虚拟机,就能发现正是硬盘里安装的 Windows了
自动启停线性阵列 安装 libvirt qemu hook helper 这是个脚本,在 libvirt 启动、关闭虚拟机时自动运行 hook
,安装命令:
1 paru aur/libvirt-hook-helper-git -S
Libvirt hook提供一种在libvirt服务某个生命周期执行特定脚本的能力,hook脚本放置在/etc/libvirt/hooks目录,关于VM管理的脚本入口文件是/etc/libvirt/hooks/qemu,其默认用法如下:
1 /etc/libvirt/hooks/qemu $vm_name $hook_name $sub_name $extra
更多信息请查阅:Libvirt文档 。要使用这个hook,需要判断VM实例名称、hook名称、子动作名称等参数,颇为不便。我在这里使用VFIO-Tools Hook Helper 对hook使用流程进行简化。Libvirt hook helper实际上是一个脚本,内容如下:
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 #!/usr/bin/env bash GUEST_NAME="$1 " HOOK_NAME="$2 " STATE_NAME="$3 " MISC="${@:4} " BASEDIR="$(dirname $0) " HOOKPATH="$BASEDIR /qemu.d/$GUEST_NAME /$HOOK_NAME /$STATE_NAME " set -e if [ -f "$HOOKPATH " ] && [ -s "$HOOKPATH " ] && [ -x "$HOOKPATH " ]; then eval \"$HOOKPATH \" "$@ " elif [ -d "$HOOKPATH " ]; then while read file; do if [ ! -z "$file " ]; then eval \"$file \" "$@ " fi done <<< "$(find -L "$HOOKPATH " -maxdepth 1 -type f -executable -print;) " fi
简单来说就是优化了hook脚本的管理方式,安装完hook helper后重启libvirtd服务,即可通过如下结构管理VM hook:
1 /etc/libvirt/hooks/qemu.d/$vm_name /$hook_name /$sub_name /*
例如名称为windows10的VM,其prepare hook、begin子动作要执行的脚本是setup.sh,则将脚本放在如下位置:
1 /etc/libvirt/hooks/qemu.d/win10/prepare/begin/setup.sh
Hook数量不限,类型不限定是shell脚本,在hashbbang中指定任何解释器均可。较为重要的几个hook类型如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 /etc/libvirt/hooks/qemu.d/$vm_name /prepare/begin/* /etc/libvirt/hooks/qemu.d/$vm_name /start/begin/* /etc/libvirt/hooks/qemu.d/$vm_name /started/begin/* /etc/libvirt/hooks/qemu.d/$vm_name /stopped/end/* /etc/libvirt/hooks/qemu.d/$vm_name /release/end/*
以上述Windows10 VM为例,创建启动和停止hook,用来线性阵列启停操作自动化。
自动启停线性阵列 Hook 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #!/usr/bin/env bash echo "[HOOK] Triggered with args: $1 $2 $3 " >> /tmp/hook.logdate >> /tmp/hook.logVM_ACTION="$2 /$3 " SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]} " ) " && pwd) " if [[ "$VM_ACTION " == "prepare/begin" ]]; then $SCRIPT /md0/start-md0.sh $SCRIPT /md1/start-md1.sh elif [[ "$VM_ACTION " == "release/end" ]]; then $SCRIPT /md0/stop-md0.sh $SCRIPT /md1/stop-md1.sh fi
整理 hook 结构 将创建的所有hooks整理一下,利用软链接的形式存放到Win10 VM对应的生命周期目录中,最后得到如下结构:
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 tree -ahl [ 72] . ├── [ 419] manage-vdisks.sh ├── [ 186] md0 │ ├── [1.0M] loop-efi0 │ ├── [1.0M] loop-efi1 │ ├── [100M] loop-winefi │ ├── [795M] loop-winrec │ ├── [ 16M] loop-winres │ ├── [ 955] start-md0.sh │ ├── [ 199] stop-md0.sh │ └── [ 55] .win10-loop-devices ├── [ 120] md1 │ ├── [1.0M] loop-efi0 │ ├── [1.0M] loop-efi1 │ ├── [ 696] start-md1.sh │ ├── [ 199] stop-md1.sh │ └── [ 23] .win10-loop-devices ├── [ 10] prepare │ └── [ 50] begin │ ├── [ 22] 00-manage-vdisks.sh -> ../../manage-vdisks.sh │ ├── [ 9] md0 -> ../../md0 [recursive, not followed] │ └── [ 9] md1 -> ../../md1 [recursive, not followed] └── [ 6] release └── [ 50] end ├── [ 22] 00-manage-vdisks.sh -> ../../manage-vdisks.sh ├── [ 9] md0 -> ../../md0 [recursive, not followed] └── [ 9] md1 -> ../../md1 [recursive, not followed] 11 directories, 16 files
完成后,就可以启动关闭虚拟机进行测试。
1 2 3 4 5 6 7 virsh start windows10 ls /dev/md*virsh shutdown windows10 ls /dev/md*