minos 2.5 中断虚拟化——vGIC

  • 首发公号:Rand_cs

这一节开始讲述真正的中断虚拟化,首先来看硬件方面的虚拟化。前文 minos 2.3 中断虚拟化——GICv2 管理 主要讲述 GICv2 的 Distributor 和 CPU Interface,在 Hypervisor 存在的情况下,它们都是为 Hypervisor 服务的。现在有了 vm,vm 里面的内核也需要操作 GIC,怎么办?我们模拟一个 GIC 设备给 vm 使用。

GICv2 主要就是 Distributor 和 CPU Interface,我们也主要就是模拟这两部分。不过 GICv2 是支持虚拟化的,提供了 Virtual CPU Interface,我们可以直接使用相关特性。

vGIC 基本原理

我们做如下规定,host 端的 gic 叫做 hgic,host 的设备树文件中记录了其接口 base 分别为 hgicd_base,hgicc_base,hgicc_base,它们是真正的物理地址。

同理虚机使用的 gic 叫做 vgic,虚机的设备树文件记录了其接口 base 为 vgicd_base,vgicc_base,vgicc_base(一般没有,或者有不会使用)

而中断虚拟化做的事情之一就是,模拟实现 vgic 给虚机使用

Virtual CPU Interface

如果 hgic 具有虚拟化扩展,那么 hgic 为每个 CPU 增加了一组 Virtual CPU Interface,分为两部分:

  1. Virtual interface control,提供了一系列的控制寄存器,名称前缀以 GICH_xxx 开头,这些寄存器只能由 hypervisor 访问
  2. Virtual CPU interface,与 CPU 相连,可以向运行着虚拟机的 CPU 发送中断信号。也提供了一系列的寄存器,以 GICV_xxx 开头,这些寄存器和 GICC_xxx 的功能一样。

这时,我们再来看一下 GICv2 的架构图,有两种 CPU Interface,它们都可以向 CPU 发送信号,只是在虚拟化的情况下,Virtual CPU Interface 发送信号给 CPU 的时候,CPU 上面运行着的是虚拟机,并且此中断将会由虚拟机里面的 handler 来处理

有了 hgic Virtual CPU Interface 的物理支持,虚拟机需要的 vgic 已经齐了一半了。Virtual CPU Interface 和 CPU Interface 的作用是一样的,GICC_xxx,GICV_xxx 都是对应的。

但还剩下一个极其重要的步骤,虚拟机如何使用 hgic 提供的 Virtual CPU Interface?

host 提供 vgicv_base(pa) 给 guest 使用,但是 guest 访问的地址是 vgicc_base(gpa),所以这下清楚了,minos 需要做的就是将 vgicc_base 映射到 hgicv_base

Virtual Distributor

GICv2 的虚拟化扩展没有提供 Virtual Distributor 物理支持,那咱们就只能软件模拟 Virtual Distributor。

如何模拟一个设备?核心就是模拟设备寄存器读写。设备就是一个类(结构体),寄存器是成员变量,读写操作是成员函数。

软件模拟设备最核心的一点:如何让虚机读写 trap 到我们自己用软件实现的设备,也就是一条访存指令,如何调到我们实现的读写函数?

我们可以通过 stage2 traslation 实现,vgicd 的一系列寄存器地址(gpa),我们不给他映射到实际的物理地址 pa,那么虚机在访问 vgicd_xxx 的时候,就会出现 page fault,相关的 handler 里面判断是否是因为访问了 vgicd_xxx,如果是,调用设备读写函数。

上述就是模拟一个 gic 设备的基本原理,总结如下:

  1. hgic 虚拟化扩展提供了 virtual cpu interface,可以供 guest 作为 cpu interface 使用,核心是将 vgicc_base 映射到 hgicv_base
  2. hgic 没有提供 virtual distributor 支持,所以 virtual distributor 必须软件模拟实现。就是实现一个类,成员变量当作寄存器,成员函数为读写操作。核心是通过 stage2 address translation,对于 vgicd_base 开始的一段空间,不创建 stage2 映射,然后访问 vgicd_base 时将其 trap 到软件实现的设备读写函数

vGIC 实现

vdev

结构定义

struct vdev {
    char name[VDEV_NAME_SIZE + 1];     // 虚拟设备名称
    int host;                 
    struct vm *vm;               // vdev 服务的 vm
    struct vmm_area *gvm_area;   // vdev 内存空间
    struct list_head list;

