虚拟化

查看幻灯片

课程目标:

  • 模拟基础知识
  • 虚拟化基础知识
  • 半虚拟化基础知识
  • 对虚拟化的硬件支持
  • Xen 虚拟机监视器(hypervisor)概述
  • KVM 虚拟机监视器概述

模拟(emulation)基础知识

  • 指令被模拟(每次执行时都会模拟)
  • 其他系统组件也被模拟:
    • MMU
    • 物理内存访问
    • 外围设备
  • 目标架构——被模拟的架构
  • 主机架构——模拟器运行所基于的架构
  • 如果是模拟,目标架构和主机架构可以不同

虚拟化(virtualization)基础知识

  • 由 Popek 和 Goldberg 在 1974 年的一篇论文中定义
  • 保真度
  • 性能
  • 安全性
../_images/ditaa-91f08f7db4b54069e16694eab8d75c06400fc47b.png

经典虚拟化

  • 捕获(trap)和模拟
  • 主机和目标使用相同的架构
  • 大多数目标指令可以直接执行
  • 目标操作系统在主机上以非特权模式运行
  • 特权指令被捕获和模拟执行
  • 有两种机器状态:主机和客户机

软件虚拟化

  • 并非所有架构都可以被虚拟化;例如 x86 架构:
    • CS 寄存器编码当前特权级(CPL)
    • 一些指令不会引发捕获(例如 popf 指令)
  • 解决方案:使用二进制翻译来模拟指令

MMU 虚拟化

  • “虚假”的虚拟机物理地址由主机转换为实际的物理地址
  • 客户机虚拟地址 -> 客户机物理地址 -> 主机物理地址
  • 主机硬件不直接使用客户机页表
  • 虚拟机页表经过验证后,在主机上被翻译成一组新的页表(影子页表)

影子页表

 

../_images/ditaa-8632e22c6d89bd18f97c9cef127444486b5077df.png

延迟影子同步

  • 客户机页表的更改通常通过批处理进行
  • 为了避免重复的捕获、检查和转换,将具有写访问权限的客户机页表条目进行映射
  • 在以下情况下更新影子页表:
    • 刷新 TLB
    • 在主机页面故障(page fault)处理程序中

I/O 仿真(emulation)

 

../_images/ditaa-bb69666d75b9670e542682753fb8cc9b77ff8894.png
/*
 * QEMU model of the UART on the SiFive E300 and U500 series SOCs.
 *
 * Copyright (c) 2016 Stefan O'Rear
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU General Public License,
 * version 2 or later, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "qemu/osdep.h"
#include "qapi/error.h"
#include "qemu/log.h"
#include "chardev/char.h"
#include "chardev/char-fe.h"
#include "hw/irq.h"
#include "hw/char/sifive_uart.h"

/*
 * Not yet implemented:
 *
 * Transmit FIFO using "qemu/fifo8.h"
 */

/* Returns the state of the IP (interrupt pending) register */
static uint64_t uart_ip(SiFiveUARTState *s)
{
    uint64_t ret = 0;

    uint64_t txcnt = SIFIVE_UART_GET_TXCNT(s->txctrl);
    uint64_t rxcnt = SIFIVE_UART_GET_RXCNT(s->rxctrl);

    if (txcnt != 0) {
        ret |= SIFIVE_UART_IP_TXWM;
    }
    if (s->rx_fifo_len > rxcnt) {
        ret |= SIFIVE_UART_IP_RXWM;
    }

    return ret;
}

static void update_irq(SiFiveUARTState *s)
{
    int cond = 0;
    if ((s->ie & SIFIVE_UART_IE_TXWM) ||
        ((s->ie & SIFIVE_UART_IE_RXWM) && s->rx_fifo_len)) {
        cond = 1;
    }
    if (cond) {
        qemu_irq_raise(s->irq);
    } else {
        qemu_irq_lower(s->irq);
    }
}

