minos 2.5 中断虚拟化——vGIC

首发公号:Rand_cs
该项目来自乐敏大佬:https://github.com/minosproject/minos

这一节开始讲述真正的中断虚拟化,首先来看硬件方面的虚拟化。前文 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 寄存器。

本文就先这么多,xxx

首发公号:Rand_cs

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/673406.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Nvidia Jetson/Orin +FPGA+AI大算力边缘计算盒子:3D扫描仪 实时创建 VR 内容

虽然 VR 技术彻底改变了娱乐、医疗、建筑、教育和产品设计等各个日常生活领域&#xff0c;但创建 VR 内容仍然是一项不易突破的挑战。 英伟达在旧金山举行的 Jetson TX2发布会上&#xff0c;展示了Jetson TX2如何能够加快 AI 计算、图形和计算机视觉的运行速度&#xff0c;并且…

【一小时学会Charles抓包详细教程】Charles 抓包相关设置 (7)

&#x1f680; 个人主页 极客小俊 ✍&#x1f3fb; 作者简介&#xff1a;程序猿、设计师、技术分享 &#x1f40b; 希望大家多多支持, 我们一起学习和进步&#xff01; &#x1f3c5; 欢迎评论 ❤️点赞&#x1f4ac;评论 &#x1f4c2;收藏 &#x1f4c2;加关注 Charles 抓包相…

数据库学习总结

Mysql学习总结 汇总数据 聚集函数&#xff1a; 函数 说明 AVG() 返回某列的平均值 COUNT() 返回某列的行数 MAX() 返回某列的最大值 MIN() 返回某列的最小值 SUM() 返回某列值之和 例&#xff1a; AVG函数&#xff1a; select avg(grade) from topic; COUNT函…

WiFi蓝牙模块促进传统零售数字化转型:智能零售体验再升级

随着科技的不断发展&#xff0c;数字化转型已经成为了各行各业的必然趋势。在传统零售业中&#xff0c;WiFi蓝牙模块的应用正逐渐推动着行业的数字化转型&#xff0c;为消费者带来更加智能化、便捷化的零售体验。本文MesoonRF美迅物联网将从以下几个方面阐述WiFi蓝牙模块在传统…

稍微学学react

文章开始前&#xff0c;先划划水~ 今日份开心&#xff1a; 今天看之前发布的按钮npm包下载量有162次&#xff0c;早知道好好做了 今日份不开心&#xff1a; 爬岗位看到一个整体都挺满意的岗位&#xff0c;公司位置和发展大方向都好喜欢&#xff01;&#xff01;&#xff01;…

机器学习学习(2)

基于数据流图的编程范式:声明式编程(Declarative Programming )、命令式编程(Imperative Programming ); 声明式编程(Declarative Programming ) 代表性框架:TensorFlow, CNTK, Caffe2 特点:用户只需要表达模型结构和需要执行的任务,无需关注底层的执行流程,框…

【UE+GIS】UE5GIS CAD或shp构建3D地形

贴合地形的矢量图形实现方法 一、灰度图的制作和拉伸换算1、基于高程点集实现2、基于等高线实现3、拉伸计算 二、生成地形模型的实现方案1、3Dmax导入灰度图2、使用ArcMap/Arcpro/FME等GIS数据处理工具3、UE导入灰度图 三、地形上叠加地形渲染效果的实现方案1、贴花2、数据渲染…

【transformers】pytorch基础

传送门&#xff1a;https://transformers.run/c2/2021-12-14-transformers-note-3/ pytorch基础知识 tensor &#xff1a; 张量。 需要知道的内容&#xff1a; 张量构建张量计算自动微分形状调整广播机制索引与切片升降维度 Tensor 张量&#xff1a;理解成高纬度的向量就完…

【最新鸿蒙应用开发】——什么是状态管理?

状态管理 在声明式UI编程框架中&#xff0c;UI是程序状态的运行结果&#xff0c;用户构建了一个UI模型&#xff0c;其中应用的运行时的状态是参数。当参数改变时&#xff0c;UI作为返回结果&#xff0c;也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染&#xf…

系统安全及其应用

