minos 2.5 中断虚拟化——vGIC

news/2025/3/10 11:04:36/文章来源:https://www.cnblogs.com/randcs/p/18238643
  • 首发公号: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 服务的 vmstruct 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");elselist_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 转换为 ipaif ((esr_value &ESR_ELx_S1PTW) || (dfsc == FSC_FAULT))ipa = get_faulting_ipa(vaddr);elseipa = guest_va_to_ipa(vaddr, 1);// hyp 来处理虚拟设备的 mmioret = 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)) {// 找到对应的虚拟设备,调用其操作函数来处理 mmioret = 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);elsereturn 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);elsereturn 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);elsereturn 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);elsereturn 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 的 groupcase 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 上// 这里模拟实现中,全部为 0case 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 该中断,否则无影响// 这里模拟实现中,全部设置为 0case 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 的 typecase GICD_ICFGR...GICD_ICFGRN:*value = vgicv2_get_virq_type(vcpu, offset);break;// GIC 版本信息,0x2 << 4 表示这是一个 gicv2case 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 gicv2host_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 machineret = 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) * 32dev->gicd_typer |= (vm->vspi_nr >> 5) - 1; // gicd 的一些信息,设置为 0dev->gicd_iidr = 0x0;// 设置该 virtual gic distributor 的一些操作函数dev->vdev.read = vgicv2_mmio_read;  // gicd read functiondev->vdev.write = vgicv2_mmio_write;dev->vdev.deinit = vgicv2_deinit;dev->vdev.reset = vgicv2_reset;// 注册该 vgic,即添加到 vm 的 vdev_listvdev_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 interfacecreate_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_VIRTASSERT((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);
#endifreturn 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_infofor (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);// 创建一个 vmoduleregister_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 Registeruint32_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 空间到 vmif (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,中断优先级过滤器// 只有中断优先级高于该寄存器值的中断才允许发送给 cpucase 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 numbercase GICC_IAR:/* get the pending irq number */*value = get_pending_virq(get_current_vcpu());break;// Running Priority Register// secure extension 可能会使用,这里直接返回全 0case 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 表示这是 gicv2case 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 iduint32_t resv : 3;  // 保留uint32_t pr : 5;    // 该virtual integrrupt 的优先级uint32_t state : 2; // 指示该中断的状态,invalid、pending、active、pending and activeuint32_t grp1 : 1;  // 表示该 virtual integrrupt 是否是 group 1 virtual integrrupt// 0 表示这是一个 group 0 virtual interrupt,表示安全虚拟中断,可配置是按照 virq 还是 vfiq 发送给 vcpu// 1 表示这是一个 group 1 virtual interrupt,表示非安全虚拟中断,该中断以 virq 的形式触发,而不是 vfiquint32_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 interruptif (virq_is_hw(virq))// 记录 physical interrupt idpid = virq->hno;else {// 如果是一个 sgi 类型 virtual interrupt if (virq->vno < 16)// lr 中的 bit12-10 表示 requsting cpu idpid = 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 interruptgich_lr->state = 1;   //表示 pendinggich_lr->hw = !!virq_is_hw(virq);// virq->id 表示第几个 LRwritel_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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

minos 2.6 中断虚拟化——虚拟中断子系统

