1. 实验背景:为什么操作系统课程要专门讲内核虚拟化
1.1 从课程定位说起
计操理论课讲到第十讲的时候,刚好是我带学生接触内核虚拟化的节点。前面九章一直在讲进程、内存、文件系统这些传统操作系统核心模块,学生普遍会有一种感觉:操作系统就是管资源的中枢,跟“虚拟化”这个词好像关联不大。但真正走到第十章,看到 openEuler 实验里第九章的内核虚拟化内容,很多人才意识到前面那些基础其实是虚拟化技术的根基。
这门课的定位不是纯理论,也不是纯工具操作。它要求学生先理解操作系统对 CPU、内存、I/O 的抽象方式,然后在 openEuler 上亲手验证这些抽象能不能被“再次抽象”。简单说,虚拟化的本质就是在原有的操作系统之上再构建一层资源切分和隔离机制,让多个客户机系统共享同一套物理资源。openEuler 实验选这一章作为内核部分的收尾,用意非常明显:前面学的调度、内存管理、中断处理,到了虚拟化场景里全都会被重新激活。
我自己的感受是,这一讲如果只停留在“哦,KVM 是内核模块,装上就能用”的层面,就太可惜了。实验手册的编排顺序其实暗含了一条线索:先看 CPU 虚拟化怎么通过内核态和用户态的配合实现,再看内存虚拟化怎么通过页表机制落地,最后落到 I/O 虚拟化时,virtio 这类半虚拟化方案为什么能成为主流。这条线索在 openEuler 实验第九章里都有对应步骤,关键是做实验的人能不能把这些步骤串成体系。
1.2 内核虚拟化在现代操作系统中的实际地位
把视野拉宽一点看,内核虚拟化已经不是服务器领域的专有名词了。云厂商的弹性计算、容器平台的隔离底座、嵌入式场景里的安全域划分,底层基本都要跟 KVM、内核命名空间、cgroup 这些机制打交道。拿 openEuler 来说,它自己就深度集成了虚拟化相关组件,官方文档里对 KVM、QEMU 的适配做了大量工作。
从操作系统原理的角度理解,内核虚拟化最核心的价值在于提供了一种“受控的共享”。物理资源只有一份,但通过内核的虚拟化模块,可以把 CPU 时间片、内存页、设备中断按照策略分配给多个客户系统,同时保证它们互相干扰足够小。这种思路跟进程管理的核心诉求非常像,只是多了一层“整个系统都被虚拟化”的维度。
实验里要接触的东西,比如 /dev/kvm 设备节点、内核模块加载、CPU 的 VT-x 或者 AMD-V 特性开关,其实就是在操作系统层面打开虚拟化能力的开关。理解了这层关系,后面无论是做容器编排还是做虚拟化平台二次开发,都不会觉得陌生。
2. 实验环境与前置准备
2.1 openEuler环境搭建要点
openEuler 实验第九章要求的环境并不复杂,但有几个细节容易在实验课上卡住。
首先是宿主机系统的选择。实验手册建议在 openEuler 上直接操作,但我带学生时发现,很多人本机装的是其他发行版,然后又开一台 openEuler 虚拟机来做实验。这里有一个容易忽略的坑:如果 openEuler 本身就是虚拟机,而宿主机没有开启嵌套虚拟化,那 openEuler 里再创建虚拟机时,KVM 根本起不来。这个坑在问题排查部分我会详细展开,前置准备阶段只需要记住一句话:要实验内核虚拟化,最好在物理机上跑 openEuler,或者在宿主机 BIOS 和虚拟机管理器里把嵌套虚拟化打开。
其次是内核版本和 CPU 虚拟化支持的检查。实验开始前,至少要在终端里跑两个命令确认基础条件:
grep -E "vmx|svm" /proc/cpuinfolsmod | grep kvmvmx 代表 Intel 的虚拟化扩展,svm 代表 AMD 的虚拟化扩展,如果这两个标志一个都没有,说明当前环境不支持硬件辅助虚拟化,后面的实验基本没法正常进行。lsmod 的结果里如果看不到 kvm_intel 或 kvm_amd,说明 KVM 模块还没加载,也需要先处理。
第三个要点是实验用的磁盘空间和镜像管理。内核虚拟化实验会创建客户机镜像文件,还要安装客户机操作系统,默认路径如果放在系统盘的小分区里,很容易把空间挤爆。我一般建议单独挂一个数据目录,比如 /data/vm 或者 /opt/vms,给足 20GB 以上的空闲空间,再做实验时就不会因为磁盘写满导致镜像损坏。
2.2 实验前必须确认的内核特性
除了 CPU 标志和 KVM 模块,还有一些内核配置项会直接影响实验。openEuler 默认内核通常已经开启了虚拟化相关支持,但保险起见,可以查看当前内核的 config 里几个关键选项:
zcat /proc/config.gz | grep VIRTUALIZATION重点关注 CONFIG_KVM、CONFIG_KVM_INTEL、CONFIG_KVM_AMD 这三个选项。如果它们不是 y 或 m,那这个内核本身就没有编译 KVM 相关代码,后面做实验时会发现 /dev/kvm 根本不存在。
还有一点是内存大小。实验里要跑一个客户机系统,最少建议给宿主机留 4GB 以上物理内存,否则客户机分配 2GB 内存后,宿主机自己反而会频繁使用交换分区,实验响应会变得很迟钝。我以前在一个只有 2GB 内存的旧笔记本上尝试过这个实验,客户机系统安装过程慢到让人怀疑人生,后来直接切换到物理服务器上才顺畅。
系统时间同步也值得注意。创建虚拟机、制作镜像时,如果宿主机时间不对,客户机安装完成后可能出现证书校验或网络请求失败的问题,表面上看是系统配置问题,实际上来源是时间偏差。实验前用 chronyc 或者 timedatectl 把系统时间校准到正确状态,能省掉很多莫名其妙的排查过程。
3. 内核虚拟化实验的整体设计思路
3.1 实验章节的结构逻辑
openEuler 实验第九章从内容编排上看,明显遵循了“先借力、再构建、后验证”的实验教学思路。第一步是借用系统已经提供好的虚拟化工具链把客户机跑起来,让学习者先看到一个完整的虚拟化实例。第二步是逐渐拆解,看看这个虚拟化实例背后依赖了哪些内核模块、哪些设备节点、哪些 CPU 特性。第三步是回到内核态,做一些类似内核模块加载、退出、参数验证的操作,把用户态的 QEMU 和内核态的 KVM 串起来。
这种结构的好处是,它不让学习者一上来就面对晦涩的内核代码,而是先建立感性认识。我反复跟学生强调,当你用 virt-manager 或者命令行创建一台虚拟机并成功启动客户机系统时,千万不要把注意力只放在“能跑起来”上。那个瞬间应该反问自己:CPU 指令是如何被捕获并转交给 KVM 的?内存页是怎么完成客户机物理地址到宿主机物理地址转换的?设备访问为什么能维持接近原生的性能?
实验手册虽然没有要求阅读内核源码,但每一步操作对应的原理都应该在理论课上对应上。比如创建虚拟机时选择的 virtio 网卡和 virtio 磁盘,本质上是半虚拟化 I/O 的一个具体实现,它的特点是客户机知道自己运行在虚拟化环境中,通过前端驱动与宿主机的后端进程通信,绕过设备模拟的开销。这些内容在标准的操作系统教材里不一定单独成章,但在 openEuler 实验里是直接落地的。
3.2 从理论到实践的映射关系
我把实验的关键步骤和操作系统原理做了个对照,方便学生建立映射:
| 实验步骤 | 涉及的内核虚拟化概念 | 理论课对应知识点 |
|---|---|---|
| 检查 CPU 虚拟化标志 | 硬件辅助虚拟化 | CPU 特权级与指令执行模型 |
| 加载 kvm 模块 | 内核虚拟化模块机制 | 内核模块、设备驱动模型 |
| 创建客户机镜像 | 虚拟磁盘抽象 | 文件系统与块设备抽象 |
| 配置虚拟 CPU 数量 | 虚拟 CPU 调度 | 进程调度与时间片分配 |
| 配置客户机内存 | 内存虚拟化与影子页表 | 内存管理、页表机制 |
| 选择 virtio 设备 | 半虚拟化 I/O | 中断处理与 DMA 机制 |
做完这个映射之后,再看实验任务就不会觉得劳动密集。比如配置客户机内存大小,看起来只是填一个数字,但这个数字背后要考虑宿主机的内存水位、内存超分配合适性、客户机负载特性等因素。理论课里讲到的“虚拟地址到物理地址转换”,到虚拟化场景就升级成“客户机虚拟地址、客户机物理地址、宿主机物理地址”的三层转换,实验里通过修改内存大小和观察 qemu-kvm 进程的内存占用就能直观感受到这层关系。
3.3 工具链选择的考量
openEuler 实验里主要涉及 QEMU、KVM、libvirt 这一套组合。很多学生问为什么不直接用 VirtualBox 或者 VMware,这个问题本身反映了对“内核虚拟化”理解的偏差。VirtualBox 和 VMware 是完整的虚拟化产品,封装了太多细节,而 QEMU 加 KVM 的组合更接近操作系统内核层面的原貌。
KVM 是 Linux 内核自带的虚拟化模块,它只负责把 CPU 的虚拟化扩展能力暴露给用户态,同时提供内存虚拟化的部分支持。QEMU 则承担了设备模拟和客户机管理的职责。两者分工明确,刚好能让学生区分“哪些能力来自内核态”“哪些能力来自用户态”。
libvirt 属于更上层的管理工具,它不是必须的,但实验里用它能简化虚拟机生命周期管理。我的建议是实验前期尽量手动敲 qemu-system-x86_64 命令行,哪怕只是复制手册里的命令,也要留意每个参数的含义。直接用 libvirt 虽然快,但会把 virtio、vnc、内存大小这些关键字段藏在 XML 配置里,反而不利于理解。
4. 实操全过程:一步一步跑通内核虚拟化实验
4.1 确认宿主机虚拟化底座的完整步骤
我在每次实验课上都会带着学生先做一遍“虚拟化底座自检”,这部分虽然简单,但非常值得养成习惯。
第一步,检查 CPU 是否支持虚拟化扩展。在 openEuler 终端执行:
egrep -c '(vmx|svm)' /proc/cpuinfo输出结果如果大于 0,说明当前 CPU 支持。如果输出 0,先别急着装虚拟化软件,回 BIOS 设置里找 Intel Virtualization Technology 或 SVM Mode 的开关,打开后重启再试。
第二步,检查 /dev/kvm 设备节点是否存在:
ls -l /dev/kvm正常情况下会看到一个字符设备文件。如果不存在,要么是 KVM 模块没加载,要么是内核编译时没启用 KVM。手动尝试加载模块:
modprobe kvm_intel加载成功后再次检查 /dev/kvm。如果还是看不到,那就需要检查内核配置,或者升级内核。
第三步,安装必要的虚拟化用户态工具。openEuler 下可以用 dnf 安装:
dnf install -y qemu-kvm libvirt virt-install安装完成后启动 libvirtd 服务并设为开机自启:
systemctl enable --now libvirtd这里我遇到过一种比较隐蔽的情况:libvirtd 服务显示 running,但 virt-install 创建虚拟机时仍然报“无法连接 hypervisor”。原因通常是当前用户不在 libvirt 用户组里。把用户加入 libvirt 组并重新登录就能解决:
usermod -aG libvirt $(whoami)4.2 创建虚拟磁盘镜像的细节与参数
实验里需要为客户机准备一块虚拟磁盘。常用的工具是 qemu-img,其基本命令格式为:
qemu-img create -f qcow2 /data/vms/openeuler-guest.qcow2 20G这里我强调几个容易被忽略的参数细节。
格式选择 qcow2 而不是 raw,是因为 qcow2 支持按需分配、快照和压缩,20G 的逻辑大小不会立刻占用 20G 物理空间,初始占用通常只有几百 KB。raw 格式虽然性能略好,但会一次性占用全部大小,对学生实验来说反而容易造成磁盘浪费和管理困难。
磁盘路径选择 /data/vms 而不是默认的 /var/lib/libvirt/images,主要是考虑空间独立性和备份便利性。如果系统盘空间紧张,把虚拟磁盘放到单独挂载的数据分区里,后面做快照、扩容都方便。
qemu-img create 创建的是空镜像,后续安装客户机操作系统时才真正写入数据。有些学生会跳过 create 环节,直接使用一个下载好的现成镜像文件,这样虽然快,但看不到虚拟磁盘的初始化过程。我还是建议完整走一遍创建流程,因为后面排查磁盘故障时,对镜像文件的组成方式有直觉很重要。
4.3 客户机安装过程中的关键参数选择
客户机系统安装有两种常见方式:一种是通过 virt-install 半交互式安装,另一种是直接用 qemu-system-x86_64 加 -cdrom 参数启动安装镜像。实验课上我更推荐用 virt-install,因为它把网络、磁盘、显示配置聚合成一条命令,更适合批量操作和后续追踪。
一个典型命令如下:
virt-install \ --name openeuler-guest \ --vcpus 2 \ --memory 2048 \ --disk path=/data/vms/openeuler-guest.qcow2,format=qcow2 \ --cdrom /data/iso/openEuler-22.03.iso \ --network network=default,model=virtio \ --graphics vnc,listen=0.0.0.0 \ --os-variant openeuler-unknownvcpus 参数设置虚拟 CPU 数量。不要盲目把客户机 CPU 数量设成跟宿主机相同,尤其当宿主机本身只有 4 核时,给客户机分配 2 核是比较均衡的选择。对实验负载来说,2 核通常足够。
memory 参数设置客户机内存。2048 也就是 2GB,是运行现代 Linux 客户机的最低舒适线。如果宿主机内存充足,设到 4096 会让客户机编译或跑测试时更流畅。
network network=default 表示使用 libvirt 的默认虚拟网络,也就是 NAT 网络。model=virtio 指定网卡使用 virtio 半虚拟化模型。这里值得多说一句,virtio 网卡在客户机安装时要确保系统内核包含 virtio_net 驱动,否则可能出现网卡无法识别的情况。openEuler 默认内核一般已经包含这个驱动,但如果是自定义裁剪过的内核,就必须额外留意。
graphics vnc,listen=0.0.0.0 表示开放 VNC 远程显示端口。在实际实验环境中,如果通过 SSH 连接宿主机,本地没有物理显示设备,没有这个参数就无法观察客户机安装界面。
4.4 实验中的内核模块操作与验证
完成客户机安装后,实验并没有结束。这一章的“内核虚拟化”落脚点还是在内核,所以还要回到宿主机做一组模块操作。
查看当前加载的 KVM 模块:
lsmod | grep kvm输出里出现 kvm_intel 和 kvm 两行是正常的。kvm 是公共模块,kvm_intel 是 Intel 平台特有的实现,如果是在 AMD 平台上看到 kvm_amd,也是正常情况。
接下来可以做一次模块卸载再加载的操作,直观感受内核模块生命周期:
modprobe -r kvm_intel modprobe kvm_intel在执行卸载时要注意,如果有客户机正在运行,模块可能因为被占用而无法卸载,需要在操作前先关闭客户机。这个“占用冲突”本身就是内核模块引用计数的实际演示,比单纯背概念有用得多。
实验里还可以通过以下命令查看 KVM 模块的相关参数:
modinfo kvm_intel输出会显示模块的 license、依赖、参数等信息。留意参数列表里的 nested 相关选项,这是嵌套虚拟化的开关。部分 CPU 或内核版本要求显式开启 nested 才能在同一台宿主机上的虚拟机里再创建虚拟机,这时需要:
echo 'options kvm_intel nested=1' > /etc/modprobe.d/kvm_intel.conf然后重启系统或者重新加载模块。这个知识点的应用场景就是,当你需要在虚拟机里继续做虚拟化实验时,没有它就只能看着 KVM 报错而毫无办法。
4.5 虚拟化环境的性能观测
实验手册通常还包含一组性能观测任务,用来验证虚拟化环境的运行状态。我常用的命令组合包括:
查看宿主机整体负载:
top -d 1查看 qemu-kvm 进程的 CPU 占用:
ps aux | grep qemu观察客户机内负载与宿主机进程的对应关系。理论上,客户机 CPU 密集任务会体现在宿主机对应 qemu-kvm 进程的 CPU 占用率上升,这种“从进程角度看虚拟化”的方式非常直观。
另外还可以用 virsh 查看客户机状态和资源分配:
virsh list --all virsh dominfo openeuler-guestdominfo 输出的 CPU 和内存信息,与创建时的参数保持一致。如果发现客户机里的实际内存小于分配值,优先检查宿主机可用内存是否不足,因为内存超分配时 QEMU 可能无法预留完整内存区域。
5. 实验中的常见问题与排查实录
5.1 客户机创建失败,报错提示不支持虚拟化
这是出现频率最高的问题,几乎每届学生里都有几个人遇到。报错信息里通常包含“kvm is not available”或者“unsupported configuration: Domain requires KVM but it is not available”。原因大概率是嵌套虚拟化没开启。
排查路径如下:
先确认宿主机物理 CPU 支持虚拟化,前面提到的 /proc/cpuinfo 检查能做初步判断。然后确认 /dev/kvm 存在。如果 openEuler 本身是虚拟机,还要检查宿主机的虚拟机软件是否开启了“向客户机传递 CPU 虚拟化扩展”的选项。
验证嵌套虚拟化是否开启,可以查看:
cat /sys/module/kvm_intel/parameters/nested输出 Y 表示开启,N 表示未开启。若为 N,则使用前面提到的 modprobe.d 配置文件开启 nested 参数,之后必须重新加载 kvm_intel 模块或者重启系统。
这里有一个隐蔽细节,有些系统加载 kvm_intel 时已经附带排查是否存在 BIOS 设置被锁定的情况。部分笔记本默认会禁用虚拟化扩展,即使软件层全部配置正确,仍然始终报错。我的排查习惯是先在物理机上的其他系统里做同样的检查,如果物理系统里 /proc/cpuinfo 都看不到 vmx 标志,那就直接进 BIOS。
5.2 qemu-img 创建镜像后空间不足
有学生反馈,明明创建了 20G 的 qcow2 镜像,客户机安装到一半提示磁盘空间不足。这个问题的根源通常不是镜像文件大小,而是存放镜像的目录所在分区空间不够。
qemu-img create 创建的是稀疏文件,逻辑大小 20G 但初始实际占用很小,安装过程中才会逐步增长。如果宿主机的 / data 分区本身只有 10G 可用空间,那镜像增长到 10G 就会触发空间不足。排查方法:
df -h /data/vms看到的分区可用空间才是真实约束。解决办法是换一个空间更大的目录,或者把分区调整后重新创建镜像。给学生的建议,尽量在实验准备阶段就把镜像目录放到剩余空间超过 30G 的分区上,避免中途崩溃。
5.3 客户机安装时网卡无法识别
virtio 网卡在现代内核里默认可用,但使用自定义内核镜像或最小化安装镜像时,可能缺少 virtio_net 驱动。报错现象是安装界面里看不到网卡接口。
处理方法有两种。第一种是安装时改用 e1000 模拟网卡,虽然性能略差,但兼容性更好,适合边缘排查:
--network network=default,model=e1000第二种是给客户机内核添加 virtio 驱动支持,但这需要在制作镜像时就把相关模块打进 initramfs,步骤较繁琐。实验场景下优先推荐第一种。
安装完成并登录系统后,可以把网卡模型改回 virtio 验证驱动情况。如果改为 virtio 后网络消失,说明客户机内核缺少驱动,此时再决定是补充驱动还是保持 e1000。
5.4 VNC 连接黑屏或无法显示
VNC 连接问题在实验课上也很常见。virt-install 指定 --graphics vnc,listen=0.0.0.0 后,需要找到 VNC 端口号:
virsh vncdisplay openeuler-guest输出类似 :1,对应的实际端口是 5900+1,也就是 5901。连接时如果黑屏,先确认客户机是否真的启动了,再确认 VNC 客户端连接的端口是否正确。
另一种常见情况是服务器防火墙拦截了 VNC 端口。openEuler 默认配置下,如果启用了 firewalld,需要放行端口:
firewall-cmd --add-port=5900-5910/tcp --permanent firewall-cmd --reload5.5 问题速查表
| 问题现象 | 排查命令 | 常见解决方法 |
|---|---|---|
| KVM 不可用 | ls -l /dev/kvm | 开启 BIOS 虚拟化开关,加载 kvm 模块 |
| 嵌套虚拟化无法开启 | cat /sys/module/kvm_intel/parameters/nested | 配置 modprobe.d 后重载模块 |
| 镜像空间不足 | df -h /data/vms | 更换大分区目录,重建镜像 |
| 网卡无法识别 | dmesg 查看 virtio 报错 | 改用 e1000 模型后补驱动 |
| VNC 连接失败 | virsh vncdisplay | 检查端口和防火墙规则 |
| libvirtd 无法管理客户机 | virsh list --all | 将用户加入 libvirt 组 |
6. 实操心得与扩展方向
6.1 最有收获的几个瞬间
整轮实验带下来,我认为最有价值的时刻不是在客户机成功启动时,而是在排查问题、理解报错的过程中。
第一个收获是理解 KVM 模块引用计数的实际意义。第一次执行 modprobe -r kvm_intel 时收到“Module is in use”的提示,我意识到客户机进程正持有这个模块的引用,这种冲突背后其实反映了内核模块生命周期与进程生命周期之间的耦合关系。单纯看教材和亲手触发一次报错,记忆深度完全不同。
第二个收获是 qcow2 镜像的稀疏特性。创建完镜像立刻看到目录里文件很小,但 df 显示的逻辑容量是 20G,理解了这个差异后才明白为什么虚拟化环境中“容量规划”必须同时关注逻辑大小和物理占用。
第三个收获是 virtio 半虚拟化模型的性能优势。在实验里分别在两张网卡模型间切换,跑一次网络吞吐对比后,学生对 virtio 的价值会形成非常直观的认知,这比任何 PPT 上的架构图都更有说服力。
6.2 后续可自行拓展的方向
内核虚拟化实验结束后,如果还想继续深挖,我建议几个方向。
第一个方向是嵌套虚拟化的继续实验。开启 nested 参数后,可以在客户机里再搭一套 KVM 环境,感受“虚拟化里的虚拟化”对性能的影响。很多云原生课程里提到的“裸金属容器”概念,也跟这条技术线相关。
第二个方向是内存虚拟化相关的性能调优。实验里只配置了内存大小,但实际环境中还涉及大页内存(HugePages)、NUMA 绑定、内存热插拔等机制。用 qemu-kvm 启动客户机时加入 -object memory-backend-file 参数,查看不同内存后端对客户机性能的影响,能加深对内存抽象层次的理解。
第三个方向是结合内核模块开发看虚拟化加速。比如写一个简单的内核模块,在客户机里加载,观察它跟宿主机 KVM 模块之间的互动,或者利用 perf 工具分析虚拟机退出(VM Exit)事件。虽然门槛较高,但对真正想走底层开发路线的学生来说,这个方向的价值非常大。
我在实际操作中的体会是,实验本身只是一个起点,真正的收获来自于反复折腾、阅读报错、再对照理论回看的过程。内核虚拟化这块内容,越早摆脱“照着命令敲”的状态,越能理解操作系统这门课在背后做的事。