这周实验课刚好推进到第九章,主题是openEuler下的内核虚拟化。很多人听到“虚拟化”三个字总觉得是运维岗才会碰的东西,但操作系统理论课把这个题目放进来,其实是想让你把前面学的特权级、系统调用、中断、进程调度全部串起来——KVM就是那个能让你直观看到CPU时间片怎么被“出租”、内存怎么被“隔离”、设备怎么被“虚拟”的绝佳案例。这篇文章是我按实际带实验的顺序整理出来的,从原理拆解、环境搭建,到创建虚拟机、分配资源、排查报错,全部是能直接照着操作的内容。不管是还没做这个实验的同学,还是想自己搭一套KVM环境玩玩的开发者,都可以参照这份流程走一遍。
1. 实验目标与内核虚拟化入门
1.1 这个实验到底在做什么
先搞清楚这门课为什么要把虚拟化放在操作系统实验里。第九章的理论部分重点讲了两件事:一是虚拟机监视器如何通过CPU的虚拟化扩展(Intel的VT-x、AMD的SVM)实现特权级隔离;二是虚拟机本质上仍然是进程,只不过是被内核特殊“包裹”过的进程。实验部分要求你在openEuler系统上把这套机制亲手用起来。
落到具体操作上,实验一共就三件事:安装KVM相关组件、创建一台虚拟机、用命令行完成生命周期管理。听起来不难,但实际做一遍你会发现,之前学过的“用户态/内核态切换”在这里有了新含义——QEMU进程在用户态模拟设备,KVM模块在内核态处理CPU敏感指令,客户机的内核跑在所谓的Guest Mode里。理论课上用一张图讲清楚的内容,实验课上你得用一条条命令、一个个报错来重新理解。
我给这个实验定的验收标准是:不看笔记的情况下,能用virt-install参数说出每个选项对应虚拟化原理的哪个部分,能用virsh完成虚拟机的创建、暂停、恢复、销毁和快照回滚。能做到这两点,说明第九章的核心知识是真的进脑子了,而不只是会敲命令。
1.2 KVM、QEMU、libvirt各司其职
很多初学者第一次接触这三件套的时候会懵:一会说KVM是虚拟化,一会说QEMU是虚拟化,libvirt又来插一脚。它们其实分层很清楚。
KVM是内核模块,它的核心作用是把CPU的硬件虚拟化能力暴露给用户态。装好KVM之后,系统里会出现一个/dev/kvm设备文件,用户态程序通过ioctl系统调用跟它打交道。KVM自己只管CPU和内存的虚拟化,不管设备模拟——也就是说没有QEMU的KVM是“有房但没家具”的状态。
QEMU负责设备模拟。网卡、磁盘、串口、显卡这些硬件,都是QEMU在用户态用软件模拟出来的。你给虚拟机分配一块virtio磁盘,实际上是QEMU在宿主机的文件系统上开了一个大文件,然后把这个文件“伪装”成一块硬盘给客户机用。生活化的类比是:KVM是房东,QEMU是装修队,房东提供产权和空间,装修队负责把毛坯房变成能住人的样子。
libvirt则是统一管理接口。有了它,你不需要直接跟QEMU的命令行参数打交道,而是用virsh、virt-manager这些工具下发XML配置,libvirt帮你翻译成QEMU能懂的东西。实验中90%的操作其实都是通过libvirt完成的,只有少数场景需要直接qemu-img操作磁盘镜像。
展开说一句,明白这个分层对你的实验意义很大:报错排查时,如果问题出在CPU虚拟化,那是KVM层的事;如果出在设备或者镜像,多半是QEMU层的事;如果配置下发不对,那就要检查libvirt的XML。
1.3 为什么用openEuler做实验载体
选openEuler作为实验平台有几个实际考量。首先是软件源里直接有完整的虚拟化组件,一条dnf命令就能装齐,不需要像某些发行版那样手动加第三方源,这对实验课来说省掉了一大半环境问题。其次是它的内核版本比较新,对KVM的支持很完善,而且官方文档里针对虚拟化有专门章节,学生遇到问题能自己查资料。
另外,openEuler的osinfo数据库支持也做得不错。创建虚拟机时virt-install有一个--os-variant参数,用来指定客户机操作系统类型,如果这个参数不认识openEuler,安装时会报错。但如果你的虚拟机操作系统选的是某个通用Linux发行版,影响也不大。我建议拿到实验机之后先跑一条osinfo-query os | grep -i open看看当前版本的osinfo数据库里有没有openEuler条目,这样可以少踩一个坑。
从学习价值上讲,openEuler的默认配置比较“朴素”,没有太多花哨的封装,KVM、QEMU、libvirt之间各层次的关系暴露得很清楚。相比之下,一些自带图形化管理面板的发行版反而容易让学生只点鼠标不思考。
2. 实验环境准备与坑位预警
2.1 硬件检查:先确认CPU愿意“配合”
虚拟化实验最怕的就是CPU压根不支持硬件虚拟化,或者支持了但BIOS里没开。这一步没做好,后面所有操作都会报同一个错:/dev/kvm不存在。
检查方法很简单,登进openEuler之后执行:
grep -Ec '(vmx|svm)' /proc/cpuinfo lscpu | grep -i 虚拟化第一行返回的数字如果大于0,说明CPU带有虚拟化扩展标志;第二行能看到具体的虚拟化类型。如果是Intel CPU,对应vmx;AMD对应svm。数字为0或者lscpu查不到虚拟化字样,大概率要去BIOS里开启Intel VT-x或者AMD-V功能。
还有一个容易忽略的点:如果是自己装的物理机,系统里开了嵌套虚拟化但没开透传,也会出现KVM初始化异常。实验课的机器一般是物理机或者已经配置好的云主机,这块问题不大,但如果你是拿家里电脑练手,一定要确认VMware或者别的虚拟机软件里勾选了“向客户机暴露虚拟化指令”这类选项。
内存方面建议至少8GB,我见过4GB内存的机器跑两台虚拟机,宿主机直接开始疯狂swap,实验卡得没法看。磁盘需要预留至少40GB,因为客户机安装完系统之后还要做快照实验,空间太小会出现“磁盘镜像虚胖”的问题,后面章节细说。
2.2 安装组件:一条dnf命令的事
确认硬件没问题后,安装虚拟化组件。openEuler下的包名和CentOS系比较接近,我用的是这组:
sudo dnf install -y qemu-kvm qemu-img libvirt virt-install virt-manager bridge-utils这里稍微解释一下每个包的作用:qemu-kvm是KVM配套的QEMU用户态程序,qemu-img是磁盘镜像管理工具,libvirt提供管理守护进程和客户端工具,virt-install是命令行创建虚拟机的工具,virt-manager是图形界面管理工具,bridge-utils是网桥工具。安装完成后,依次执行:
sudo systemctl enable --now libvirtd sudo systemctl status libvirtd ls -l /dev/kvmlibvirtd是libvirt的守护进程,所有virsh命令都要经过它。第三条命令确认/dev/kvm设备存在,这是KVM模块正常加载的标志。如果ls提示找不到文件,先手动加载模块试试:
sudo modprobe kvm_intel或者根据CPU厂商换成kvm_amd。加载还是失败的话,回到上一节检查BIOS设置。
还有一个新手必踩的权限坑:直接用普通用户执行virsh list会报“Failed to connect socket to '/var/run/libvirt/virtlogd-sock'”或者权限不足的错误。解决办法是把当前用户加入libvirt用户组,然后重新登录一次会话:
sudo usermod -aG libvirt $(whoami)2.3 网络方案:默认NAT还是手动桥接
libvirt默认会创建一个名为default的NAT网络,虚拟机的网卡通过它访问外网,宿主机充当网关做地址转换。这个方案对实验够用,但有两个缺点:第一,外部主机无法直接访问虚拟机,只能从宿主机SSH进去再跳转;第二,NAT的性能和延迟都不如桥接。
如果你只是想完成实验验收,直接下面几步确认default网络存在即可:
virsh net-list --all sudo virsh net-start default sudo virsh net-autostart default想做桥接的话,需要把宿主机物理网卡加入一个网桥。以NetworkManager为例,先创建桥接连接,再把物理网卡挂进去:
sudo nmcli connection add type bridge ifname br0 con-name br0 sudo nmcli connection add type ethernet slave-type bridge con-name br0-port1 ifname ens33 master br0这里ens33要替换成你实际的物理网卡名,用ip addr查看。配置完成后,虚拟机指定网络为bridge:br0,就能获得和宿主机同一网段的IP,外部可以直接访问。桥接配置相对折腾一点,但做通之后对理解虚拟网络很有帮助,而且后续做VNC远程连接实验时桥接会更顺手。
3. 虚拟机创建与生命周期管理实操
3.1 准备系统镜像与磁盘文件
安装虚拟机必须有一个可引导的安装介质。openEuler官方提供了ISO镜像,下载后放到固定目录,比如/home/user/isos/。如果你是离线环境,也可以用教官提前准备好的镜像仓库。
接着创建虚拟机的磁盘镜像。这里建议用qcow2格式:
sudo qemu-img create -f qcow2 /var/lib/libvirt/images/oslab-vm.qcow2 20Gqcow2格式最核心的特点是Copy-on-Write,也就是写时复制。你给虚拟机分配20G空间,宿主机上这个文件实际只有不到200K,只有客户机真正写入数据时才逐步占用宿主机磁盘。类比一下就是去饭店订了20个座位的包间,实际付钱只按来的人数算。这对实验课的场景非常友好,十台虚拟机开出来,宿主机磁盘压力也不大。
创建完镜像,顺手确认一下:
qemu-img info /var/lib/libvirt/images/oslab-vm.qcow2能看到虚拟大小20G、磁盘格式qcow2,基本信息就齐了。
3.2 virt-install 参数逐行讲透
创建虚拟机的完整命令如下,我每次实验课都会把这条命令拆开讲,因为每个参数背后都对应着一个虚拟化原理:
sudo virt-install \ --name oslab-vm \ --memory 2048 \ --vcpus 2 \ --disk path=/var/lib/libvirt/images/oslab-vm.qcow2,format=qcow2,bus=virtio \ --network network=default,model=virtio \ --os-variant openeuler-22.03 \ --graphics vnc,listen=0.0.0.0 \ --cdrom /home/user/isos/openEuler-22.03-x86_64.iso逐个解释:
- --name后面是虚拟机名字,libvirt用它作为唯一标识,后续所有virsh命令都要引用这个名字。
- --memory指定内存大小,单位是MiB,2048即2GB。注意不要给太多,宿主机内存要留出一部分给自身运行。
- --vcpus指定虚拟CPU数量。这里的vCPU对应的是“线程”而不是完整物理核心,超卖是允许的,但你得清楚这是牺牲性能换资源密度。
- --disk指定磁盘路径、格式和总线类型。bus=virtio的意思是客户机使用半虚拟化驱动访问磁盘,性能远好于模拟IDE设备。这里有个前提:客户机系统需要内置virtio驱动,openEuler的内核默认支持,所以可以直接用。
- --network指定网络类型和网卡模型。network=default表示接在上一节说的NAT网络上,model=virtio同样是为了性能。
- --os-variant告诉libvirt客户机系统类型,它会据此优化CPU型号、设备配置等。版本号要看osinfo数据库支持情况,拿不准就先osinfo-query os查。
- --graphics vnc表示通过VNC提供图形安装界面,listen=0.0.0.0允许远程连接。
- --cdrom指定安装ISO镜像。
命令执行后,virt-install会输出“Starting install...”然后进入等待状态。此时可以用virt-manager打开图形界面看安装进度,也可以直接VNC连接。
3.3 生命周期管理:创建之后才是真正开始时
安装完系统重启进入客户机系统之后,虚拟化管理才算真正开始。所有日常操作集中在virsh一条命令上。
先熟悉查询类操作:
virsh list # 查看正在运行的虚拟机 virsh list --all # 查看所有虚拟机,包括已关闭的 virsh dominfo oslab-vm # 查看虚拟机配置信息 virsh vcpuinfo oslab-vm # 查看vCPU绑定情况然后是生命周期操作:
virsh shutdown oslab-vm # 发送ACPI关机信号,优雅关机 virsh start oslab-vm # 启动已关闭的虚拟机 virsh reboot oslab-vm # 发送重启信号 virsh destroy oslab-vm # 强制断电,相当于拔电源 virsh undefine oslab-vm # 删除虚拟机定义,但保留磁盘文件这几条命令建议反复练习到条件反射。实验考试常见的题目就是“将虚拟机oslab-vm优雅关机后重新启动”,很多人一紧张直接用destroy,这在虚拟机没有重要数据时无所谓,但真实生产环境里destroy等于直接拔电,数据丢失风险极大。我给学生定的要求是:shutdown优先,确实无响应才用destroy。
还有两个容易被忽略的操作:autostart设置开机自启,以及快照管理:
virsh autostart oslab-vm virsh snapshot-create-as oslab-vm base "系统初始状态" virsh snapshot-list oslab-vm virsh snapshot-revert oslab-vm base快照相当于给虚拟机拍了一张“可回退的照片”。我的建议是做完一次成功的系统安装之后立刻打一个快照,后续配置改坏了直接回滚,不用重新装系统。这个习惯在真实生产环境中非常值钱。
4. 资源分配与调优细节
4.1 CPU拓扑与绑定策略
虚拟机创建时给的--vcpus 2只是“数量”,实际上vCPU的调度完全由宿主机内核完成。默认情况下,宿主机内核会把vCPU线程调度到任意物理CPU核心上。这样做的好处是负载均衡,但也带来了缓存亲和性问题——vCPU在核心间来回迁移,L2缓存反复失效,性能打折。
实验里可以体验一下手动绑核。先用lscpu看一下宿主机CPU拓扑,然后用virsh vcpupin把vCPU固定到指定物理核心:
virsh vcpupin oslab-vm 0 2 virsh vcpupin oslab-vm 1 3 virsh emulatorpin oslab-vm 0-3第一行把vCPU 0绑到物理核心2,第二行把vCPU 1绑到物理核心3,第三行把QEMU进程整体绑到0-3号核心。这样vCPU线程和QEMU模拟线程都不会跑到核心4以上的区域去,其他虚拟机再有负载也不容易争抢同一片CPU缓存。
还有一点需要注意:如果你所在的环境是云主机或者别人共享的机器,绑定前一定确认那些物理核心没有被其他虚拟机占用,否则两个vCPU绑到同一核心,性能反而更差。实操中可以用top看到进程PID,再用taskset -p检查一下亲和性掩码,判断绑定是否生效。
4.2 内存大页配置
KVM虚拟化里内存性能的关键是TLB命中率。默认4K内存页下,2GB客户机内存需要51万多个页表项,TLB根本装不下,几乎每次内存访问都要页表遍历。解决方案是使用2MB甚至1GB的大页。
实验机上配置大页比较简单。临时生效可以这样:
echo 1024 > /proc/sys/vm/nr_hugepages这表示预留1024个2MB大页,总共2GB。想让虚拟机使用大页,需要修改域XML,在memoryBacking节点加hugepages配置:
virsh edit oslab-vm在 区域添加:
<memoryBacking> <hugepages/> </memoryBacking>保存后重启虚拟机,通过以下命令确认生效:
grep HugePages_Total /proc/meminfo如果看到HugePages_Total有数值变化,并且虚拟机的RSS不再计入普通内存,说明大页启用成功。这个实验点做完,再去跑一个内存密集型测试,对比一下延时会非常有体感。
4.3 磁盘性能与快照策略
磁盘部分最容易踩的坑是格式选择。raw格式性能比qcow2好,但不支持快照和精简置备;qcow2功能全,性能有少量损耗。实验和日常使用,我推荐qcow2,理由很简单:快照能力太重要了,性能差异在普通机械盘上根本感知不到,在NVMe盘上差距也不大。
qemu-img的几个常用操作建议熟练掌握:
qemu-img info /var/lib/libvirt/images/oslab-vm.qcow2 qemu-img resize /var/lib/libvirt/images/oslab-vm.qcow2 30G qemu-img convert -f qcow2 -O raw oslab-vm.qcow2 oslab-vm.rawresize只能扩大,不能缩小,而且扩大之后客户机里还要用分区工具扩展文件系统。convert可以把镜像格式互转,实验里可以用来体会raw和qcow2的体积差异——你会发现同样内容的系统,raw镜像文件比qcow2大不少。
另外强调一点:虚拟机开启状态下不要直接用qemu-img命令操作磁盘文件,必须先把虚拟机shutdown,否则镜像很容易损坏。我见过不止一个学生图省事,虚拟机运行时直接convert镜像,结果整个系统盘报废。
4.4 virtio半虚拟化驱动的价值
实验里有一个对比环节值得认真做:分别用模拟设备和virtio设备创建虚拟机,然后对比磁盘读写速度和网络吞吐。
virtio模型的关键在于,客户机知道自己是虚拟机,所以主动配合宿主机的高效I/O通道,而不是老老实实地模拟传统硬件。以网卡为例,e1000模拟网卡需要QEMU逐条处理硬件中断和寄存器操作,而virtio网卡通过共享内存环形队列批量传输数据。用一个简单对比表格说明:
| 对比维度 | 传统模拟设备 | virtio半虚拟化 |
|---|---|---|
| 磁盘性能 | 差,CPU占用高 | 好,接近物理设备 |
| 网络吞吐 | 低,延迟高 | 高,延迟低 |
| 驱动要求 | 无需特殊驱动 | 客户机需内置驱动 |
| CPU开销 | 高 | 低 |
实验时创建两台虚拟机,一台用bus=sata,一台用bus=virtio,然后在客户机里跑dd测试写磁盘。我第一次跑这个对比时,virtio的写入速度大概是模拟设备的4到5倍,这个数据足够说明问题。
5. 高频报错与排查手记
5.1 常见问题速查表
以下是我在这个实验上收集到的最高频报错,按出现概率排序:
| 报错现象 | 可能原因 | 排查命令/解法 |
|---|---|---|
| 无法打开/dev/kvm | CPU虚拟化未开启或KVM模块未加载 | grep -c vmx /proc/cpuinfo;modprobe kvm_intel |
| ERROR Failed to connect socket to '/var/run/libvirt/libvirt-sock' | libvirtd未启动 | systemctl start libvirtd |
| error: Failed to connect socket to '/var/run/libvirt/virtlogd-sock' | 当前用户不在libvirt组 | usermod -aG libvirt 用户名;重新登录 |
| unsupported configuration: CPU mode 'custom' is not supported | --os-variant类型不匹配 | osinfo-query os查看可用类型 |
| 客户机安装界面黑屏 | --graphics参数配置有问题或VNC端口被占用 | 换VNCS端口,检查监听地址 |
| 虚拟机网络不通 | default网络未启动或网卡模型不匹配 | virsh net-info default |
| 虚拟机启动极慢,CPU 100% | 宿主机内存不足或超卖严重 | free -h检查宿主机内存 |
| 磁盘镜像体积异常膨胀 | qcow2碎片化或频繁快照 | qemu-img info查看实际占用 |
5.2 三个值得细说的经典故障
第一个是libvirtd连不上。有一批实验机器每次重启后libvirtd都没有自动启动,查了很久发现不是服务配置问题,而是机器内存紧张导致libvirtd被OOM杀掉,systemd虽然设置了enable,但进程一直起不来。解决办法是调高libvirtd的内存限制,或者给宿主机加内存。排查命令是journalctl -u libvirtd,日志里会直接写出Out of memory。
第二个是客户机系统装完了没有网卡。openEuler客户机默认可能没有启用NetworkManager服务,导致即使virtio网卡被识别,也没有拿到IP。解决办法是进入客户机后先检查ip addr,如果网络接口状态是DOWN,就用nmcli或者systemctl restart NetworkManager重新拉起网络服务。这个坑跟KVM本身无关,但会让没有Linux网络基础的同学卡很久。
第三个是快照回滚失败。virsh snapshot-revert提示“cannot restore to snapshot as it is not the current snapshot”,视频教程里很少讲清楚。这是因为Libvirt的快照回滚默认要求目标快照是当前叶子节点,如果有更新的快照存在,你需要加--force参数强制回滚。但要注意,--force回滚会放弃目标快照之后的所有快照链,操作前先把快照列表导出一份备份:
virsh snapshot-list oslab-vm --tree5.3 排查思路分享
给刚接触虚拟化的同学一个通用的排查路径:先看宿主机,再看libvirt,最后进客户机。宿主机层用dmesg | grep -i kvm看内核日志,确认KVM模块有没有异常;libvirt层用journalctl -u libvirtd看守护进程日志,或者/var/log/libvirt/qemu目录下每个虚拟机有自己的日志文件;客户机层就是进去之后用systemctl status排查各个服务。
按照这个顺序查,90%的问题都能定位。不要一上来就怀疑虚拟化本身,多数报错其实出在环境配置和服务状态上。
6. 实验之外:嵌套虚拟化与扩展思考
6.1 顺手试试嵌套虚拟化
嵌套虚拟化是这个实验最有趣的扩展点,意思是“虚拟机里面再开虚拟机”。openEuler的KVM默认支持嵌套,但需要模块参数配合:
sudo modprobe kvm_intel nested=1确认是否生效:
cat /sys/module/kvm_intel/parameters/nested输出Y说明支持嵌套。然后你在客户机系统里就能按照同样的流程再装一套KVM环境。这个操作在云原生领域有现实意义——很多CI流水线就是这么跑的,宿主机上的隔离容器里再起一套完整虚拟化环境。
我做实验的时候给学生演示过这个,大家第一次看到虚拟机里有虚拟机,都把“虚拟化是假的”这个疑问解决了:嵌套环境里的虚拟机性能确实下降明显,但指令集和系统调用路径依然是完整的。这种层层包裹的结构,恰恰体现了操作系统抽象和复用的精髓。
6.2 KVM与云计算的关系
第九章实验做完,值得想一个更大的问题:云计算平台上的虚拟机,底层绝大多数都是KVM。一台物理机上跑几十台云主机,靠的是什么?靠的就是你在实验里用到的这些机制:CPU超卖、内存大页、qcow2精简置备、virtio高性能通道、快照迁移。实验课让你手动操作的每一件事,在云端都是自动化平台在调度。
理解了这层关系,再看当前主流云平台,思路会通很多。虚拟化管理面的大部分工作,无非是给KVM这层能力包一层更友好的API和调度策略。你在实验里学会了virsh,等于掌握了这棵技术树的根部。
6.3 给后来者的私房建议
最后说点实验课之外的经验。第一,虚拟机命名别乱起,学号拼音也好、实验编号也好,整齐划一的命名能让你在virsh list的时候一眼找到目标。第二,每完成一个阶段立刻打快照,不要攒到最后,不然出错只能重装。第三,所有修改域配置的操作,先备份XML文件:
virsh dumpxml oslab-vm > oslab-vm.xml.bak这个习惯成本极低,但能在你把配置改坏之后救你一命。
我自己当初做这个实验的时候,最深的体会是:虚拟化不是神秘魔法,它就是操作系统内核能力的一种组合运用。你理解了进程、理解了特权级、理解了中断,再看KVM的每一处设计都顺理成章。做完第九章的实验,建议回头翻翻理论课讲系统调用那一章,你会发现自己原来已经摸到了相对完整的轮廓。