写点什么

I/O 虚拟化之软件模拟

  • 2024-06-15
    浙江
  • 本文字数:4659 字

    阅读完需:约 15 分钟

I/O虚拟化之软件模拟

关注微信公众号:Linux 内核拾遗

文章来源:https://mp.weixin.qq.com/s/nbwX9Rj6J5iCLkYjcsbCTQ


之前的文章中对 I/O 虚拟化技术的演化历程、基本原理和主流模型作了详细的介绍,如果对这些背景知识还不太了解,可以直接移步:


I/O虚拟化概述(一)


I/O虚拟化概述(二)—— I/O虚拟化原理


I/O虚拟化概述(三)—— 主流I/O虚拟化模型


本文将深入探讨 I/O 虚拟化中的软件模拟技术,希望能为读者提供对该领域的深入理解和实践指导。

1 软件模拟概述

软件模拟是一种 I/O 全虚拟化技术,它通过软件方式实现硬件设备的模拟,使得虚拟机可以像访问真实硬件一样访问虚拟设备。


在 I/O 虚拟化中,软件模拟通常涉及模拟网络接口卡(如 virtio-net)、磁盘控制器(如 virtio-blk)等设备,以提供给虚拟机。

1.1 工作原理

I/O 虚拟化中的软件模拟工作原理是通过虚拟化管理程序(如 QEMU)在宿主机上模拟虚拟机所需的硬件设备。当虚拟机发出 I/O 请求时,虚拟化管理程序拦截这些请求并将其转换为宿主机上对应的硬件操作。比如,对于网络设备的模拟,虚拟机发送的数据包会被 QEMU 接收并转发到宿主机的物理网络接口,实现虚拟机与外部网络的通信。这种模拟方式无需虚拟机直接访问物理硬件,提高了系统的灵活性和安全性。


下图是 QEMU/KVM 以纯软件方式模拟 I/O 设备的示意图:



主要包括如下的步骤流程:


  1. 发起 I/O 操作请求

  2. 当 Guest 中的设备驱动程序发起 I/O 操作请求时,这个请求会被 KVM 模块中的 I/O Trap Code 拦截。

  3. I/O 请求处理与共享页面

  4. KVM 模块处理该 I/O 请求,并将请求的信息存放到 I/O Sharing Page(共享页面)中,然后通知用户空间的 QEMU 进程。

  5. QEMU 获取并模拟 I/O 操作

  6. QEMU 进程从 I/O Sharing Page 中读取具体的 I/O 操作信息,并将该操作交由 QEMU 的 I/O Emulation Code(硬件模拟代码)进行处理。

  7. 硬件模拟代码模拟 I/O 操作,与宿主机的实际设备驱动进行交互,完成 I/O 操作。

  8. 返回 I/O 操作结果

  9. QEMU 进程模拟代码获取操作结果,并将结果放回 I/O Sharing Page 中,然后通知 KVM 模块。

  10. KVM 模块中的 I/O Trap Code 读取 I/O Sharing Page 中的操作结果,并将结果返回给 Guest 中的设备驱动程序。


需要注意的是:


  • 客户机阻塞:在等待 I/O 操作结果时,客户机(Guest)进程可能会被阻塞,直到 I/O 操作完成。

  • 大块 I/O 和 DMA:当客户机通过 DMA(直接内存访问)方式进行大块 I/O 操作时,QEMU 不会将 I/O 操作结果放到 I/O Sharing Page 中,而是通过内存映射的方式将结果直接写入客户机的内存中。完成后,KVM 模块会通知客户机 DMA 操作已完成。

1.2 优缺点分析

QEMU/KVM 纯软件 I/O 设备模拟有以下的优点:


  • 灵活性:可以通过软件模拟出各类硬件设备,而无需修改客户机操作系统,并且独立于 hypervisor,可在不同虚拟化平台上使用。

  • 安全性:隔离虚拟机与物理硬件,减少安全风险,同时 I/O 操作在受控环境中进行,便于监控。

  • 便于调试和开发:软件层面的模拟便于调试和测试。设备可共享和复用,便于开发和维护。


