首发公号: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,分为两部分:
- Virtual interface control,提供了一系列的控制寄存器,名称前缀以 GICH_xxx 开头,这些寄存器只能由 hypervisor 访问
- 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 设备的基本原理,总结如下:
- hgic 虚拟化扩展提供了 virtual cpu interface,可以供 guest 作为 cpu interface 使用,核心是将 vgicc_base 映射到 hgicv_base
- 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 发送虚拟中断的方式:
- 虚拟中断和物理中断关联,当物理中断发生时,这个物理中断的 handler 就是向 CPU 发送一个虚拟中断
- 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