首页 > 其他分享 >minos 2.5 中断虚拟化——vGIC

minos 2.5 中断虚拟化——vGIC

时间:2024-06-08 15:22:39浏览次数:31  
标签:struct vgicc vm base vGIC vdev vgicv2 2.5 minos

  • 首发公号: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

标签:struct,vgicc,vm,base,vGIC,vdev,vgicv2,2.5,minos
From: https://www.cnblogs.com/randcs/p/18238643

相关文章

  • minos 2.6 中断虚拟化——虚拟中断子系统
    首发公号:Rand_csHypervisor需要对每个虚机的虚拟中断进行管理,这其中涉及的一系列数据结构和操作就是虚拟中断子系统VIRQ虚拟中断描述符structvcpu{uint32_tvcpu_id;.........../**membertorecordtheirqlistwhichthe*vcpuishandlingn......
  • minos 2.4 中断虚拟化——中断子系统
    首发公号:Rand_cs前面讲述了minos对GICv2的一些配置和管理,这一节再往上走一走,看看minos的中断子系统中断中断描述符/**ifairqishandledbyminos,thenneedtoregister*theirqhandlerotherwiseitwillreturnthevnum*tothehandlerandpassthe......
  • Affinity Designer for Mac(强大的矢量图设计软件)v2.5.2版
    AffinityDesigner中文版是一款专为设计师打造的强大矢量图形设计软件,它以无与伦比的迅捷、流畅和精确性赢得了设计师们的广泛赞誉。无论您是专注于广告设计、网页设计、图标设计还是用户界面(UI)设计,AffinityDesignerMac版都能为您带来前所未有的高效体验。这款软件将彻底革新......
  • Affinity Photo for Mac(专业级图像处理软件)v2.5.2版
    AffinityPhotoforMac是一款专为Mac用户打造的专业级图像处理软件,具备强大的编辑功能,包括图层管理、滤镜效果和色彩调整等,能够满足从摄影师到设计师以及绘画爱好者的各种图像处理需求。它不仅保持了高性能,还注重用户体验和界面设计,使得操作更加直观流畅。此外,AffinityPhoto......
  • Error: Failure while executing; mpdecimal-2.5.1.catalina.bottle.tar.gz` exited w
    ==>Installingdependenciesfornode:mpdecimal,ca-certificates,[email protected],readline,sqlite,xz,[email protected],brotli,cmake,c-ares,icu4c,libnghttp2andlibuv==>Installingnodedependency:mpdecimalError:Failurewhileexecuting;`/usr/bin/......
  • 《Linux内核完全注释》学习笔记:2.5 Linux内核对内存的使用方法
    在Linux0.11内核中,为了有效地使用机器中的物理内存,内存被划分成几个功能区域,如图2-9所示。图2-9物理内存使用的功能区域分布图Linux内核程序占据在物理内存的开始部分,接下来是用于供硬盘或软盘等块设备使用的高速缓冲区部分。当一个进程需要读取块设备中的数据时,系统会......
  • Spire.Doc for Java 12.5.1 -2024-05-30
    Spire.DocforJavaisaprofessionalWordAPIthatempowersJavaapplicationstocreate,convert,manipulateandprintWorddocumentswithoutdependencyonMicrosoftWord.Byusingthismultifunctionallibrary,developersareabletoprocesscopioustasks......
  • 探索气象数据的多维度三维可视化:PM2.5、风速与高度分析
    探索气象数据的多维度可视化:PM2.5、风速与高度分析摘要在现代气象学中,数据可视化是理解复杂气象模式和趋势的关键工具。本文将介绍一种先进的数据可视化技术,它能够将PM2.5浓度、风速和高度等多维度数据以直观和动态的方式展现出来。一、视角与复位气象数据可视化首先需......
  • sybase12.5服务器端的安装
    sybase12.5客户端安装sybase安装教程转载https://blog.51cto.com/u_16213592/7296293sybase数据库的安装分为服务器端和客户端,本文先介绍一下服务器端的安装。1、和其他程序一样,双击setup.exe.2、出现欢迎界面,直接点击next即可。3、下面选择相应国家的协议,咱是中国人,当......
  • 【python】中国五大城市PM2.5 数据分析(含数据集及完整代码)
    目录问题描述(数据集、项目代码已资源绑定)前瞻 数据预处理数据可视化北京上海广州成都沈阳总览对比图北京部分代码问题描述(数据集、项目代码已资源绑定,见文章开头) 数据集前瞻         随着工业化和城市化的迅速发展,全球多个城市都面临着空气污......