static uint64_t
uart_read(void *opaque, hwaddr addr, unsigned int size)
{
    SiFiveUARTState *s = opaque;
    unsigned char r;
    switch (addr) {
    case SIFIVE_UART_RXFIFO:
        if (s->rx_fifo_len) {
            r = s->rx_fifo[0];
            memmove(s->rx_fifo, s->rx_fifo + 1, s->rx_fifo_len - 1);
            s->rx_fifo_len--;
            qemu_chr_fe_accept_input(&s->chr);
            update_irq(s);
            return r;
        }
        return 0x80000000;

    case SIFIVE_UART_TXFIFO:
        return 0; /* Should check tx fifo */
    case SIFIVE_UART_IE:
        return s->ie;
    case SIFIVE_UART_IP:
        return uart_ip(s);
    case SIFIVE_UART_TXCTRL:
        return s->txctrl;
    case SIFIVE_UART_RXCTRL:
        return s->rxctrl;
    case SIFIVE_UART_DIV:
        return s->div;
    }

    qemu_log_mask(LOG_GUEST_ERROR, "%s: bad read: addr=0x%x\n",
                  __func__, (int)addr);
    return 0;
}

static void
uart_write(void *opaque, hwaddr addr,
           uint64_t val64, unsigned int size)
{
    SiFiveUARTState *s = opaque;
    uint32_t value = val64;
    unsigned char ch = value;

    switch (addr) {
    case SIFIVE_UART_TXFIFO:
        qemu_chr_fe_write(&s->chr, &ch, 1);
        update_irq(s);
        return;
    case SIFIVE_UART_IE:
        s->ie = val64;
        update_irq(s);
        return;
    case SIFIVE_UART_TXCTRL:
        s->txctrl = val64;
        return;
    case SIFIVE_UART_RXCTRL:
        s->rxctrl = val64;
        return;
    case SIFIVE_UART_DIV:
        s->div = val64;
        return;
    }
    qemu_log_mask(LOG_GUEST_ERROR, "%s: bad write: addr=0x%x v=0x%x\n",
                  __func__, (int)addr, (int)value);
}

static const MemoryRegionOps uart_ops = {
    .read = uart_read,
    .write = uart_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .valid = {
        .min_access_size = 4,
        .max_access_size = 4
    }
};

static void uart_rx(void *opaque, const uint8_t *buf, int size)
{
    SiFiveUARTState *s = opaque;

    /* Got a byte.  */
    if (s->rx_fifo_len >= sizeof(s->rx_fifo)) {
        printf("WARNING: UART dropped char.\n");
        return;
    }
    s->rx_fifo[s->rx_fifo_len++] = *buf;

    update_irq(s);
}

static int uart_can_rx(void *opaque)
{
    SiFiveUARTState *s = opaque;

    return s->rx_fifo_len < sizeof(s->rx_fifo);
}

static void uart_event(void *opaque, QEMUChrEvent event)
{
}

static int uart_be_change(void *opaque)
{
    SiFiveUARTState *s = opaque;

    qemu_chr_fe_set_handlers(&s->chr, uart_can_rx, uart_rx, uart_event,
        uart_be_change, s, NULL, true);

    return 0;
}

/*
 * Create UART device.
 */
SiFiveUARTState *sifive_uart_create(MemoryRegion *address_space, hwaddr base,
    Chardev *chr, qemu_irq irq)
{
    SiFiveUARTState *s = g_malloc0(sizeof(SiFiveUARTState));
    s->irq = irq;
    qemu_chr_fe_init(&s->chr, chr, &error_abort);
    qemu_chr_fe_set_handlers(&s->chr, uart_can_rx, uart_rx, uart_event,
        uart_be_change, s, NULL, true);
    memory_region_init_io(&s->mmio, NULL, &uart_ops, s,
                          TYPE_SIFIVE_UART, SIFIVE_UART_MAX);
    memory_region_add_subregion(address_space, base, &s->mmio);
    return s;
}

部分虚拟化

  • 修改客户机操作系统以与虚拟机监视器(VMM)合作
    • CPU 部分虚拟化
    • MMU 部分虚拟化
    • I/O 部分虚拟化
  • VMM 提供超级调用(hypercalls)用于:
    • 激活/停用中断
    • 更改页表
    • 访问虚拟化外设
  • VMM 使用事件触发虚拟机中的中断

Intel VT-x

  • 硬件扩展,将 x86 架构转换为可以进行经典虚拟化的状态
  • 新的执行模式:非根模式(non-root mode)
  • 每个非根模式实例使用虚拟机控制结构(VMCS)来存储其状态
  • VMM 在根模式(root mode)下运行
  • 通过 VM-entry 和 VM-exit 在两种模式之间进行切换

虚拟机控制结构

  • 客户机信息:虚拟 CPU 的状态
  • 主机信息:物理 CPU 的状态
  • 保存的信息:
    • 可见状态:段寄存器、CR3、IDTR 等
    • 内部状态
  • 不能直接访问 VMCS,但可以使用特殊指令访问某些信息

