xvisor源码分析:从Type-1虚拟化到ARM/RISC-V实战 做系统软件这些年我一直觉得虚拟化是最有意思、也最容易劝退的方向。KVM动辄几十万行QEMU从用户态到内核态绕一大圈如果你刚接触源码很容易被ioctl和各种helper淹没。后来我翻到一个相对小众、但极度适合阅读的开源方案就是今天要聊的xvisor。xvisor全称eXtensible Versatile hyperVISOR是一个GPLv2协议下的Type-1型hypervisor直接运行在硬件上不依赖Linux内核支持ARMv7-A、ARMv8-A、RISC-V和x86等架构。我花了大半个月做源码梳理也反复在QEMU上跑过ARM guest。这篇文章就围绕xvisor源码分析展开讲清楚它的架构、启动流程、CPU虚拟化、内存虚拟化、中断虚拟化以及从源码到编译、运行、调试的完整思路。无论你是刚开始接触虚拟化还是已经看过Linux内核里KVM那套实现xvisor都值得认真读一遍。它代码量不算大结构清晰没有复杂的历史包袱适合用来建立对Hypervisor整体的模型认知。下面进入正题。1. 从零认识xvisor为什么说它是嵌入式虚拟化的一个“样板间”1.1 它和KVM、QEMU到底差在哪Type-1与Type-2的本质区别聊xvisor之前先把虚拟化的几个概念掰扯清楚。KVM是Linux内核里的一个模块运行时需要先启动Linux再利用内核的调度、内存管理、设备驱动能力去创建虚拟机。这种模型叫Type-2也就是宿主型虚拟化。QEMU负责用户态的设备模拟和CPU模拟在大部分场景下和KVM配合使用。也就是说KVM不是完整的hypervisor它更像“帮Linux内核获得虚拟化能力的那部分”真正的软硬件协调逻辑要围绕内核来做。xvisor则是纯正的Type-1裸机型hypervisor。它开机直接跑在硬件上不依赖任何操作系统所有虚拟机都由它直接管理。换成通俗的话KVM像你请了一个秘书帮老板整理日程xvisor就是老板自己直接拍板什么事都亲自管。Type-1的好处是代码路径更短、开销更低、隔离性更强但代价是所有活都得自己做——中断控制器、定时器、调度器、内存管理、设备模拟一个都不能少。这么说起来好像很复杂但恰恰是xvisor“小而全”的特点让它成了绝佳的源码学习对象。KVM的代码分散在arch/x86/kvm、virt/kvm、include/uapi/linux/kvm.h等一大堆地方你很难在短时间里从零拼出完整链路。xvisor是单体式结构核心代码就在core目录和arch目录里模块边界非常清楚。1.2 它适合干什么不只是服务端虚拟化更是嵌入式混合关键系统的底座xvisor主要面向嵌入式场景。ARM处理器上跑一个Linux当功能丰富的宿主再跑一个RTOS满足实时性要求或者两个Linux guest互相隔离其中一个崩溃了不影响另一个。这种需求在汽车域控制器、工业网关、物联网安全产品里越来越常见。它配置偏静态化虚拟机列表、资源划分、内存区域都由设备树Device TreeFDT描述启动时一次性解析完毕。这看起来不如KVM灵活但对嵌入式产品来说反而是优点启动过程确定、资源边界固定、出了问题更容易复现。所以这篇文章适合三类读者一是刚接触虚拟化、想在源码层面建立全局认知的学生二是做嵌入式BSP、固件、安全启动相关工作的开发者想评估在现有ARM平台上加一层hypervisor的代价三是已经在用KVM、Xen但想对比学习“无OS裸机型”虚拟化实现的人。如果你只是日常用VMware Workstation跑个Windows那xvisor可能离你比较远但如果你好奇“虚拟机管理器到底在干什么”它比任何闭源商业方案都透明。2. 源码整体架构拆解拿到Hypervisor源码先看哪儿2.1 打开仓库先别急着看代码目录结构就是一张“地图”xvisor源码可以从GitHub上的xvisor/xvisor获取。以我梳理过的版本为例仓库顶层主要分这几块每个目录承担完全不同的责任。include公共头文件包括list、fdt、memory、vmm相关的数据结构和接口声明。lib基础库相当于“精简版/裸机版libc”提供字符串、链表、哈希表、DTB解析等工具。Hypervisor没法直接用glibc所以这里的实现都是自己维护的。core整个hypervisor的核心逻辑包括vmm_init、vmm_host、vmm_guest、vmm_vm、vmm_vcpu、vmm_scheduler等关键模块对应“虚拟机生命周期管理”。arch架构相关代码。以ARM为例下面有arm/arm32、arm/arm64再往下拆分cpu、mmu、gic等文件全部是直接和硬件寄存器打交道的部分。drivers各种硬件驱动串口、时钟、中断控制器、VirtIO设备透明后端的实现。emulator设备模拟器主要给guest提供虚拟设备比如virtio-blk、virtio-net。tests自测试和示例配置。scripts构建辅助脚本生成DTS、打镜像、编排文件系统等辅助工作。刚上手的人最容易犯的错误就是一头扎进arch目录跟汇编代码死磕。我的建议是反过来先看core再回头看arch。因为arch里的代码大多是“怎么把上下文保存到内存”、“怎么切换页表”、“怎么配置GIC”这类具体动作而这些动作之所以存在是因为core里某个流程需要它们。先知道为什么再知道怎么做源码阅读效率会高很多。2.2 构建系统与配置参考defconfig和.configxvisor的构建风格和Linux内核很像顶层Makefile负责编译你需要在编译前指定架构和交叉工具链。最基础的命令是这样git clone https://github.com/xvisor/xvisor.git cd xvisor export CROSS_COMPILEaarch64-linux-gnu- export ARCHarm64 make defconfig make -j8编译产物一般会生成xvisor、xvisor.bin以及相关的dtb和guest镜像。第一次拿到源码建议先用默认配置编译一遍别急着改功能。这样做的目的是把环境问题先排掉确保你有一台“能跑的基准”。xvisor的配置里有很多开关比如是否开启VirtIO、是否支持某个平台、调试日志级别等。这些配置都在defconfig文件中体现熟悉Linux内核menuconfig的人会感到很亲切。需要强调一点xvisor是一个裸机程序它不链接标准C库所以它的很多“偏方”比如自己维护的printf、自己实现的page allocator你都要从“裸环境”的角度去理解。2.3 三条主线CPU虚拟化、内存虚拟化、中断虚拟化虚拟化这个主题拆开来看核心就是三条线CPU虚拟化、内存虚拟化、设备与中断虚拟化。xvisor的源码也是按这个逻辑组织起来的。CPU虚拟化通过vCPU结构描述“一个虚拟CPU的上下文”包括寄存器组、运行状态、当前guest所属的虚拟机等。核心文件在core/vmm_vcpu.c和相关arch实现里。内存虚拟化核心对象是vmm_region表示guest的一段物理内存区域。xvisor通过向guest展示“guest物理地址空间”再靠硬件MMU的第二阶段翻译把它映射到真实物理地址。中断虚拟化包括中断控制器直通与虚拟中断注入。ARM上主要是GIC相关代码RISC-V上是PLIC或APLIC相关代码。这部分的难点在于一个中断到底该通知哪个guest以及guest退出后hypervisor如何决定下一步执行什么。我把这三条主线作为后面几章的分析重点。如果你自己看源码建议记住一个词trap。所有虚拟化的核心交互本质上都是guest在执行某条特权指令或访问某块敏感资源时触发异常然后陷入到hypervisorhypervisor处理后返回到guest。理解了trap路径就等于抓住了整个虚拟化的“经线”。3. 核心机制解析与源码导读把trap路径当成主线3.1 启动流程从冷启动到guest运行起来以ARM64为例xvisor启动顺序大致是CPU从EL3或EL2入口进入先执行arch下的启动汇编初始化基本的异常向量表、栈指针、MMU和页表然后进入C语言阶段的vmm_init。vmm_init是整个系统的入口它会依次做这些事初始化host资源内存分配器、页表、CPU→ 解析设备树vmm_devtree_parse→ 创建设备树中定义的虚拟机实例vmm_guest_init→ 初始化调度器和定时器vmm_scheduler_init→ 最后进入调度循环让第一个可运行的guest vCPU跑起来。这个过程可以在QEMU里实际验证。我给读者的建议是把断点设置在vmm_guest_init上因为这里会读取设备树里的每个guest节点并给它们创建对应的VM和vCPU。断点一停你可以看到设备树被解析到什么程度、每个guest分配了哪些内存、串口和VirtIO设备有没有被正确注册。源码导读到这里我已经把“骨架”拆出来了。很多人看源码会纠结某个数据结构的字段这其实是次要的。先跟着启动流程走一遍知道什么时候创建VM、什么时候创建vCPU、什么时候开始调度后面那些细枝末节自然就串起来了。3.2 内存虚拟化GPA到SPA的两级映射内存虚拟化是所有虚拟化实现里最绕、也最见功底的地方。先明确概念真实物理地址叫SPASystem Physical Addressguest眼里看到的地址叫GPAGuest Physical Address。guest里的Linux内核会再做一层虚拟地址映射所以一个guest应用要真正访问内存至少要经历“guest虚拟地址→guest物理地址→系统物理地址”两级翻译。xvisor靠硬件MMU的第二阶段翻译来实现GPA到SPA的映射。在ARM上叫Stage-2 MMU在RISC-V里对应H-extension的stage2地址翻译。xvisor启动时会为每个guest建立一个独立的Stage-2页表并把它配置到虚拟化硬件寄存器中。guest访问任意外设或者物理内存都要经过这层页表的校验与映射。我在源码分析时重点关注了vmm_region这个结构。它描述了第几个内存区域、起始GPA、物理映射、大小、权限等字段。设备树里每个guest的memory节点最后都会变成一连串vmm_region。如果guest想访问的GPA没有对应regionMMU翻译不过去就会产生Stage-2 fault然后陷入到hypervisor交由异常处理函数决定是报错退出还是动态映射。这里有一个典型的坑guest的内存区域必须按页对齐最好是2MB或1GB对齐因为Stage-2页表有可能用大页block entry做映射。如果guest内存起始地址没有按2MB对齐硬件无法使用块描述符就只能退化成4KB页页表项数量猛增TLB命中率也会变差。我在调试时遇到过guest启动极慢的现象后来发现就是内存起始地址没对齐导致每次访问都触发Stage-2 fault。3.3 CPU虚拟化与上下文切换vCPU就是状态机从源码角度看vCPU本质上就是一个结构体里面保存了通用寄存器、系统寄存器、程序状态寄存器、异常返回地址、运行状态等。ARM64上guest在EL1运行当guest执行hvc指令、或访问需要trap的寄存器、或发生中断硬件会切换到EL2跳转到xvisor注册的异常向量表。这时xvisor需要把当前vCPU的上下文保存到内存里然后根据异常原因决定下一步做什么。如果是定时器中断就更新定时器状态如果是VirtIO通知就处理设备请求如果guest执行了WFI等待中断调度器可能直接把这个vCPU标记为阻塞把物理CPU让给另一个guest的vCPU运行。最核心的寄存器有几个ELR_EL2保存guest被trap时的PCSPSR_EL2保存guest的程序状态vbar_el2指向hypervisor异常向量表。上下文切换的代码在arch里通常是一个汇编宏负责把x0到x30以及spsr、elr批量压栈和恢复。源码阅读建议先别一行行读汇编把这个宏当成“黑盒”理解调用点在哪里它保存了哪些东西恢复顺序是什么就够了。xvisor的调度器是“协作式可抢占式”混着的。每个guest都有一个优先级和调度策略vCPU会被放到运行队列、等待队列。如果你在多核平台上跑两个guest会看到Linux的smp相关打印那说明每个guest的多个vCPU正在被xvisor分配到不同的物理CPU上运行。3.4 中断与设备虚拟化GIC直通和VirtIO模型硬件中断虚拟化是嵌入式hypervisor里最看基本功的部分。ARM平台上的GIC提供了虚拟化扩展可以让某些中断直接路由给guest而不需要每次让hypervisor插手。xvisor会为每个guest配置中断亲和性和中断号映射使guest的驱动能够直接操作GIC的CPU接口来enable/disable中断。不过真正让guest能用起来的还是设备虚拟化。xvisor不像QEMU那样全套模拟PCI设备和大量外设它走的是轻量级VirtIO路线。VirtIO是一种半虚拟化标准guest里装一个前端驱动hypervisor这边提供后端处理队列两者共享一块内存区域通过MMIO或PCI通知机制交互。以virtio-blk为例guest里的Linux驱动把块设备请求写入virtqueue然后通过写一个MMIO寄存器通知hypervisor。这个MMIO地址在guest看来就是一块普通外设地址空间但实际上访问时会trap到xvisor的异常处理程序xvisor再去调用后端驱动完成实际操作。设备模型的好处是大幅减少了设备模拟开销坏处是需要guest支持特定驱动Linux内核默认就带virtio驱动所以实际用起来很方便。源码里可以重点关注emulator和drivers/virtio两个目录。学习路径建议先看virtio-mmio传输层再往上读blk或net后端最后再回来看GIC。只要把“数据通过共享内存传递、事件通过MMIO触发”这层理解透xvisor的设备模型就算摸到了。4. 实操从源码到在QEMU上跑起一个完整guest4.1 交叉编译环境准备一套能跑的基准源码分析不能光看不练最好把xvisor跑起来。没有真实开发板也没关系用QEMU模拟ARM平台完全够用。建议准备一个aarch64交叉编译环境。Ubuntu/Debian可以安装gcc-aarch64-linux-gnu和对应的交叉工具链sudo apt install gcc-aarch64-linux-gnu sudo apt install device-tree-compiler然后编译xvisor。注意一定要指定ARCH和CROSS_COMPILE同时建议开启额外的调试日志。启动阶段如果想看详细过程可以在make的时候加上配置文件相关选项。不同版本的构建选项略有差异但侧重点一致先保证交叉工具链正确再保证defconfig正确。如果编译过程中出现找不到crt0或者找不到main多半是你用了普通gcc而不是裸机工具链。xvisor是独立二进制不依赖libc对工具链的“裸机属性”有要求。4.2 生成DTS与引导镜像把guest“钉”在资源上xvisor把整个系统的静态配置放在设备树里。你需要让xvisor知道物理内存多大串口在哪哪个guest用哪段内存内核镜像放在哪里初始化参数是什么。以我常用的方式为例给xvisor用的主设备树里会包含这样一个guest节点guest { compatible xvisor,guest; device_type guest; #address-cells 2; #size-cells 2; cpu_num 2; memory { device_type memory; guest-phys-addr 0x0 0x80000000; host-phys-addr 0x0 0x80000000; guest-phys-size 0x0 0x20000000; }; vdevices { virtio0 { compatible virtio,mmio; guest-phys-addr 0x0 0x01000000; interrupt 33; }; }; };这段配置的意思是给这个guest分配一段从0x80000000开始的512MB物理内存并在它的GPA 0x01000000处放一个VirtIO-MMIO设备。guest内核启动参数root、console等放在chosen节点里。这样guest启动后看到的是一个“拥有自己内存和外设的机器”。这个过程手动做很繁琐好在xvisor源码里带了生成guest镜像和DTS的脚本。实际编译时你可以在Makefile参数里指定guest kernel镜像路径构建系统会自动把镜像打包进一个initramfs式的image里并在生成DTS时填好内存和加载地址。4.3 QEMU启动、日志与实时调试编译好之后用QEMU跑起来大概是这个样子qemu-system-aarch64 \ -machine virt \ -cpu cortex-a57 \ -m 512M \ -smp 2 \ -nographic \ -kernel xvisor.bin如果你的构建流程生成了完整的guest image那么启动日志里会先出现xvisor的版本信息和启动信息然后进入调度guest的Linux内核开始打印启动日志。看到两套启动日志并存意味着xvisor已经成功把一个Linux虚拟机拉起来了。平时我调试时会加几个参数-s是开启GDB server-d int是记录中断异常事件-D log.txt可以保存QEMU日志。xvisor自身也支持日志级别调整把日志打到perf甚至debug档能看到大量设备树解析和设备初始化输出。配合GDB断点我可以在vmm_guest_init里观察guest VM的数据结构也可以在异常向量表入口处看trap原因。这一步是我认为整个xvisor学习路线中最重要的一环。因为虚拟化的核心行为就是“guest执行指令→触发trap→hypervisor处理→返回guest”你边看QEMU日志边断在异常向量表里整个过程在真实硬件上要调试很久才能厘清但在模拟器里很容易复现和理解。5. 源码分析中常见的坑实打实的排查记录5.1 问题速查表先对照看看自己踩了哪个我把实际踩过的坑整理成一张表。它不能覆盖所有问题但覆盖了我遇到的高频场景。现象大概率原因排查方向编译报错找不到标准库使用了普通gcc工具链换成aarch64裸机/独立工具链确认CROSS_COMPILE生效启动后只有xvisor logoguest没反应guest镜像没正确加载或DTS内存配置错误检查guest的memory节点和host-phys-addr是否真实存在且可用guest启动后卡在early consolechosen节点bootargs里console参数不对确认串口设备节点兼容字符串和行为比如pl011还是ns16550guest访问外设触发异常设备树里vdevices没有正确描述设备查看vmm_devtree解析日志确认VirtIO-MMIO地址、中断号是否与驱动匹配调度器切换卡顿或死锁中断未正确注入guest检查GIC相关配置确认中断路由是给哪个guest多次启动结果不一致设备树或内存地址没有按页对齐检查内存区域对齐和起始地址避免page table无法映射这些都是我在分析xvisor源码时反复遇到过的。前两个问题最隐蔽因为日志都有误导性编译报错指向某个看似正常的底层函数实际是工具链不对guest没反应时串口只显示xvisor的信息让人误以为是调度器问题实际是资源没配上。5.2 我的三步排查法当源码报错时别乱改遇到问题不要立刻改源码。我总结了一个排查顺序对裸机hypervisor尤其有效。第一步看日志等级。xvisor日志有明确分级默认可能是info信息太少。先打开perf或debug日志重跑一次确认问题出现在设备树解析阶段、host初始化阶段、guest初始化阶段还是运行阶段。第二步确认异常发生在哪一段地址空间。用GDB看异常时寄存器的值尤其是PC和ELR。如果PC落在guest地址范围内说明是guest执行出问题如果PC落在xvisor自己的某个函数里则说明hypervisor侧逻辑出问题。这一步能快速缩小排查范围。第三步对着DTS做减法。没有任何客套话把guest设备树里的设备逐个删掉直到只能启动一个最小Linux。通过这种“最小化实验”你能找到是哪个设备或哪段内存导致了问题。这个方法看似简单但在源码分析的工作中能省掉一大半乱翻代码的时间。5.3 值得继续深入的两个方向多guest调度与RISC-V支持xvisor本身还在持续演进。如果想继续深入我建议两个方向。第一个多guest调度实验。xvisor支持多个guest并发运行你可以让两个Linux guest共享同一组物理CPU观察调度策略对实时性和吞吐量的影响。打开调度相关的日志可以看到每个vCPU何时被换入、换出触发原因是时间片耗尽还是WFE。第二个RISC-V架构支持分析。RISC-V的虚拟化扩展相对干净xvisor对RISC-V的支持代码比较新但异常流程和MMU关注的思路与ARM一致。对比着看你会发现xvisor把架构无关部分和架构相关部分拆得很干净这个设计本身就是很好的源码阅读教材。6. 最后想分享的一点源码阅读心得源码阅读这件事我一直觉得“跑不起来就不要谈读代码”。xvisor这条线尤其如此。它不像普通应用软件跑起来能看到明确的用户界面理解你的输入输出对不对。Hypervisor的行为全都在串口日志和异常向量表里体现如果你不亲手编译、运行、打断点只看代码永远理解不了vmm_scheduler到底在等谁。我自己的习惯是双屏操作左边源码编辑器右边QEMU窗口两边对照着看。遇到一个trap就在终端看异常信息在源码里搜对应的handler。这样“从编译到运行、从运行到源码、从源码再回到运行”的闭环建立起来之后学习速度会非常快。xvisor不是最强大的商业化hypervisor但它是理解虚拟化原理的极佳窗口。按目录、按trap路径、按实际运行去拆解读完之后你对KVM、Xen、乃至商业hypervisor的理解都会上升一个台阶。如果你打算在自己的板卡上移植xvisor或者想把某个虚拟设备加进去顺着core和arch两条主线走比从头看代码要少走很多弯路。