    // vdev 操作集
    int (*read)(struct vdev *, gp_regs *, int,
            unsigned long, unsigned long *);
    int (*write)(struct vdev *, gp_regs *, int,
            unsigned long, unsigned long *);
    void (*deinit)(struct vdev *vdev);
    void (*reset)(struct vdev *vdev);
    int (*suspend)(struct vdev *vdev);
    int (*resume)(struct vdev *vdev);
};

minos 定义了上述结构体表示一个虚拟设备抽象

void host_vdev_init(struct vm *vm, struct vdev *vdev, const char *name)
{
    if (!vm || !vdev) {
        pr_err("%s: no such VM or VDEV\n");
        return;
    }

    memset(vdev, 0, sizeof(struct vdev));
    vdev->vm = vm;
    vdev->host = 1;
    vdev->list.next = NULL;
    vdev->deinit = vdev_deinit;
    vdev->list.next = NULL;
    vdev->list.pre = NULL;
    vdev_set_name(vdev, name);
}

相关初始化函数如上所示,很简单,各个字段设置成默认值就行

// 虚拟设备添加内存 范围,只是在该 vm 中分配一个 vma,将信息记录到 vma,没有做映射
// MARK,这里没有做实际的物理内存分配和 stage2映射
// 当 guest read 该段内存的时候,vm trap 到 hyp,然后 hyp 负责给 vm 读取内存数据
int vdev_add_iomem_range(struct vdev *vdev, unsigned long base, size_t size)
{
    struct vmm_area *va;

    if (!vdev || !vdev->vm)
        return -ENOENT;

    /*
     * vdev memory usually will not mapped to the real
     * physical space, here set the flags to 0.
     */
    // 这里相当于将 vdev 的内存范围记录到 vm->mm,但是并没有建立实际的映射
    va = split_vmm_area(&vdev->vm->mm, base, size, VM_GUEST_VDEV);
    if (!va) {
        pr_err("vdev: request vmm area failed 0x%lx 0x%lx\n",
                base, base + size);
        return -ENOMEM;
    }
    // 一个 vdev 所有内存段 vma 连接成一个链表,这里添加
    vdev_add_vmm_area(vdev, va);

    return 0;
}

此函数向 vm 注册该虚拟设备使用的内存,对于虚拟机来说增加了一段“有效的” gpa 地址空间,之所以打上引号是因为该段 gpa 地址空间在 host 并没有实际分配物理内存以及 stage2 映射,当虚机读写这部分空间的时候会 trap 到 host 处理

void vdev_add(struct vdev *vdev)
{
    if (!vdev->vm)
        pr_err("%s vdev has not been init\n");
    else
        list_add_tail(&vdev->vm->vdev_list, &vdev->list);
}

这是向 vm 注册一个虚拟设备,就是将其添加到 vm 的 vdev_list 链表

TRAP IO

私以为虚拟设备最为核心的一块儿就是 TRAP IO 了,当虚机向设备内存(内存映射寄存器)读写的时候,触发 data abort exception,然后 trap 到 EL2,让 hyp 来处理内存读写,来看 minos 中如何实现的

static int dataabort_tfl_handler(gp_regs *regs, int ec, uint32_t esr_value)
{
    uint32_t dfsc = esr_value & ESR_ELx_FSC_TYPE;
    unsigned long vaddr, ipa, value;
    int ret, iswrite, reg;
    ..................
    // 从 ESR 寄存器获取当前操作是读 or 写
    iswrite = dabt_iswrite(esr_value);
    reg = ESR_ELx_SRT(esr_value);
    // 获取要读写的数据源地址
    value = iswrite ? get_reg_value(regs, reg) : 0;
    // 从 FAR 获取出错地址
    vaddr = read_sysreg(FAR_EL2);
    // 将 gva 转换为 ipa
    if ((esr_value &ESR_ELx_S1PTW) || (dfsc == FSC_FAULT))
        ipa = get_faulting_ipa(vaddr);
    else
        ipa = guest_va_to_ipa(vaddr, 1);
    // hyp 来处理虚拟设备的 mmio
    ret = vdev_mmio_emulation(regs, iswrite, ipa, &value);
    ...............
}