虚拟机进入和退出

  • 虚拟机进入——使用新指令将 CPU 切换到非根模式,并从 VMCS 加载虚拟机状态;主机状态保存在 VMCS 中
  • 允许在客户机中注入中断和异常
  • 根据 VMCS 的配置,虚拟机退出将自动触发
  • 当虚拟机退出时,主机状态从 VMCS 加载,客户机状态保存在 VMCS 中

虚拟机执行控制字段

  • 选择触发虚拟机退出的条件;示例:
    • 如果生成外部中断
    • 如果生成外部中断并且 EFLAGS.IF 被设置
    • 如果修改了 CR0-CR4 寄存器
  • 异常位图——选择生成虚拟机退出的异常
  • IO 位图——选择生成虚拟机退出的 I/O 地址(IN/OUT 访问)
  • MSR 位图——选择生成虚拟机退出的 RDMSR 或 WRMSR 指令

扩展页表

  • 减少 MMU 虚拟化的复杂性,提高性能
  • 不再需要通过虚拟机退出来访问 CR3、INVLPG 和页面故障
  • EPT 页表由 VMM 控制
../_images/ditaa-cc9a2e995be74ee99646ea4bf0e551d766fa92ef.png

VPID

  • 虚拟机进入和退出会强制 TLB 刷新——丢失 VMM / VM 的转换信息
  • 为了避免这个问题,每个虚拟机(VPID 0 保留给 VMM)关联一个 VPID(虚拟处理器 ID)标签
  • 所有 TLB 条目都被标记
  • 在虚拟机进入和退出时,只刷新与标签相关的条目
  • 在搜索 TLB 时,只使用当前的 VPID

I/O 虚拟化

  • 以受控的方式从虚拟机直接访问硬件
    • 将主机的 MMIO 直接映射到客户机
    • 转发中断
../_images/ditaa-3901edd823cdc7a6f429ebc37cbc541e650abc96.png

相比于模拟设备时的陷阱 MMIO,我们可以通过映射到客户机的页表,允许客户机直接访问 MMIO。

设备产生的中断由主机内核处理,并向 VMM 发送信号,VMM 将中断注入到客户机中,就像对于模拟设备一样。

VT-d 使用 I/O MMU(DMA 重映射)来保护和转换虚拟机物理地址。

../_images/ditaa-d880751969de8642b2613caaca345d71acea4500.png
  • 消息传递中断(MSI)= DMA 写入 IRQ 控制器的主机地址范围(例如 0xFEExxxxx)
  • 地址的低位和数据指示要发送到哪个 CPU 的哪个中断向量
  • 中断重映射表指向应该接收中断的虚拟 CPU(VMCS)
  • I/O MMU 将捕获 IRQ 控制器的写入并在中断重映射表中查找
    • 如果该虚拟 CPU 当前正在运行,则直接接收中断
    • 否则,在一个表中设置一个位(发布的中断描述符表),下次运行该 vCPU 时将注入中断
../_images/ditaa-2cb0eb0056bb775d1446843d62241fd660662c96.png
  • 单根——输入输出虚拟化
  • 具有多个以太网端口的物理设备将显示为 PCI 总线上的多个设备
  • 物理功能用于控制且能配置
    • 呈现自身为新的 PCI 设备
    • 使用哪个 VLAN
  • 新的虚拟功能在总线上枚举,并可以分配给特定的客户机

qemu

  • 通过 Tiny Code Generator(TCG)使用二进制翻译进行高效的模拟
  • 支持不同的目标和主机体系结构(例如,在 x86 上运行 ARM 虚拟机)
  • 进程级和完全系统级的仿真
  • MMU 仿真
  • I/O 仿真
  • 可与 KVM 一起用于加速虚拟化

KVM

../_images/ditaa-f8fcc760ef5dad50d1038ed3426d0fcce12fd3e6.png
  • 用于硬件虚拟化的 Linux 设备驱动程序(例如 Intel VT-x、SVM)
  • 基于 IOCTL 的接口,用于管理和运行虚拟 CPU
  • VMM 组件在 Linux 内核中实现(例如中断控制器、定时器)
  • 如果存在,使用影子页表或 EPT
  • 使用 qemu 或 virtio 进行 I/O 虚拟化

类型 1 和类型 2 的 Hypervisor

  • 类型1 = 裸机 Hypervisor
  • 类型2 = 嵌入在现有内核/操作系统中的 Hypervisor

Xen

../_images/xen-overview.png