但是(且最关键的是)它有以下的缺点:


  • 性能开销:占用大量 CPU 资源,性能较硬件原生 I/O 性能低。每次 I/O 操作的路径较长,有较多的VM-EntryVM-Exit发生,需要多次上下文切换,也需要多次数据复制,这增加了 I/O 延迟。

  • 复杂性:实现和维护复杂,需要详细模拟硬件行为。多层次协调增加代码复杂性,调试困难。

  • 阻塞和资源竞争:虚拟机等待 I/O 操作时可能被阻塞。多虚拟机并发 I/O 操作可能导致资源竞争和性能下降。

2 QEMU/KVM 中的软件模拟

QEMU 在用户空间中独立进行设备模拟,虚拟设备通过 hypervisor 提供的接口供其他虚拟机(VM)调用。由于设备模拟独立于 hypervisor,这意味着我们可以模拟任何设备,并且这些模拟设备可以在不同的 hypervisor 间共享。


本节我们讲述 QEMU 如何进行设备模拟。

2.1 QEMU 设备 I/O 处理

在虚拟化环境中,虚拟机(VM)在访问某些硬件资源(如物理内存或 I/O 端口)时,可能会触发各种事件,这些事件会导致虚拟机退出到 Hypervisor,并将控制权从 VM 转移到 Hypervisor(如 KVM),以便对这些事件进行适当处理。这些事件被称为 VM-exit 事件。


由于设备模拟通常是在 QEMU 中完成的,因此 KVM 不会处理 I/O 访问导致 VM-exit 事件,而是退出到 QEMU 中进一步处理。


在 QEMU 中,这些事件会根据其类型进行不同的处理。在设备 I/O 模拟中,KVM_EXIT_IO 和 KVM_EXIT_MMIO 是两种常见的 VM-exit 原因,分别对应 I/O 端口访问和内存映射 I/O(MMIO)访问。


KVM_EXIT_IO 事件表示虚拟机试图通过访问 I/O 端口与设备进行通信,可用于控制设备、读取状态或写入数据,包括虚拟串口、键盘和硬盘控制器等设备。当虚拟机执行 I/O 指令(如 IN 或 OUT 指令)后,KVM 捕获事件并将控制权传递给 QEMU 处理,QEMU 通过 kvm_handle_io 函数进行处理。


KVM_EXIT_MMIO 事件表示虚拟机试图通过内存映射 I/O(MMIO)操作与设备进行交互,例如虚拟化的显卡、网卡以及其他通过 MMIO 进行通信的设备。在这种操作中,设备的寄存器被映射到虚拟机的物理内存地址空间中,虚拟机通过读写这些地址来与设备进行通信。当虚拟机尝试访问映射内存区域时,KVM 捕获到这个事件并将控制权交给 QEMU,QEMU 利用 address_space_rw 函数处理该事件。


以下是 QEMU 中 I/O 事件处理部分的代码:


int kvm_cpu_exec(CPUState *cpu){    ...    do {        ...        run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0);        ...        switch (run->exit_reason) {        case KVM_EXIT_IO:            DPRINTF("handle_io\n");            /* Called outside BQL */            kvm_handle_io(run->io.port, attrs,                            (uint8_t *)run + run->io.data_offset,                            run->io.direction,                            run->io.size,                            run->io.count);            ret = 0;            break;        case KVM_EXIT_MMIO:            DPRINTF("handle_mmio\n");            /* Called outside BQL */            address_space_rw(&address_space_memory,                                run->mmio.phys_addr, attrs,                                run->mmio.data,                                run->mmio.len,                                run->mmio.is_write);            ret = 0;            break;        ...        }    } while (...);}
复制代码


代码首先通过kvm_vcpu_ioctl(cpu, KVM_RUN, 0)尝试运行 VCPU,直到遇到导致 VM-exit 的事件。


当 VCPU 由于某个事件退出(VM-exit)时,run_ret会获取 KVM_RUN 的返回值,run->exit_reason会包含退出的原因:


  • 对于 I/O 事件(KVM_EXIT_IO):调用kvm_handle_io()处理 I/O 端口访问,参数包括 I/O 端口号、属性、数据偏移、方向(读/写)、大小和计数等。

  • 对于 MMIO 事件(KVM_EXIT_MMIO):调用address_space_rw()处理 MMIO 访问,参数包括内存空间地址、物理地址、属性、数据、数据长度和是否写操作等。

2.2 PIO 模拟

实际上,PIO 请求的处理函数kvm_handle_io()内部封装了address_space_rw()函数,但它使用了专门的端口地址空间address_space_io来进行操作,而不是通常用于内存映射 I/O 的address_space_memory