// hyp 处理 mmio
int vdev_mmio_emulation(gp_regs *regs, int write,
        unsigned long address, unsigned long *value)
{
    struct vm *vm = get_current_vm();
    struct vdev *vdev;
    struct vmm_area *va;
    int idx, ret = 0;
    // 遍历该 vm 的虚拟设备
    list_for_each_entry(vdev, &vm->vdev_list, list) {
        idx = 0;
        va = vdev->gvm_area;
        // 遍历该虚拟设备的内存空间(vmm_area)
        while (va) {
            // 根据出错地址 ipa 查找该地址落在哪个区间内
            if ((address >= va->start) && (address <= va->end)) {
                // 找到对应的虚拟设备,调用其操作函数来处理 mmio
                ret = handle_mmio(vdev, regs, write,
                        idx, address - va->start, value);
                if (ret)
                    pr_warn("vm%d %s mmio 0x%lx in %s failed\n", vm->vmid,
                            write ? "write" : "read", address, vdev->name);
                return 0;
            }
            idx++;
            va = va->next;
        }
    }
    .............
}

static inline int handle_mmio_write(struct vdev *vdev, gp_regs *regs,
        int idx, unsigned long offset, unsigned long *value)
{
    if (vdev->write)
        return vdev->write(vdev, regs, idx, offset, value);
    else
        return 0;
}

static inline int handle_mmio_read(struct vdev *vdev, gp_regs *regs,
        int idx, unsigned long offset, unsigned long *value)
{
    if (vdev->read)
        return vdev->read(vdev, regs, idx, offset, value);
    else
        return 0;
}
// 调用 vdev 的读写函数
static inline int handle_mmio(struct vdev *vdev, gp_regs *regs, int write,
        int idx, unsigned long offset, unsigned long *value)
{
    if (write)
        return handle_mmio_write(vdev, regs, idx, offset, value);
    else
        return handle_mmio_read(vdev, regs, idx, offset, value);
}

这里我们先看一下 mmio trap 后的处理流程, 整个 trap 以及通知 guest 的流程会在后面讲述。当 trap mmio 的时候,host 通过 ESR、FAR 寄存器可以知道虚机想访问哪个地址,然后 host 就去查询该地址落在哪一个 vdev,找到之后就去调用 vdev 的读写函数

vgicv2

模拟实现 vgicd

这一节来看 minos 中一个具体的虚拟设备实现:vgicv2

// 定义虚拟 gicv2 设备
struct vgicv2_dev {
    struct vdev vdev;
    uint32_t gicd_ctlr;        // vgicd 三寄存器,它们存放着一些设备信息
    uint32_t gicd_typer;
    uint32_t gicd_iidr;
    unsigned long gicd_base;   // vgic 的 base 信息
    unsigned long gicc_base;
    unsigned long gicc_size;  
    uint8_t gic_cpu_id[8];
};

定义了一个 vgicv2 设备,主要包括了一个 vdev 结构(因为只有 gicd 需要模拟),还存放了一些 vgic 信息。前面说过模拟实现一个设备可以看做是实现一个类,minos 里基本也是这样,只是说这个“类”成员分布在各个地方,但是基本思想没变,变量模拟寄存器,然后实现函数来模拟读写寄存器值的操作

// vgic 内存映射寄存器 读写 handler
static int vgicv2_mmio_handler(struct vdev *vdev, gp_regs *regs,
        int read, unsigned long offset, unsigned long *value)
{
    struct vcpu *vcpu = get_current_vcpu();
    struct vgicv2_dev *gic = vdev_to_vgicv2(vdev);

    if (read)
        return vgicv2_read(vcpu, gic, offset, value);
    else
        return vgicv2_write(vcpu, gic, offset, value);
}

