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*