Minos 虚拟化: Hypercall 与设备虚拟化
本文拆解 Minos 中 guest 与 hypervisor 的通信与设备模拟机制:Hypercall 的 ID 编码与调用约定、从 HVC 指令到 handler 的分发链路、VM 管理接口,并以 create_guest_vm 为线索串起 mvm—驱动—hypervisor 的完整数据流,最后落到 vdev 框架与 virtio-mmio 块设备虚拟化的实例。
1. 背景与设计选择
1.1 一个需要”双向通信”的系统
minos 采用”VM0 托管其他 VM”的模型:VM0(host Linux)负责创建、销毁、控制所有 guest VM,并通过用户态进程 mvm 来管理每个 guest。这个模型天然产生两个方向的通信需求:
- VM0 → hypervisor(控制面):VM0 要”创建一个新 VM”、”启动一个 VM”、”把一块 guest 内存映射到自己的地址空间”——这些是改变系统状态的管理操作,需要一个同步的控制通道。这就是 hypercall(HVC)。
- guest → VM0(数据面/服务面):guest 运行中遇到”需要 VM0 协助”的事件(访问 virtio 设备寄存器、请求时间、请求重启),需要一个异步的请求-应答通道。这就是第 5、6 章讲的 vmcs + MMIO trap。
两个方向用不同的机制,因为它们的语义完全不同:
| 方向 | 机制 | 同步性 | 谁发起 | 用途 |
|---|---|---|---|---|
| VM0 → hypervisor | HVC hypercall | 同步(hvc 指令陷入 EL2,处理完 eret 返回) |
VM0(host) | 创建/销毁/控制 VM、内存映射、发 virq |
| guest → VM0 | MMIO trap + vmcs | 异步(guest 写共享内存,virq 通知 VM0,轮询应答) | guest | 设备模拟、时间、重启等服务请求 |
注意两者方向相反:hypercall 是”VM0 向 hypervisor 下达指令”,vmcs 是”guest 向 VM0 请求服务”。这一章主题是前者,但两者在 virtio 场景(第 7 章)会串联起来。
1.2 hypercall 与传统系统调用的区别
传统 OS 里,用户程序用 svc(system call)陷入内核请求服务;在 minos 里,VM0 的 Linux 用 hvc(hypercall)陷入 EL2 的 hypervisor 请求服务。两者共享一个关键点:调用方通过寄存器传参、通过寄存器取返回值(ARM 的 SMCCC 约定),但服务对象不同:
- Linux 的
svc→ 服务方是 Linux 内核本身; - VM0 的
hvc→ 服务方是 minos hypervisor(EL2)。
第 2 章先讲 hypercall 的”号码”怎么编码,第 3 章讲号码怎么分发到 handler,第 4 章看 handler 具体做什么。
2. Hypercall 的 ID 编码与调用约定
2.1 为什么需要一套编码规则
hypervisor 面对两类调用:HVC(hypervisor 自有的 hypercall)和 SMC(ARM 标准调用,主要是 PSCI)。两者都要在 EL2 被分发给正确的 handler。为了让”分发”统一、可扩展,minos 复用了 ARM SMCCC(SMC Calling Convention)的函数标识符(Function Identifier)位布局:把”服务类别”编码进 ID 的固定位段,分发时只查这一个位段就能定位 handler。
2.2 Function Identifier 的位布局
arch/aarch64/include/asm/svccc.h:6-18 按 ARM_DEN0028B 定义了 ID 的位含义:
1 | Function Identifier (32-bit) bit layout |
对应的掩码宏(svccc.h:15-18):
| 宏 | 值 | 含义 |
|---|---|---|
SVC_CTYPE_MASK |
0x80000000 |
bit31:fast call 标志 |
SVC_BTYPE_MASK |
0x40000000 |
bit30:64 位调用标志 |
SVC_STYPE_MASK |
0x3f000000 |
bit[29:24]:服务类别 |
SVC_FID_MASK |
0x0000ffff |
bit[15:0]:函数号 |
分发只依赖 STYPE(bit[29:24]):hypervisor 从 ID 里取出这一位段,作为查表下标。svc_service.c:40 正是这么做的:type = (svc_id & SVC_STYPE_MASK) >> 24;。
2.3 minos 的 hypercall ID 编码
include/virt/hypercall.h:10-17 定义编码规则:
1 |
服务类别(hypercall.h:5-8):
| 宏 | 值 | 用途 |
|---|---|---|
HVC_TYPE_VM0 |
0x8 |
VM 生命周期管理(VM0 专用) |
HVC_TYPE_MISC |
0x9 |
杂项(取 vmid、让出 CPU、能力查询) |
HVC_TYPE_VMBOX |
0xa |
VM 间通信(vmbox,Phase 10) |
HVC_TYPE_DEBUG_CONSOLE |
0xb |
调试控制台 |
以 HVC_VM_CREATE 为例(hypercall.h:20,HVC_VM0_FN(0)):
1 | HVC_VM_CREATE = HVC_CALL_BASE + (0x08 << 24) + 0 |
关键点:**HVC_VM_CREATE 的值 0xc8000000 里,0x8 正好落在 STYPE 位段(bit[29:24])**。所以第 3 章分发时 (0xc8000000 & 0x3f000000) >> 24 = 0x08,查到”VM0 服务”这一类的 handler。
同样地,PSCI 标准调用(第 3.4 章)的 ID 是 PSCI_0_2_FN_BASE = 0x84000000(psci.h:14),取 STYPE 位段:(0x84000000 & 0x3f000000) >> 24 = 0x04,正好等于 SVC_STYPE_STDSMC——这就是为什么 PSCI 调用能不加修改地进入 std_smc_handler。
2.4 调用侧的传参与返回
调用方(VM0 的 Linux)把 ID 放 x0,参数放 x1-x6,执行 hvc 指令陷入 EL2。arch/aarch64/virt/trap.c:124-141 的 __arm_svc_handler 把寄存器搬到数组里交给分发函数:
1 | static int __arm_svc_handler(gp_regs *reg, int smc) |
返回时用 SVC_RET1/RET2 宏把结果写回 x0/x1(svccc.h:37-46),eret 后调用方直接从寄存器取。这就是”同步控制通道”的含义:调用全程卡在 EL2,直到结果写回寄存器。
补充:minos 的 hypercall ID 都在
HVC_CALL_BASE(0xc0000000)之上,bit31 恒为 1(fast call),所以__arm_svc_handler里的local_irq_enable()(trap.c:137-138,只对 bit31=0 的 yielding call 生效)对 minos 自身的 hypercall 实际不会触发。
ID 编码定了,分发还需要一张”类别 → handler”的表。下一章看这张表怎么建、怎么查。
3. 分发机制:从 HVC 指令到 handler
3.1 异常入口:EC 分发
guest 执行 hvc 指令时,CPU 以 EC = HVC64(0x16)陷入 EL2。trap.c:392-420 的 handle_vcpu_sync_exception 读出 ESR 的 EC 字段,查 guest_sync_descs[] 表:
1 | esr_value = read_esr_el2(); |
注意:ret_addr_adjust 对 HVC 是 0(trap.c:363)——HVC 陷入时 ELR_EL2 已指向下一条指令,不需要再加;而 WFx 表项是 4(trap.c:344)——WFI/WFE 陷入时 ELR_EL2 停在 WFI 指令本身,需 +4 跳过它,否则会再次执行 WFI。
HVC64 的表项(trap.c:363):
1 | DEFINE_SYNC_DESC(guest_ESR_ELx_EC_HVC64, EC_TYPE_AARCH64, |
aarch64_hypercall_handler(trap.c:320-329)先看 VM 有没有自定义的 HVC 处理钩子(arm_data->hvc_handler),没有就走通用路径 __arm_svc_handler(reg, 0)(第 2.4 节),最终到 do_svc_handler。SMC64 同理,走 aarch64_smccall_handler(trap.c:331-340)→ __arm_svc_handler(reg, 1)。
3.2 handler 注册表:section 收集
分发表本身由 编译期 section + 链接期收集 构建,这与 Phase 5 的 DEFINE_SYNC_DESC、Phase 7 的 DEFINE_PLATFORM 是同一套模式。svccc.h:78-94 定义两个注册宏:
1 |
struct svc_desc(svccc.h:71-76)含 type_start/type_end(可服务的一整段类别)和 handler。各模块用宏把自己”登记”进对应 section:
hypercall.c:144-148:vm_hvc_handler覆盖HVC_TYPE_VM0、misc_hvc_handler覆盖HVC_TYPE_MISC;smc_service.c:110-111:std_smc_handler覆盖SVC_STYPE_STDSMC(PSCI 标准服务)。
开机时 svc_service_init(svc_service.c:77-87)把 section 展开填进两张表:
1 | static struct svc_desc *smc_descs[SVC_STYPE_MAX]; /* svc_service.c:26-27 */ |
即:hvc_descs[0x08] = vm_hvc_handler,hvc_descs[0x09] = misc_hvc_handler,smc_descs[0x04] = std_smc_handler。**表的下标就是 STYPE**。
3.3 查表分发:do_svc_handler
do_svc_handler(svc_service.c:29-57)是分发核心:
1 | int do_svc_handler(gp_regs *regs, uint32_t svc_id, uint64_t *args, int smc) |
一次 HVC 调用的完整分发路径:
关键理解:**svc_service.c 只负责”把 ID 的 STYPE 位段翻译成 handler 指针”**,不关心具体业务。加一种新的 hypercall 类别 = 用 DEFINE_HVC_HANDLER 注册一个新 handler 覆盖一个未用的 STYPE,无需改分发代码——这就是”可扩展”的来源。
3.4 SMC 通道:PSCI 的虚拟化
smc_service.c 用同一套机制处理 guest 的 PSCI 调用(CPU 开关、系统复位等)。ARM 的 PSCI 标准函数 ID 落在 SVC_STYPE_STDSMC = 0x04(svccc.h:24),因此 DEFINE_SMC_HANDLER(..., SVC_STYPE_STDSMC, SVC_STYPE_STDSMC, std_smc_handler) 就把所有 PSCI 调用引到 std_smc_handler(smc_service.c:24-108):
1 | static int std_smc_handler(gp_regs *c, uint32_t id, unsigned long *args) |
这里把 guest 的 CPU_ON/CPU_OFF/SYSTEM_RESET 等 PSCI 调用翻译成对 vCPU/VM 的操作——相当于 hypervisor 替 guest 模拟了”电源管理固件”。其中 vcpu_power_on 就是 Phase 7 讲过的多 vCPU 二次启动入口。
理解点:HVC 与 SMC 只是入口指令不同,分发框架完全复用(
smc标志选择smc_descs还是hvc_descs)。PSCI 之所以无需额外注册就能接入,是因为它的标准 ID 本身就带STYPE=0x04。
4. Hypercall 的 VM 管理接口
4.1 一个函数管理所有 VM 操作
vm_hvc_handler(virt/hypercall.c:28-107)是 VM0 侧所有 VM 管理 hypercall 的总入口,按 ID 的函数号(bit[15:0])switch 分发。开头有一段重要的安全检查:
1 | static int vm_hvc_handler(gp_regs *c, uint32_t id, uint64_t *args) |
vm_is_host_vm(get_current_vm()) 检查发起者是否为 VM0——这是”VM0 中心模型”在安全上的体现:只有 VM0 能执行 VM 生命周期操作,其他 guest 调用这里直接 panic。
各 case 几乎都是”取参数 → 调 virt/vm.c 或 virt/vm_pm.c 里的函数 → HVC_RET1 回传结果”的薄封装。分类如下。
4.2 VM 生命周期:create / destroy / restart / power
| 宏 | ID | 实现 | 作用 |
|---|---|---|---|
HVC_VM_CREATE |
HVC_VM0_FN(0) |
create_guest_vm(vm.c:593) |
创建 guest,返回新 vmid |
HVC_VM_DESTORY |
HVC_VM0_FN(1) |
destroy_guest_vm(vm_pm.c:315) |
销毁 guest |
HVC_VM_RESTART |
HVC_VM0_FN(2) |
vm_reset(vm_pm.c:229) |
复位 guest |
HVC_VM_POWER_UP |
HVC_VM0_FN(3) |
power_up_guest_vm(vm_pm.c:431) |
上电启动 guest |
HVC_VM_POWER_DOWN |
HVC_VM0_FN(4) |
vm_power_off(vm_pm.c:322) |
下电 guest |
create_guest_vm 的完整数据流留到第 5 章专门展开;vm_reset/vm_power_off 按”native VM / guest VM”分两条路(vm_pm.c:209-227 的 __vm_reset)。guest 路径并不由 hypervisor 直接重启:__vm_reset 先停所有 vCPU(vcpu_enter_poweroff),guest 分支 guest_vm_reset(vm_pm.c:201-207)只把 vm->state 置 VM_STATE_OFFLINE,再经 vmcs 异步通知 mvm(trap_vcpu_nonblock(VMTRAP_REASON_REBOOT));真正重载镜像并 IOCTL_POWER_UP_VM → do_start_vm 的是 mvm 侧(tools/mvm/main/mvm.c:578-608 的 __vm_reboot)。
4.3 内存与资源:mmap / create resource / virtio mmio init
| 宏 | ID | 实现 | 作用 |
|---|---|---|---|
HVC_VM_MMAP |
HVC_VM0_FN(5) |
create_vm_mmap(vm.c:578) |
把 guest 内存映射进 VM0,返回 VM0 侧 IPA |
HVC_VM_CREATE_RESOURCE |
HVC_VM0_FN(13) |
os_create_guest_vm_resource(os.c:119) |
从 guest dtb 解析创建 VM 资源(vdev/virqchip 等) |
HVC_VM_VIRTIO_MMIO_INIT |
HVC_VM0_FN(11) |
virtio_mmio_init(virtio_mmio.c:224) |
为 virtio 设备分配共享内存并双向映射 |
这三个分别在后续章节展开:HVC_VM_MMAP 在第 5.4 节,HVC_VM_VIRTIO_MMIO_INIT 在第 7.4 节,HVC_VM_CREATE_RESOURCE 对应第 5 章的资源装配。先记住一句话:**HVC_VM_MMAP 让 VM0 获得访问 guest 内存的能力,HVC_VM_VIRTIO_MMIO_INIT 让 VM0 获得操作 virtio 寄存器文件的能力**——它们共同支撑了”VM0 替 guest 处理事务”。
4.4 中断与 vmcs:send virq / create vmcs
| 宏 | ID | 实现 | 作用 |
|---|---|---|---|
HVC_VM_SEND_VIRQ |
HVC_VM0_FN(7) |
send_virq_to_vm(virq.c:299) |
向 guest 注入一个 SPI virq |
HVC_VM_CREATE_VMCS |
HVC_VM0_FN(8) |
vm_create_vmcs(vmcs.c:154) |
创建 VMCS 共享内存并映射到 VM0 |
HVC_VM_CREATE_VMCS_IRQ |
HVC_VM0_FN(9) |
vm_create_vmcs_irq(vmcs.c:189) |
为每个 vCPU 分配一个 hvm virq |
HVC_VM_REQUEST_VIRQ |
HVC_VM0_FN(10) |
request_vm_virqs(vm.c:474) |
批量申请一批 virq |
send_virq_to_vm(virq.c:299)要求 virq ≥ VM_LOCAL_VIRQ_NR(只支持 SPI),找到 desc 后走 Phase 5 的注入路径。这组接口是第 7 章 virtio 完成”数据就绪 → 通知 guest”的通道:后端在 VM0 处理完请求,通过 HVC_VM_SEND_VIRQ 让 hypervisor 往 guest 注入中断。
4.5 杂项:misc_hvc_handler
misc_hvc_handler(hypercall.c:122-142)给所有 VM(不只 VM0)提供几个通用服务:
| 宏 | ID | 作用 |
|---|---|---|
HVC_GET_VMID |
HVC_MISC_FN(0) |
返回当前 VM 的 vmid |
HVC_SCHED_OUT |
HVC_MISC_FN(1) |
主动让出 CPU(sched()) |
HVC_GET_VM_CAP |
HVC_MISC_FN(2) |
返回 VM 能力位(VM_CAP_HOST/VM_CAP_NATIVE) |
get_vm_capability(hypercall.c:112-120)用宏把 VM 的 flags 翻译成能力位:VM_FLAGS_HOST → VM_CAP_HOST、VM_FLAGS_NATIVE → VM_CAP_NATIVE。VM0 的 Linux 驱动通过 HVC_GET_VM_CAP 判断自身是否为 host(minos_hypercall.h:172-175 的 vm_is_host_vm())。
第 4 章看到的是”hypervisor 侧”的接口;第 5 章把它接到 VM0 侧的实际调用链上,回答”mvm 用户进程怎么一步步调到这里”。
5. 完整数据流:create_guest_vm 的旅程
5.1 mvm:用户态发起者
mvm 是运行在 VM0 里的用户态进程(tools/mvm/main/mvm.c),一个 mvm 进程管理一个 guest。它的初始化流程(mvm_main,mvm.c:704-816)顺序固定:
其中 “创建 VM” 和 “启动 VM” 是分开的两步:create_and_init_vm 里的 create_new_vm(mvm.c:121-154)构造 struct vmtag(名称、OS、核数、内存、入口地址)后发 IOCTL_CREATE_VM;真正的上电在 mvm_main_loop 里发 IOCTL_POWER_UP_VM。这与 Phase 7 的 NO_AUTO_START + vcpu_online 对应:创建只建立 VM 结构,上电才让 vCPU 进就绪队列。
5.2 内核驱动:ioctl → hvc 的翻译层
VM0 内核里加载了 generic/minos-linux-driver/minos.c(mvm 设备驱动)。它把 mvm 的 ioctl 翻译成 hypercall。mvm 的 ioctl(IOCTL_CREATE_VM, &vmtag) 落到驱动的 vm0_ioctl(minos.c:1006-1021)→ ioctl_create_vm(minos.c:984-1004):
1 | static int ioctl_create_vm(struct file *filp, void __user *p) |
create_new_vm(minos.c:951-982)调用 hypercall 封装 hvc_vm_create(info),再为这个 vmid 建立设备节点、注册 mmu_notifier(guest 内存被释放时通知 hypervisor 解除映射):
1 | static int create_new_vm(struct vmtag *info) |
hypercall 的最终封装在 minos_hypercall.h:50-63,用 Linux 的 SMCCC 辅助函数直接执行 hvc:
1 | static inline unsigned long __minos_hvc(uint32_t id, unsigned long a0, ...) |
(HVC_VM_CREATE 进 x0、vmtag 进 x1。)
至此链条是:mvm → ioctl → 驱动 → hvc 指令 → EL2。第 3 章的分发机制接手后进入第 4 章的 create_guest_vm。
5.3 hypervisor 侧:create_guest_vm
create_guest_vm(virt/vm.c:593-623):
1 | int create_guest_vm(struct vmtag __guest *tag) |
四步对应四个关注点:
copy_from_guest(vm.c:600):tag是 VM0 内核的虚拟地址,hypervisor 要把这块数据从 VM0 的地址空间拷到 EL2。它逐页做”VM0 虚拟地址 → IPA → 物理地址 → 临时映射 → memcpy”(vmm.c:500-529)。这也是学习计划里提到的安全边界——guest 输入都从这里进来。vmtag_check_and_config(vm.c:441):校验 vmtag 的合法性,检查/分配 vCPU 亲和性(Phase 7 的vcpu_affinity动态分配就在这)。create_vm(vm.c:885):分配新 vmid(alloc_new_vmid)、__create_vm建 VM 结构、create_vcpus(vm.c:786)逐个建 vCPU 任务。与 Phase 7 完全衔接。guest_mm_init(vm.c:563):建立 guest 的 stage2 页表,映射 guest 内存。
5.4 mmap:把 guest 内存映射给 VM0
VM 创建后,mvm 还需要”访问 guest 内存”的能力(比如把 kernel 拷进去、让 virtio 后端读写 vring)。这就是 map_vm_memory(mvm.c:70-119):
映射完成后,mvm 进程得到一个连续的虚拟地址区间(vm->mmap),guest 的物理内存与 mvm 的进程内存共享同一块物理页。gpa_to_hvm_va(tools/mvm/include/minos/vm.h:100-101)就是做这个换算的:
1 |
含义:guest 物理地址 → mvm 进程虚拟地址,靠”减去 guest 内存基址、加上 mvm 映射基址”完成。virtio 后端用它对 vring 描述符做地址换算(第 7.6 节)。vm_load_images 正是先 mmap、再往这块共享内存里写入 kernel/dtb。
完整创建数据流:
5.5 vCPU 线程与 vmcs 事件循环
创建完 VM、映射完内存,mvm 为每个 vCPU 建一个线程(vm_create_vcpus,mvm.c:311-374)来响应 guest 的服务请求。每个 vCPU 线程绑定一个 eventfd,并把它注册到 VM0 内核(IOCTL_REGISTER_VCPU → register_vm_event,minos.c:338-390)。之后线程在 epoll 上等事件(vm_vcpu_thread,mvm.c:510-561):
1 | while (1) { |
handle_vcpu_event 读 vmcs 的 trap_type/trap_reason/trap_data,按类型分派:VMTRAP_TYPE_MMIO 交给 vcpu_handle_mmio(mvm.c:427-453)遍历 VM 的 vdev 列表模拟设备访问;VMTRAP_TYPE_COMMON 处理重启/关机/取时间等。处理完 vmcs_ack(mvm.c:418-425)把 guest_index++,guest 端的等待循环才放行(Phase 7 第 6 章)。
到这里,”VM0 怎么管 VM”已经走通。但 guest 想要”用设备”还差关键一环:guest 访问设备寄存器时,是谁、在哪一步把请求交给 VM0 的 mvm? 这就是第 6 章的 vdev 机制。
6. vdev 机制:设备虚拟化的框架
6.1 为什么需要 vdev
虚拟设备有两种实现位置:hypervisor 内部实现(如 vGIC、vtimer,直接操作硬件状态)和 VM0 用户态实现(如 virtio 块设备,读写镜像文件)。对于后者,hypervisor 需要一种统一的方式:把 guest 对一段地址的 MMIO 访问,翻译成对 VM0 后端的一次请求。
struct vdev(include/virt/vdev.h:14-29)就是这样一个抽象:它描述”虚拟设备”,持有该设备在 guest 地址空间里的 MMIO 区间,以及 read/write 回调:
1 | struct vdev { |
vdev_add_iomem_range(vdev.c:102-123)在 guest 的 mm 里 split_vmm_area 划出一段 VM_GUEST_VDEV 区,挂进 gvm_area 链表——**这段区间就是 vdev 在 guest 眼中的”设备寄存器文件”**。
6.2 vdev 的注册:从设备树节点创建
hypervisor 侧的 vdev 由 资源解析 创建:resource.c 遍历 VM 的 dtb,遇到”设备类”节点就查 __vdev section 里的创建函数。virtio_mmio.c:222 的注册:
1 | VDEV_DECLARE(virtio_mmio, virtio_match_table, virtio_create_device); |
virtio_create_device(virtio_mmio.c:170-221)做的事:
1 | ret = translate_device_address(node, &base, &size); /* 取设备 MMIO 基址 */ |
6.3 MMIO trap → vdev_mmio_emulation
guest 访问 vdev 的 MMIO 区间时产生数据异常,走 Phase 3 的 dataabort_tfl_handler(trap.c:241-294),最终调到 vdev_mmio_emulation(vdev.c:198-238):
1 | int vdev_mmio_emulation(gp_regs *regs, int write, |
逻辑分两层:
- 先在 hypervisor 的
vdev_list里找:命中就调用该 vdev 的read/write回调(virtio 的实现在第 7 章)。 - 找不到 → 转发给 VM0:
trap_vcpu(VMTRAP_TYPE_MMIO, ...)(vmcs.h:46-50)→__vcpu_trap(vmcs.c:24-129)走 vmcs 共享内存协议,把”这次 MMIO 读/写”投递给 VM0 的 mvm(第 5.5 节的事件循环会收到并处理)。native VM 例外,直接返回-EACCES让 hypervisor 注入 abort。
这里能看到 minos 的”双层设备模型”:能快速处理的(vGIC/vtimer)留在 hypervisor,费资源的(块/网络)下沉到 VM0 用户态。第 7 章 virtio 用的就是下沉路径。
6.4 转发链路小结
guest 一次 MMIO 写请求落到 mvm 的完整转发链(第 7 章会具体到 virtio):
7. virtio-mmio:块设备虚拟化实例
7.1 整体架构:三方协作
virtio 是”前端-后端”模型,在 minos 里被拆成三方:
- guest 侧:Linux 自带的 virtio-mmio 前端驱动,把 virtio 设备当作 MMIO 寄存器 + 共享内存中的 vring 队列。
- hypervisor 侧(
virt/virtio_mmio.c):分配一块共享内存作为”设备寄存器文件”,同时映射到 guest 和 VM0;guest 的 MMIO 写会 trap 进来,hypervisor 负责把写操作转发给 VM0 后端。 - VM0 侧(
tools/mvm/devices/virtio/):mvm 里的 virtio 后端(virtio_block.c等),真正读写磁盘/网络设备,通过 vring 与 guest 交换数据。
7.2 guest 侧如何发现并使用 virtio 设备
前面几节默认”guest 直接操作 MMIO 寄存器”,但 guest 怎么知道有这样一个设备、怎么初始化它,需要补上。答案分两步:mvm 先把设备”装进”guest 的 dtb,Linux 内核再按标准 virtio-mmio 流程探测和初始化。
第 1 步:mvm 把设备写进 guest dtb。 guest 启动装配时(mvm 侧 os_setup_vm → linux_setup_env → vdev_setup_env → vdev_setup_dtb,tools/mvm/devices/vdev.c:232-241),mvm 遍历自己创建的所有 virtio vdev,用 dtb_add_virtio(vdev.c:167-208)在 guest 设备树 /smb 节点下加一个子节点:
1 | fdt_setprop(dtb, node, "compatible", "virtio,mmio", 12); /* 匹配 Linux 驱动 */ |
这个节点告诉 guest 内核:地址 0x1fe00000 + index*0x1000(VM_VIRTIO_IOMEM_BASE)处有一个 virtio-mmio 设备,中断号是 gvm_irq。guest 内核和 mvm 都靠同一个 vring/寄存器文件通信,而设备”长什么样”由 dtb 描述。
第 2 步:Linux 内核探测并初始化。 guest 的 Linux 自带 drivers/virtio/virtio_mmio.c 驱动,按平台设备匹配 "virtio,mmio" 后走标准 virtio 初始化流程:
流程要点:
- 探测:
virtio_mmio_probe读寄存器文件确认设备存在——读MagicValue(应为0x74726976)、Version、DeviceID、VendorID。这些是读操作,而寄存器文件映射进 guest 是VM_IO|VM_RO(第 7.3 节),读直接命中共享内存,不 trap。 - 特性协商:写
Status=ACKNOWLEDGE→ 读HostFeatures(设备能力)→ 写GuestFeatures(协商结果)→ 写Status=DRIVER_OK。这些写操作 trap 到 hypervisor,经virtio_mmio_write转发给 mvm 后端(第 7.5 节)。 - 建队列:guest 在自己的普通内存里分配 vring 三区(desc/avail/used),把地址写进
QueueDesc/Avail/Used寄存器,写QueueReady=1告知设备”队列就绪”。 - 使用:此后每次 I/O,guest 把请求写进 vring 的 desc/avail 环,写
QueueNotify通知;mvm 后端处理完写 used 环并发中断,guest 中断处理里读 used 环拿结果——这就是第 7.7 节的完整链路。
关键理解:guest 侧不需要任何 minos 专用代码——它只是一个标准 Linux virtio-mmio 设备。三方里只有 mvm 和 hypervisor 在”造假设备”(dtb 节点 + 共享寄存器文件 + 事件转发),guest 完全按标准流程使用。
补充(vring 地址的换算):
- 寄存器里存的是 guest 的 IPA——guest 视角的”物理地址”,实际是 Stage2 翻译的输入(Phase 4)。
- mvm 能直接读写 vring,因为同一块物理页被同时映射进三个地址空间:guest 的 Stage2、VM0 的 Stage2、mvm 进程页表(第 5.4 节建 guest 时映射好)。
gpa_to_hvm_va只算偏移:vm->mmap + (gpa - mem_start),数据仍在那块物理页上,两端用不同地址称呼它,无数据搬运。
7.3 寄存器文件与共享内存
virtio-mmio 规范定义了一组寄存器(generic/include/generic/virtio_mmio.h),hypervisor 把这组寄存器放在一块共享物理内存里,同时映射给 guest 和 VM0。关键寄存器(virtio_mmio.h:8-48):
1 | virtio-mmio register file (4K, shared between guest and VM0) |
各寄存器用途(含义):QueueNotify 写队列号触发通知;Status 承载设备状态机(0x070);QueueDesc/Avail/Used 三组寄存器保存 vring 三个区的地址(0x080-0x0a4);Config 是设备配置区(0x100),如块设备的容量。
vring 本身不在寄存器文件里,而是由 guest 分配在自己的普通内存中(通过 QueueDesc/Avail/Used 寄存器把地址告诉设备)。后端通过第 5.4 节的 gpa_to_hvm_va 把 vring 地址从 guest IPA 换算成 mvm 虚拟地址,直接读写。
7.4 virtio_mmio_init:共享内存的建立
virtio_mmio_init(virtio_mmio.c:224-262)由 HVC_VM_VIRTIO_MMIO_INIT(第 4.3 节)触发,参数是 guest 侧的基址 gbase:
1 | int virtio_mmio_init(struct vm *vm, unsigned long gbase, |
同一块物理内存,两个视角:
- 映射进 guest 的
gbase(guest 读寄存器直接命中,写会 trap); - 映射进 VM0 的
hva(mvm 通过vdev_map_iomem→hvm_map_iomem(mvm.c:285-297)再 mmap 进自己进程)。
7.5 寄存器写转发:virtio_mmio_write
guest 写寄存器会 trap 到 EL2,vdev_mmio_emulation 命中 virtio vdev 后调 virtio_mmio_write(virtio_mmio.c:40-147)。它分三类处理:
1 | switch (offset) { |
三类动作(代码里的 ①②③④ 按行为归类):
- 纯共享内存读写(①
HOST_FEATURES_SEL、QUEUE_SEL/QUEUE_NUM等):直接把值写进dev->iomem(共享内存),VM0 后端能直接看到,不需要额外通知; - 只转发(②
QUEUE_NOTIFY):用trap_mmio_write_nonblock(vmcs.h:79-84,nonblock=1)异步通知 VM0”有队列要处理”,不等应答; - 改值后转发(③④
STATUS/QUEUE_READY):STATUS算出本次新增的状态位(value - tmp)写回共享内存再转发;QUEUE_READY用QUEUE_SEL的值替换写入值再转发,让后端知道”哪个队列就绪”。两者都用阻塞版trap_mmio_write通知并等待处理结果。
trap_mmio_write/trap_mmio_write_nonblock(vmcs.h:72-84)是第 6.4 节 __vcpu_trap 的薄封装,把请求经 vmcs 投递给 VM0。**hypervisor 在这里不实现设备逻辑,只做”地址/值的中转”**——真正的设备在 mvm。
7.6 VM0 后端的 virtio 实现
mvm 里 virtio 后端分为两层:通用 virtio 框架(tools/mvm/devices/virtio/virtio.c)+ 具体设备(virtio_block.c/virtio_net.c/virtio_console.c)。本节看它的结构与接口;一条完整的数据链路见第 7.7 节的时序图。
设备初始化(如 virtio_blk_init,virtio_block.c:286-397)做三件事:
virtio_device_init(virtio.c:466-530)→hv_create_virtio_device(virtio.c:68-89)算 guest 侧基址(VM_VIRTIO_IOMEM_BASE + index * 0x1000,gvm.h:60-61),发IOCTL_VIRTIO_MMIO_INIT触发第 7.4 节的共享内存建立,拿到 VM0 侧地址;__virtio_vdev_init(virtio.c:390-418)往寄存器文件里写MagicValue/Version/DeviceID/VendorID/QueueNumMax,申请设备中断vdev_alloc_irq(vdev.c:157-160);- 初始化 vring 的三个指针区。
初始化时序(从 mvm 创建设备到共享寄存器文件就绪):
MMIO 事件入口:mvm 的 vcpu_handle_mmio(mvm.c:427-453)按 guest 地址匹配 vdev,调 vdev->ops->event(virtio 的 virtio_handle_mmio,virtio.c:681-689)→ virtio_mmio_write(virtio.c:653-679)。后端只关心三个寄存器:
| 寄存器 | 后端处理 | 作用 |
|---|---|---|
STATUS |
virtio_status_event(virtio.c:532) |
设备状态变化,FEATURES_OK 时读 acked_features 做特性协商 |
QUEUE_READY |
virtio_buffer_event(virtio.c:593) |
读 vring 三个区地址,gpa_to_hvm_va 换算成 mvm 进程 VA |
QUEUE_NOTIFY |
virtio_queue_event(virtio.c:565) |
队列可处理,调 queue->callback(如 virtio_blk_notify) |
其中 virtio_buffer_event 用第 5.4 节的 gpa_to_hvm_va 把 QueueDesc/Avail/Used 从 guest IPA 换算成 mvm 进程地址——vring 描述符从此在 mvm 侧直接可见。之后各设备就在这块共享 vring 上取请求、做 I/O、回写 used 环,这就是第 7.7 节时序图展示的完整过程。
7.7 一次块设备读写的完整链路
把第 5、6、7 章串起来,guest 发一次块读请求的完整旅程:
完整链路分五段:
- guest 提交请求:guest 的 virtio-blk 驱动把”读扇区 X”写入 vring 的 desc/avail 环(这块内存是 guest 普通内存,无 trap),再写
QueueNotify寄存器触发通知。 - hypervisor 转发:
QueueNotify写 trap 到 EL2 →vdev_mmio_emulation→virtio_mmio_write的QUEUE_NOTIFY分支 →trap_mmio_write_nonblock→ vmcs 写请求、host_index++、发 virq 通知 VM0。 - VM0 处理:mvm vcpu 线程 epoll 醒来 →
handle_vcpu_event→vcpu_handle_mmio→virtio_queue_event→virtio_blk_notify→ 从共享 vring 取请求 →blockif_read异步读镜像文件。 - 完成后通知:磁盘线程读完数据写进 guest 内存 →
virtio_blk_done写状态字节 →virtq_add_used_and_signal更新 used 环 →virtio_send_irq(tools/mvm/include/minos/virtio.h:170-181)置InterruptStatus→vdev_send_irq→send_virq_to_vm→ ioctlIOCTL_SEND_VIRQ(minos.c:502-504)。 - 回到 guest:驱动
hvc_send_virq→HVC_VM_SEND_VIRQ(hypercall.c:69-72)→ hypervisorsend_virq_to_vm注入中断 → guest 中断处理里读 used 环拿结果。
对照学习计划里的实验”跟踪 create_guest_vm 的完整数据流”和”新增一个 hypercall ID”,第 5 章与第 4 章给出了完整的对照路径。
8. 关键知识点
- 两条通道方向相反:VM0→hypervisor 用同步的 HVC hypercall(管理面);guest→VM0 用异步的 MMIO trap + vmcs(服务面)。virtio 把两者串联:guest 写寄存器(trap+vmcs 下去),VM0 处理完再经
HVC_VM_SEND_VIRQ注入中断(hypercall 上来)。 - hypercall ID 复用了 SMCCC 位布局:
HVC_CALL_BASE(0xc0000000) + (STYPE << 24) + fn。分发只查STYPE位段(svc_service.c:40的(svc_id & SVC_STYPE_MASK) >> 24),加新类别只需注册新 handler。 - 分发表由 section 收集构建:
DEFINE_HVC_HANDLER/DEFINE_SMC_HANDLER把struct svc_desc放进__hvc_handler/__smc_handlersection,svc_service_init展开填进hvc_descs/smc_descs表(下标 = STYPE)。 - PSCI 复用同一分发框架:PSCI 标准函数 ID 自带
STYPE=0x04,std_smc_handler注册在SVC_STYPE_STDSMC,把 guest 的 CPU 电源操作翻译成vcpu_power_on/vcpu_off等。 - VM 管理 hypercall 只许 VM0 调用:
vm_hvc_handler开头vm_is_host_vm(get_current_vm())检查,非 VM0 直接 panic——这是”VM0 中心模型”的安全边界。 - create_guest_vm 四步:
copy_from_guest(拷 vmtag)→vmtag_check_and_config(校验/亲和性)→create_vm(vmid + vCPU)→guest_mm_init(stage2)。创建与上电分离(HVC_VM_POWER_UP才让 vCPU 上线)。 - guest 内存以共享物理页暴露给 VM0:
HVC_VM_MMAP→vm_mmap在 VM0 stage2 里映射 guest 物理页;mvm 再 mmap 到自己进程。gpa_to_hvm_va是两端地址换算的关键。 - vdev 双层模型:能快速处理的设备留 hypervisor(vGIC/vtimer),费资源的走
vdev_mmio_emulation未命中路径trap_vcpu下沉到 VM0 用户态。 - virtio 寄存器文件是共享物理页:
virtio_mmio_init一块内存映射进 guest(VM_IO|VM_RO)和 VM0;guest 读直接命中、写 trap,hypervisor 只做寄存器值的中转,设备逻辑全在 mvm。vring 在 guest 普通内存,后端用gpa_to_hvm_va直接访问。
8.1 代码文件关联速查表
本文各章讲到的机制主要落在几个关键文件(行号对应正文引用的关键位置):
| 文件 | 在本文的作用 | 关键位置 |
|---|---|---|
arch/aarch64/include/asm/svccc.h |
ID 位布局 + handler 注册宏:SVC_STYPE_MASK、DEFINE_HVC/SMC_HANDLER、SVC_RET1 |
svccc.h:17/78/87 |
arch/aarch64/virt/svc_service.c |
分发核心:do_svc_handler 查表、parse_svc_desc 建表 |
svc_service.c:29/59 |
arch/aarch64/virt/smc_service.c |
PSCI 虚拟化:std_smc_handler |
smc_service.c:24 |
arch/aarch64/virt/trap.c |
异常入口:aarch64_hypercall_handler、dataabort_tfl_handler、EC 分发 |
trap.c:320/241/392 |
include/virt/hypercall.h |
hypercall ID 编码:HVC_CALL_NUM、HVC_VM_* |
hypercall.h:10/20 |
virt/hypercall.c |
VM 管理 hypercall 入口:vm_hvc_handler/misc_hvc_handler |
hypercall.c:28/122 |
virt/vm.c |
VM 生命周期:create_guest_vm、create_vm_mmap、create_vm |
vm.c:593/578/885 |
virt/vmm.c |
内存映射:vm_mmap、create_hvm_shmem_map、copy_from_guest |
vmm.c:589/483/500 |
virt/vdev.c |
vdev 框架:vdev_mmio_emulation、vdev_add_iomem_range |
vdev.c:198/102 |
virt/vmcs.c / include/virt/vmcs.h |
共享内存协议:__vcpu_trap、trap_mmio_write |
vmcs.c:24 / vmcs.h:72 |
virt/virtio_mmio.c |
hypervisor 侧 virtio:virtio_mmio_write、virtio_mmio_init |
virtio_mmio.c:40/224 |
generic/include/generic/virtio_mmio.h |
virtio 寄存器定义 | virtio_mmio.h:8 |
generic/minos-linux-driver/minos.c |
VM0 内核驱动:ioctl→hypercall 翻译、设备节点 | minos.c:951/478/1106 |
generic/minos-linux-driver/minos_hypercall.h |
hypercall 封装:__minos_hvc、hvc_vm_create |
minos_hypercall.h:50/73 |
tools/mvm/main/mvm.c |
mvm 主流程:create_new_vm、map_vm_memory、handle_vcpu_event |
mvm.c:121/70/482 |
tools/mvm/devices/virtio/virtio.c |
mvm 侧 virtio 框架:virtio_buffer_event、virtio_mmio_write |
virtio.c:593/653 |
tools/mvm/devices/virtio/virtio_block.c |
块设备后端:virtio_blk_notify、virtio_blk_proc |
virtio_block.c:240/150 |
其余辅助文件(
virt/vm_pm.c、virt/os/os.c、tools/mvm/devices/vdev.c、tools/mvm/main/mevent.c等)在正文引用处标注了行号,需要时按引用回查即可。