// 虚拟 gicd read
static int vgicv2_read(struct vcpu *vcpu, struct vgicv2_dev *gic,
        unsigned long offset, unsigned long *v)
{
    uint32_t tmp;
    uint32_t *value = (uint32_t *)v;

    /* to be done */
    switch (offset) {
    // 全局 Distributor 中断使能位,如果为 0,则所有 pending from distributor 的中断都会被屏蔽
    case GICD_CTLR:
        *value = !!gic->gicd_ctlr;
        break;
    // 指示当前 GIC 的一些信息,比如说当前 gic 是否实现了“安全扩展”,gic 支持的最大 interrupt id,cpu interface 实现个数等等
    case GICD_TYPER:
        *value = gic->gicd_typer;
        break;
    // 每一位表示对应 irq 的 group
    case GICD_IGROUPR...GICD_IGROUPRN:
        /* all group 1 */
        *value = 0xffffffff;
        break;
    // 对于SPI和PPI类型的中断,每一位控制对应中断的转发行为:从Distributor转发到CPU interface: 
    // 读: 0:表示当前是禁止转发的; 1:表示当前是使能转发的; 写: 0:无效 1:使能转发
    case GICD_ISENABLER...GICD_ISENABLERN:
        *value = vgicv2_get_virq_unmask(vcpu, offset);
        break;
    // 对于SPI和PPI类型的中断,每一位控制对应中断的转发行为:从Distributor转发到CPU interface: 
    // 读: 0:表示当前是禁止转发的; 1:表示当前是使能转发的; 写: 0:无效 1:禁止转发
    case GICD_ICENABLER...GICD_ICENABLERN:
        *value = vgicv2_get_virq_mask(vcpu, offset);
        break;
    // 中断的 pending 状态
    // 读:0 表示该中断没有 pending 到任何 processor,
    // 读:1,如果为 PPI 和 SGI,表示该中断 pending 到了当前 processor,如果为 SPI,表示该中断至少 pending 到了 1 个 processor 上
    // 这里模拟实现中,全部为 0
    case GICD_ISPENDR...GICD_ISPENDRN:
        *value = 0;
        break;
    // 清零某中断的 pending 状态
    case GICD_ICPENDR...GICD_ICPENDRN:
        *value = 0;
        break;
    // 某中断的 active 中断
    // 读 0,表示该中断处于 not active 状态,读 1,表示该中断处于 active 状态
    // 写 0,无影响
    // 写 1,如果当前中断还未 active,那么 activate 该中断,否则无影响
    // 这里模拟实现中,全部设置为 0
    case GICD_ISACTIVER...GICD_ISACTIVERN:
        *value = 0;
        break;
    // 清零某中断的 active 状态
    case GICD_ICACTIVER...GICD_ICACTIVERN:
        *value = 0;
        break;
    // 获取每个中断的优先级,当然这里读取的是一个寄存器的值,包含了 4 个中断的优先级
    case GICD_IPRIORITYR...GICD_IPRIORITYRN:
        *value = vgicv2_get_virq_pr(vcpu, offset);
        break;
    // 获取某 GICD_ITARGETSR 寄存器里面关于亲和性的值
    // 对于 GICD_ITARGETSR0 ~ GICD_ITARGETSR7,读取会返回当前 CPU 的 id 值
    case GICD_ITARGETSR...GICD_ITARGETSR7:
        tmp = 1 << get_vcpu_id(vcpu);
        *value = tmp;
        *value |= tmp << 8;
        *value |= tmp << 16;
        *value |= tmp << 24;
        break;
    // irq 32 及以后的中断的 cpu 亲和性
    case GICD_ITARGETSR8...GICD_ITARGETSRN:
        *value = vgicv2_get_virq_affinity(vcpu, offset);
        break;
    // 获取 irq 的 type
    case GICD_ICFGR...GICD_ICFGRN:
        *value = vgicv2_get_virq_type(vcpu, offset);
        break;

    // GIC 版本信息,0x2 << 4 表示这是一个 gicv2
    case GICD_ICPIDR2:
        *value = 0x2 << 4;
    }

    return 0;
}

上述函数是虚机读取 vgicd 寄存器的实现,有了前文 minos 4.3 中断虚拟化——GICv2 管理 的了解,应该很清楚 gic 的寄存器读写方式就是 base + offset,这里模拟实现也是类似,各种 switch case 都有详细注释,以及 vgicv2_write 就是相应的逆操作,这里不再赘述。

vgic 初始化

在 vGIC 基本原理一节讲述过,要实现 vgicc 和 vgicd 有两个很重要的步骤,这一节主要就是看看在初始化阶段这两个步骤是如何实现的,这主要在 vgicv2_virqchip_init 中,我们一步步来看:

// virtual gic chip init
static struct virq_chip *vgicv2_virqchip_init(struct vm *vm,
        struct device_node *node)
{
    int ret, flags = 0;
    struct vgicv2_dev *dev;
    struct virq_chip *vc;
    struct vgicv2_info vinfo;

    pr_notice("create vgicv2 for vm-%d\n", vm->vmid);

    // 从 vm dts 中获取 vgic 的一些信息
    ret = get_vgicv2_info(node, &vinfo);
    ..........
}
//............................................................
// GICC: CPU interface寄存器
// GICD: distributor寄存器
// GICH: virtual interface控制寄存器,在hypervisor模式访问
// GICR: redistributor寄存器
// GICV: virtual cpu interface寄存器
// GITS: ITS寄存器