系统安全&#xff1a; 1&#xff09;保护数据安全&#xff0c; 2&#xff09;互联网&#xff0c;网络业务服务等&#xff0c;必须要通过工信部的资质审核 3&#xff09;保护品牌形象 应用&#xff1a; 账号安全 1&#xff09;把不需要或者不想登录的用户设置为nologin us…

echarts绘制三维柱状图

echarts ECharts 是一个使用 JavaScript 实现的开源可视化库&#xff0c;主要用于数据的可视化展示。ECharts 支持丰富的图表类型&#xff0c;如折线图、柱状图、饼图、地图、K线图等&#xff0c;可以满足不同类型数据的展示需求。 文档地址&#xff1a;echarts 本次所绘制三…

Django request.POST获取提交的表单数据

在Django中&#xff0c;request.POST 是一个特殊的属性&#xff0c;它是一个类似于字典的对象&#xff0c;用于访问通过POST方法提交的表单数据。如果你在视图中使用 print(request.POST.get(username))&#xff0c;这通常意味着你正在尝试从一个HTML表单中获取一个名为 userna…

数学建模之MATLAB入门教程(上)

前言&#xff1a; • MATLAB是美国Math Works公司出品的商业数学软件&#xff0c;用于数据分析、无线通信、深度学习、图像处理与计算机视觉、信号处理、量化金融与风险管理、机器人&#xff0c;控制系统等领域。 • MATLAB将数值分析、矩阵计算、科学数据可视化以及非线性动…

Ubuntu server 24 (Linux) 普通用户不能sudo 也不能使用root登录 忘记root密码 修复解决方案

一 普通用户无法sudo&#xff0c;同时也没有其他用户可用 #test用户使用sudo报错&#xff0c;没有权限 testtest:~$ sudo vi /etc/sudoers [sudo] password for test: test is not in the sudoers file. 二 关闭ubuntu 服务器&#xff0c;重新开机 按下ESC 键 1 出现GRUB…

【工具】探索 MOU:每用户通话时长

缘分让我们相遇乱世以外 命运却要我们危难中相爱 也许未来遥远在光年之外 我愿守候未知里为你等待 我没想到为了你我能疯狂到 山崩海啸没有你根本不想逃 我的大脑为了你已经疯狂到 脉搏心跳没有你根本不重要 &#x1f3b5; 邓紫棋《光年之外》 什么是 MOU…

RunLoop小白入门

核心概念 什么是 RunLoop ? RunLoop 是 iOS 和 macOS 应用程序框架中的一个核心概念&#xff0c;用于管理线程的事件处理。它可以看作是一个循环&#xff0c;用于持续接收和处理各种事件&#xff0c;如用户输入、定时器、网络事件等。RunLoop 在保持应用程序响应用户交互和系…

【再探】设计模式—备忘录模式与解释器模式

备忘录模式是用于保存对象在某个时刻的状态&#xff0c;来实现撤销操作。而解释器模式则是将文本按照定义的文法规则解析成对应的命令。 1 备忘录模式 需求&#xff1a;保存对象在某个时刻的状态&#xff0c;后面可以对该对象实行撤销操作。 1.1 备忘录模式介绍 提供一种状…

Anaconda创建python环境默认C盘,如何修改路径

文章目录 前言解决方案1.找到Anaconda的根目录2. 找到根目录文件夹&#xff0c;右键-属性-安全 测试-重新创建新的python环境 前言 使用 Anaconda创建python环境&#xff0c;默认在C盘。 如何修改到别的路径呢&#xff1f; base环境 是安装 Anaconda是安装的默认环境&#x…

YOLOv8_obb训练流程-原理解析[旋转目标检测理论篇]

在旋转目标检测网络中,换了个顺序,先把训练流程捋一遍,然后再取捋一下测试的流程。由下图的YOLOv8l_obb网络结构图可以看到相对于目标检测网络,旋转目标检测网络只是在Head层不相同,在每个尺度特征层中增加了Angle分支(浅蓝色),通过两个卷积组和一个Conv卷积得到得到通…

隐马尔可夫链

1 马尔可夫链 马尔科夫链&#xff08;Markov Chain&#xff09;是一种数学模型&#xff0c;它描述了一系列可能事件的概率&#xff0c;其中每个事件的发生仅依赖于前一个事件的状态。这一特性称为“无记忆性”或“马尔可夫性质”。我将用一个简单的天气预测模型作为例子来解释马…