static void kvm_handle_io(uint16_t port, MemTxAttrs attrs, void *data, int direction,                          int size, uint32_t count){    int i;    uint8_t *ptr = data;
for (i = 0; i < count; i++) { address_space_rw(&address_space_io, port, attrs, ptr, size, direction == KVM_EXIT_IO_OUT); ptr += size; }}
复制代码


其中port表示虚拟机试图访问的 I/O 端口号,data指向要读取或写入的数据缓冲区,direction指示读写操作类型,sizecount分别每次读写的数据大小(字节)和读写次数。


在 QEMU 中,每个 I/O 设备通常都会有一个相关联的MemoryRegion结构和函数表,用于实现对应的读写操作。kvm_handle_io()通过循环每次处理一个数据单元,函数内部在address_space_io地址空间上调用address_space_rw(),实现对虚拟机 I/O 内存区域的读写。

2.3 MMIO 模拟

对于 MMIO 而言,QEMU 会直接调用 address_space_rw() ,该函数首先将全局地址空间 address_space_memory 展开成 FlatView 后再调用对应的函数进行读写操作。


address_space_rw()函数代码如下:


MemTxResult address_space_rw(AddressSpace *as, hwaddr addr, MemTxAttrs attrs,                             void *buf, hwaddr len, bool is_write){    if (is_write) {        return address_space_write(as, addr, attrs, buf, len);    } else {        return address_space_read_full(as, addr, attrs, buf, len);    }}
复制代码


它根据读写操作类型is_write,选择调用address_space_writeaddress_space_read_full函数来处理读写操作。


MemTxResult address_space_read_full(AddressSpace *as, hwaddr addr,                                    MemTxAttrs attrs, void *buf, hwaddr len){    MemTxResult result = MEMTX_OK;    FlatView *fv;
if (len > 0) { RCU_READ_LOCK_GUARD(); fv = address_space_to_flatview(as); result = flatview_read(fv, addr, attrs, buf, len); }
return result;}
MemTxResult address_space_write(AddressSpace *as, hwaddr addr, MemTxAttrs attrs, const void *buf, hwaddr len){ MemTxResult result = MEMTX_OK; FlatView *fv;
if (len > 0) { RCU_READ_LOCK_GUARD(); fv = address_space_to_flatview(as); result = flatview_write(fv, addr, attrs, buf, len); }
return result;}
复制代码


可以看到,address_space_read_full()address_space_write()均调用了address_space_to_flatview()函数,将AddressSpace转换为FlatView,然后再执行具体的处理函数(flatview_read()或者flatview_write())。


static MemTxResult flatview_read(FlatView *fv, hwaddr addr,                                 MemTxAttrs attrs, void *buf, hwaddr len){    hwaddr l;    hwaddr addr1;    MemoryRegion *mr;
l = len; mr = flatview_translate(fv, addr, &addr1, &l, false, attrs); if (!flatview_access_allowed(mr, attrs, addr, len)) { return MEMTX_ACCESS_ERROR; } return flatview_read_continue(fv, addr, attrs, buf, len, addr1, l, mr);}
static MemTxResult flatview_write(FlatView *fv, hwaddr addr, MemTxAttrs attrs, const void *buf, hwaddr len){ hwaddr l; hwaddr addr1; MemoryRegion *mr;
l = len; mr = flatview_translate(fv, addr, &addr1, &l, true, attrs); if (!flatview_access_allowed(mr, attrs, addr, len)) { return MEMTX_ACCESS_ERROR; } return flatview_write_continue(fv, addr, attrs, buf, len, addr1, l, mr);}
复制代码


函数flatview_read()flatview_write()首先将逻辑地址转换为物理地址并确定对应的MemoryRegion,然后调用flatview_read_continue()或者flatview_write_continue()执行写入操作。


关注微信公众号:Linux 内核拾遗

文章来源:https://mp.weixin.qq.com/s/nbwX9Rj6J5iCLkYjcsbCTQ


发布于: 刚刚阅读数: 2
用户头像

聚沙成塔 2023-01-12 加入

分享Linux内核开发相关的编程语言、开发调试工具链、计算机组成及操作系统内核知识、Linux社区最新资讯等

评论

发布
暂无评论
I/O虚拟化之软件模拟_Linux_Linux内核拾遗_InfoQ写作社区