// gicv2 的接口base信息
struct vgicv2_info {
    unsigned long gicd_base;
    unsigned long gicd_size;
    unsigned long gicc_base;
    unsigned long gicc_size;
    unsigned long gich_base;
    unsigned long gich_size;
    unsigned long gicv_base;
    unsigned long gicv_size;
};

第一步,从虚机的设备树文件中获取 vgic 的信息,注意,是虚机使用的 vgic 信息

定义了一个 vgicv2_info 结构体,里面记录了各个接口 base 基址,对于 gic 各种寄存器前缀的含义,我总结在了上面注释中,gicr、gits 是 gicv3 gicv4 的特性,可以先不用在意

get_vgicv2_info 函数是设备树分析函数,这里不拿出来讲解了,只需要知道,该函数执行后,虚机使用的 vgic 信息会记录在 struct vgicv2_info vinfo;

// virtual gic chip init
static struct virq_chip *vgicv2_virqchip_init(struct vm *vm,
        struct device_node *node)
{
........
    // 分配 vgicv2_dev 结构体
    dev = zalloc(sizeof(struct vgicv2_dev));
    if (!dev)
        return NULL;

    // 设置 gic distributor 基址
    dev->gicd_base = vinfo.gicd_base;
    // 初始化虚拟设备 virtual gicv2
    host_vdev_init(vm, &dev->vdev, "vgicv2");
...........
}

这一步比较简单,分配 vgicv2_dev 结构体,并调用相关函数初始化

    // 添加虚拟设备的内存映射区域
    // trap all Guest OS accesses to the GIC Distributor registers, 
    // so that it can determine the virtual distributor settings for each virtual machine
    ret = vdev_add_iomem_range(&dev->vdev, vinfo.gicd_base, vinfo.gicd_size);
    if (ret)
        goto release_vdev;

vdev_add_iomem_range 函数讲过,这里就是为 vgicd 的内存分配 vmm_area,然后注册到 vm

这里有个隐藏点很重要:vdev_add_iomem_range 并没有给 vgicd 分配实际的物理内存,没有进行实际的 stage2 映射,所以虚机读写 vgicd 寄存器的时候就会发生 data abort,然后执行后续一系列的 trap mmio 流程

    // 表示实现的 cpu interface 数量,也就是 cpu 数量
    dev->gicd_typer = vm->vcpu_nr << 5; 
    // 表示 ITLinesNumber,支持的最大中断数 = (ITLinesNumber + 1) * 32
    dev->gicd_typer |= (vm->vspi_nr >> 5) - 1; 
    // gicd 的一些信息,设置为 0
    dev->gicd_iidr = 0x0;

    // 设置该 virtual gic distributor 的一些操作函数
    dev->vdev.read = vgicv2_mmio_read;  // gicd read function
    dev->vdev.write = vgicv2_mmio_write;
    dev->vdev.deinit = vgicv2_deinit;
    dev->vdev.reset = vgicv2_reset;
    // 注册该 vgic,即添加到 vm 的 vdev_list
    vdev_add(&dev->vdev);

这里就是初始化 vgicd 的一些寄存器值,设置 vgicd 的操作函数,然后向 vm 注册该虚拟设备

    /*
     * if the gicv base is set indicate that
     * platform has a hardware gicv2, otherwise
     * we need to emulated the trap.
     */
    // 如果不是 SWE,表明该平台有硬件 gicv2 虚拟化支持,创建相应的内存映射
    if (vgicv2_mode != VGICV2_MODE_SWE) {
        flags |= VIRQCHIP_F_HW_VIRT;
        pr_notice("map gicc 0x%x to gicv 0x%x size 0x%x\n",
                vinfo.gicc_base, vgicv2_info.gicv_base,
                vinfo.gicc_size);
        // remap the GIC CPU interface register address space to point to the GIC virtual CPU interface registers. 
        // 需要将 physical cpu interface 映射到 virtual cpu interface
        create_guest_mapping(&vm->mm, vinfo.gicc_base,
                vgicv2_info.gicv_base, vinfo.gicc_size,
                VM_GUEST_IO | VM_RW);
    // 否则就应该创建一个 vgicc
    } else {
        ret = vgicv2_create_vgicc(vm, vinfo.gicc_base, vinfo.gicc_size);
        if (ret)
            goto release_vgic;
    }

这一部分判断 gicv2 是否有虚拟化支持,如果有,则创建 hgicv_base 到 vgicc_base 的映射。如果没有,软件模拟实现 vgicc