首发公号:Rand_csHypervisor 需要对每个虚机的虚拟中断进行管理,这其中涉及的一系列数据结构和操作就是虚拟中断子系统 VIRQ 虚拟中断描述符 struct vcpu {uint32_t vcpu_id; .........../** member to record the irq list which the* vcpu is handling now*/struct virq_st…

minos 1.2 内存虚拟化——guest

首发公号:Rand_csminos 1.2 内存虚拟化——guest项目来自乐敏大佬:https://github.com/minosproject/minos本文继续讲述 minos 中的内存虚拟化中关于 guest 的部分,主要弄清楚一个问题,minos 如何管理 guest vm 的内存。 对于虚拟机的内存管理主要是 ipa 的管理,ipa 如何映…

minos 2.1 中断虚拟化——ARMv8 异常处理

首发公号:Rand_cs越往后,交叉的越多,大多都绕不开 ARMv8 的异常处理,所以必须得先了解了解 ARMv8 的异常处理流程 先说一下术语,从手册中的用词来看,在 x86 平台,一般将异常和中断统称为中断,在 ARM 平台,一般将中断和异常统称为异常 异常的流程,可以分为 3 个阶段,…

NSSCTF———MISC

[NISACTF 2022]huaji? [SWPU 2020]套娃 [LitCTF 2023]What_1s_BASE (初级) [SWPUCTF 2021 新生赛]gif好像有点大 [NISACTF 2022]为什么我什么都看不见 [LitCTF 2023]404notfound (初级) [LitCTF 2023]这羽毛球怎么只有一半啊(恼 (初级) [LitCTF 2023]喜欢我的压缩包么 (初级)…

BLP 模型

公号:Rand_csBLP 模型 本篇文章是调研了许多资料后对 BLP 模型的一个总结 MLS,Multi-level Security,主要关心的是数据机密性 D. Elliott Bell 和 Leonard J. LaPadula 在 1996 年提出了基本的 BLP 模型,主要有两个性质:The Simple Security Property states that a subje…

小端序题目——[LitCTF 2023]debase64

还挺有价值的,记录一下 题目DIEPE文件在Windows操作系统下默认使用小端序 IDA main函数绿框:输入长度要20 红框:base64加密 黄框:加密后的结果要与关键数据相等获取数据,我一般喜欢调试看栈,这样就少一次翻转了 取出15个数据:0x46, 0xED, 0x18, 0x96, 0x56, 0x9E, 0xD2,…

【闲话】高考集训之训什么

写的有些杂,凑活看吧好像高考语文作文被学校和《意林》押中题了,主题是“人工智能”,大势所趋。 贴一张奥奇海报(找不到具体来源了)来高考助力吧。本文来自博客园,作者:hzoi_Shadow,原文链接:https://www.cnblogs.com/The-Shadow-Dragon/p/18238551,未经允许严禁转载…

C++文件/流

C++文件读写(文本文件/二进制文件) 一、前言 常见的文件类型可分为两种:文本文件和二进制文件。 文本文件的阅读相对容易,可以使用记事本/Notepad++/VS Code等应用程序直接打开阅览;然而二进制文件由0/1序列组成,直接打卡二进制文件通常是乱码状态。如果需要读取二进制文…

SpringAMQP使用管理RabbitMQ的五种消息模型

使用SpringAMQ实现五种消息队列模型1.普通队列 2.工作队列(WorkQueue) 发布订阅=>根据交换机的不同分为三种 3.订阅模型之Fanout(广播) 4.订阅模型之Direct(路由) 5.订阅模型之Topic(话题) 使用前导: 1.在生产者和消费者项目上分别导入RabbitMQ依赖<!--AMQP依赖,包含Rab…

树开下拉列表数据源生成

改自这位兄台的一段代码 https://www.cnblogs.com/xiayang/archive/2010/07/19/1780566.html1 /// <summary>2 /// 生成一个树形的表样,3 /// </summary>4 /// <param name="dtNodeSets">菜单记录数据所在的表</param>5 …

玄机应急响应-第二章

日志分析-apache日志分析 一,提交当天访问次数最多的IP,即黑客IP:129 ::16555 192.168.200.21 192.168.200.2115 192.168.200.381 192.168.200.48$ cat access.log.1|awk -F {print $1}|sort|uniq -cflag{192.168.200.2}二,黑客使用的浏览器指纹是什么,提交指纹的md5:12 …

C#异步编程是怎么回事(番外)

在上一篇通信协议碰到了多线程,阻塞、非阻塞、锁、信号量...,会碰到很多问题。因此我感觉很有必要研究多线程与异步编程。 首先以一个例子开始我说明一下这个例子。 这是一个演示异步编程的例子。输入job [name],在一个同步的Main方法中,以一发即忘的方式调用异步方法Start…