这里出现了 vgicv2_mode,vgicv2_info,它们是两个全局变量,来看它们的初始化流程:

static int __init_text gicv2_init(struct device_node *node)
{
........................
    // 获取 platform dts 中关于 gic 的信息
    // 获取 gicc、gicd、gich、gicv base size 信息
    translate_device_address_index(node, &array[0], &array[1], 0);
    translate_device_address_index(node, &array[2], &array[3], 1);
    translate_device_address_index(node, &array[4], &array[5], 2);
    translate_device_address_index(node, &array[6], &array[7], 3);

#ifdef CONFIG_VIRT
    ASSERT((array[4] != 0) && (array[5] != 0))
    // host 映射,gich 只能由 host 访问
    gicv2_hbase = io_remap((virt_addr_t)array[4], (size_t)array[5]);
#endif
...............
#if defined CONFIG_VIRQCHIP_VGICV2 && defined CONFIG_VIRT
    // vgic 初始化
    vgicv2_init(array, 8);
#endif
    return 0;
}

// 初始化 virtual gicv2 需要用的一些信息
// data 里面是一些 gicd、gicc、gich、gicv 的基址和大小
int vgicv2_init(uint64_t *data, int len)
{
    unsigned long *value = (unsigned long *)&vgicv2_info;
    uint32_t vtr;
    int i;

    if ((data == NULL) || (len == 0)) {
        pr_notice("vgicv2 using software emulation mode\n");
        vgicv2_mode = VGICV2_MODE_SWE;
        return 0;
    }
    // 将 data 里面的信息记录到全局变量 vgicv2_info
    for (i = 0; i < len; i++) {
        value[i] = data[i];
        if (value[i] == 0) {
            pr_err("invalid vgicv2 address, fallback to SWE mode\n");
            vgicv2_mode = VGICV2_MODE_SWE;
            return 0;
        }
    }
    // gicv_base == 0 表示该 gicv2 不支持虚拟化
    if (vgicv2_info.gicv_base == 0) {
        pr_warn("no gicv base address, fall back to SWE mode\n");
        vgicv2_mode = VGICV2_MODE_SWE;
        return 0;
    }
    // VGIC Type Register, GICH_VTR
    // 记录了 GIC Virtualization Externsions 的一些信息
    vtr = readl_relaxed((void *)vgicv2_info.gich_base + GICH_VTR);
    // The number of implemented List registers, minus one
    // 获取 List register 个数
    gicv2_nr_lrs = (vtr & 0x3f) + 1;
    pr_notice("vgicv2: nr_lrs %d\n", gicv2_nr_lrs);

    // 创建一个 vmodule
    register_vcpu_vmodule("vgicv2", gicv2_vmodule_init);

    return 0;
}

从上述可知,vgicv2_info 这个全局变量记录的是 hgic 的 gicc gicd gich gicv 的 base 和 size 信息。vgicv2_mode 变量表示 hgic 是否支持虚拟化,如果平台的设备树节点有标识 gicv 的一些信息,就表示支持虚拟化。

上述就是 vgic 初始化的大致流程,代码中涉及的东西,我省略的了 vgicc 模拟和 virq_chip,virq_chip 下一节讲述,这里再来看一下 vgicc 的模拟

模拟实现 vgicc

如果 vgicv2_mode != VGICV2_MODE_SWE,表明 hgic 不支持虚拟化扩展,不支持 virtual cpu interface,不能提供 gicv 给虚机使用,那么我们就要使用软件来模拟实现一个 vgicc

// virtual gic cpu interface
struct vgicc {
    struct vdev vdev;
    unsigned long gicc_base;
    uint32_t gicc_ctlr;
    uint32_t gicc_pmr;  //Interrupt Priority Mask Register
    uint32_t gicc_bpr;  //将优先级分为group priority field and the subpriority field
};

// 创建 virtual gicc
static int vgicv2_create_vgicc(struct vm *vm, unsigned long base, size_t size)
{
    struct vgicc *vgicc;
    // 分配 vgicc 结构体
    vgicc = zalloc(sizeof(*vgicc));
    if (!vgicc) {
        pr_err("no memory for vgicv2 vgicc\n");
        return -ENOMEM;
    }
    // vgicc 中的 vdev 初始化
    host_vdev_init(vm, &vgicc->vdev, "vgicv2_vgicc");
    // 注册 vgicc 空间到 vm
    if (vdev_add_iomem_range(&vgicc->vdev, base, size)) {
        pr_err("vgicv2: add gicc iomem failed\n");
        free(vgicc);
        return -ENOMEM;
    }
    // 初始化 vgicc 的信息
    vgicc->gicc_base = base;  // vgicc_base 地址
    vgicc->vdev.read = vgicc_read;  // vgicc 寄存器读取操作
    vgicc->vdev.write = vgicc_write;
    vgicc->vdev.reset = vgicc_reset;
    vgicc->vdev.deinit = vgicc_deinit;
    vdev_add(&vgicc->vdev);

    return 0;
}

上述是创建一个 vgicc 虚拟设备操作,有了 vgicd 的经验,这个应该很容易理解,同样的是定义一个类,实现相关成员变量和成员函数的形式

再次注意 vdev_add_iomem_range 函数并没有实际对 vigcc 空间(gpa)进行 stage2 映射(映射到 pa),所以虚机读写 vgicc_xxx 的时候就会发生 data abort exception,然后发生后续的 trap mmio 流程。

来看一下 vigcc_read 的实现:

// 读取 virtual gic cpu interface 相关寄存器
static int vgicc_read(struct vdev *vdev, gp_regs *reg,
        int idx, unsigned long offset, unsigned long *value)
{
    struct vgicc *vgicc = vdev_to_vgicc(vdev);

    switch (offset) {
    // 在 cpu interface 这个 top-level 层级进行中断的屏蔽控制
    // 如果是 0,则屏蔽所有从 distributor 发送到该 cpu interface 的中断,即该 cpu interface 不能想 cpu 发送中断信号
    // 如果是 1,则相反
    case GICC_CTLR:
        *value = vgicc->gicc_ctlr;
        break;
    // Priority Mask Register,中断优先级过滤器
    // 只有中断优先级高于该寄存器值的中断才允许发送给 cpu
    case GICC_PMR:
        *value = vgicc->gicc_pmr;
        break;
    // Binary Point Register,这个寄存器指示如何将 8bit 的 priority value 分割成 group priority value 和 subpriority field,具体见文档
    case GICC_BPR:
        *value = vgicc->gicc_bpr;
        break;
    // 此寄存器存放着当前中断的 irq number
    case GICC_IAR:
        /* get the pending irq number */
        *value = get_pending_virq(get_current_vcpu());
        break;
    // Running Priority Register
    // secure extension 可能会使用,这里直接返回全 0
    case GICC_RPR:
        /* TBD - now fix to 0xa0 */
        *value = 0xa0;
        break;
    // 
    case GICC_HPPIR:
        /* TBD - now fix to 0xa0 */
        *value = 0xa0;
        break;
    // CPU Interface Identification Register
    // 提供了 GICC 本身的一些信息
    // 0x2 表示这是 gicv2
    case GICC_IIDR:
        *value = 0x43b | (0x2 << 16);
        break;
    }

    return 0;
}

可以和 minos 4.3 中断虚拟化——GICv2 管理一文 gicc_xxx 读写对比来看,它们之间到底有什么差别

List Register

对于有虚拟化扩展的 GIC,hypervisor 使用 List Registers 来维护高优先级虚拟中断的一些上下文信息。

struct gich_lr {
    uint32_t vid : 10;  // virq 中断号

    uint32_t pid : 10;  // 此 field 根据 hw 值不同而不同
                        // hw=1,表示此虚拟中断关联了一个物理中断,此 pid 为实际的 physical irq 中断号
                        // hw=0,bit19表示是否 signal eoi,给 maintenance interrupt 使用,不做讨论
                                 //bit12-10,如果这是一个 sgi 中断,即 virtual interrupt id < 15,那么此位域表示 requesting cpu id

    uint32_t resv : 3;  // 保留

    uint32_t pr : 5;    // 该virtual integrrupt 的优先级

    uint32_t state : 2; // 指示该中断的状态,invalid、pending、active、pending and active

    uint32_t grp1 : 1;  // 表示该 virtual integrrupt 是否是 group 1 virtual integrrupt
                        // 0 表示这是一个 group 0 virtual interrupt,表示安全虚拟中断,可配置是按照 virq 还是 vfiq 发送给 vcpu
                        // 1 表示这是一个 group 1 virtual interrupt,表示非安全虚拟中断,该中断以 virq 的形式触发,而不是 vfiq

    uint32_t hw : 1;    // 该虚拟中断是否关联了一个硬件物理中断
                        // 0 表示否,这是 triggered in software,当 deactivated 的时候不会通知 distributor
                        // 1 表示是,那么 deactivate 这个虚拟中断也会向对应的物理中断也执行 deactivate 操作
                        // 而具体的 deactivate 操作,如果 gicv_ctlr.eoimode=0,写 gicv_eoir 寄存器表示 drop priority 和 deactive 操作同时进行 
                        // 如果 gicv_ctlr.eoimode=1,写 gicv_eoir 寄存器表示 drop priority,写 GICV_DIR 表示 deactive
};

LR 寄存器 base 地址为 GICH_LR,GICH_xxx,GICV_xxx 都属于 Virtual CPU Interface,每个 CPU 都会对应一个 Virtual CPU Interface。GICv2 中,每个 CPU 最多 64 个 LR 寄存器。

每一个的格式如上所示,上面对每一个字段有较详细的解释,这里对一些重点内容再作补充说明。

我们将发送给 minos 的中断叫做物理中断,将发送给虚机的叫做虚拟中断。发送虚拟中断的方式为:获取一个空闲 List Register,向其中写入虚拟中断信息,随后 hgic 负责发送一个中断信号给 CPU。这里的中断信号是真实的一个物理电信号,CPU 上面运行的是虚拟机。

通常有两种向 CPU 发送虚拟中断的方式:

  1. 虚拟中断和物理中断关联,当物理中断发生时,这个物理中断的 handler 就是向 CPU 发送一个虚拟中断
  2. hypervisor 自己获取并写一个 LR 寄存器来发送虚拟中断,这通常会作为一个 hvc 功能给虚机使用

这两种方式最终都是要获取并写一个 LR 寄存器:

// 发送 virq
static int gicv2_send_virq(struct vcpu *vcpu, struct virq_desc *virq)
{
    uint32_t val;
    uint32_t pid = 0;
    struct gich_lr *gich_lr;

    if (virq->id >= gicv2_nr_lrs) {
        pr_err("invalid virq %d\n", virq->id);
        return -EINVAL;
    }

    // 如果该 virtual interrupt 对应着实际的 hardware interrupt
    if (virq_is_hw(virq))
        // 记录 physical interrupt id
        pid = virq->hno;
    
    else {
        // 如果是一个 sgi 类型 virtual interrupt 
        if (virq->vno < 16)
            // lr 中的 bit12-10 表示 requsting cpu id
            pid = virq->src;
    }
    // 构造一个 lr 寄存器值
    gich_lr = (struct gich_lr *)&val;
    gich_lr->vid = virq->vno;
    gich_lr->pid = pid;
    gich_lr->pr = virq->pr;
    gich_lr->grp1 = 0;   //这是一个 group 0 virtual interrupt
    gich_lr->state = 1;   //表示 pending
    gich_lr->hw = !!virq_is_hw(virq);
    // virq->id 表示第几个 LR
    writel_gich(val, GICH_LR + virq->id * 4);

    return 0;
}

发送虚拟中断的时候,LR.state = 1 表示 pending 状态,随后 hgic 向 CPU(其上运行的是 vcpu 线程,运行的是 guest os) 发送信号,CPU 读取 GICV_IAR 之后,LR.state 会变成 active 状态。虚机处理完虚拟中断后写 GICV_EOI or GICV_DIR 之后,LR.state 会变为 inactive 状态,这时候清空对应的 LR,如下所示

// 更新 LR
static int gicv2_update_virq(struct vcpu *vcpu,
        struct virq_desc *desc, int action)
{
    if (!desc || desc->id >= gicv2_nr_lrs)
        return -EINVAL;

    switch (action) {
    case VIRQ_ACTION_REMOVE:
        // 如果关联了物理中断,那么还需要清零对应物理中断pending状态
        // 目前 minos gicv2 没有实现像相关功能
        if (virq_is_hw(desc))
            irq_clear_pending(desc->hno);
    
    // 清空该虚拟中断在 LRs 中的记录
    case VIRQ_ACTION_CLEAR:
        writel_gich(0, GICH_LR + desc->id * 4);
        break;
    }

    return 0;
}

总之,要发送虚拟中断就是获取并写一个空闲 LR 寄存器,发送中断信号CPU响应、中断完成处理都会更改 LR.state,最后会清空对应的 LR 寄存器。

  • 首发公号:Rand_cs
posted @ 2024-06-08 15:12  Rand_CS  阅读(64)  评论(0编辑  收藏  举报