7.1 GICV3

img

img

寄存器

7.2 GIC初始化

img

根据上面这张图可知,对GIC的初始化分为四个步骤

  • 初始化Distributor,Distributor是所有core共有的
  • 初始化Redistributor,每个core都有自己的Redistributor
  • 初始化cpu interface,每个core都有自己的cpu interface
  • 初始化virtual interface,每个core都有自己的virtual interface

初始化Distributor

//gicv3.c

/* 读取GICD_CTLR寄存器的bit[31],确认GICD_CTLR寄存器写入是否有效*/
static inline void gic_dist_wait_for_rwp(void)
{
while((GICD_READ32(GICD_CTLR) >> 31) & 0x1);
}

static void gic_dist_init(void)
{
u32 gicd_type, gic_irqs;
s32 i, nr_regs;

/* Disable Distributor */
GICD_WRITE32(GICD_CTLR, 0);
gic_dist_wait_for_rwp();

gicd_type = GICD_READ32(GICD_TYPER);
/* 0x1f= 0b11111 计算支持的最大SPI*/
gic_irqs = ((gicd_type & 0x1F) + 1) * 32;
/* 用于计算控制所有中断所需的寄存器数量(每个寄存器控制 32 个中断),这里+31是向上取整。*/
nr_regs = (gic_irqs + 31) / 32;

for(i = 1; i < nr_regs; i++) {
/* Set interrupt is Non-secure Group 1 */
GICD_WRITE32(GICD_IGROUPR(i), ~((u32)(0))); // 中断分组寄存器写入全 1 表示将这些 SPI 中断分配到 非安全 Group 1
/* Disable all IRQs */
GICD_WRITE32(GICD_ICENABLER(i), ~((u32)(0))); //中断禁用寄存器,写入全 1 表示禁用所有这些 SPI 中断(初始化阶段默认禁用,避免误触发)。
/* Clear all active IRQs */
GICD_WRITE32(GICD_ICACTIVER(i), ~((u32)(0))); //清除活跃状态寄存器,写入全 1 表示清除这些中断的 “活跃” 状态(确保初始化前无残留活跃标记)。
/* Clear all active IRQs */
GICD_WRITE32(GICD_ICPENDR(i), ~((u32)(0))); //清除挂起状态寄存器,写入全 1 表示清除这些中断的 “挂起” 状态(确保初始化前无残留挂起标记)。
}

/* Set default trigger type for all spi as level triggered */
/* 配置中断触发方式 */
nr_regs = (gic_irqs + 15) / 16;
for(i = 1; i < nr_regs; i++) {
GICD_WRITE32(GICD_ICFGR(i), 0);
}

/* Before Enable Distributor, we need to wait RWP */
gic_dist_wait_for_rwp();

/* Enabel Distributor */
GICD_WRITE32(GICD_CTLR, (GICD_CTLR_ENABLE_G1A | GICD_CTLR_ENABLE_G1));
gic_dist_wait_for_rwp();
}

初始化Redistributor

/* 初始化Redistributor GICR_* */
static void gic_redist_init(void)
{
u32 waker, sre;
int cpu = coreid();

/************ gic restributor configuration **********/

/* Enable GIC Restributor */
waker = GICR_READ32(cpu, GICR_WAKER);
/*将 ProcessorSleep 位(bit [1])清零,表示处理器(PE, Processing Element)不处于且不进入低功耗状态。*/
GICR_WRITE32(cpu, GICR_WAKER, (waker & ~(1 << 1)));
/* 轮询 GICR_WAKER 的 ChildrenAsleep 位(bit [2]),等待其变为 0。*/
while(GICR_READ32(cpu, GICR_WAKER) & (1 << 2));

/* Configure SIGs and PPIs as non-secure Group 1 */
GICR_WRITE32(cpu, GICR_IGROUPR0, ~((u32)(0)));
GICR_WRITE32(cpu, GICR_IGRPMODR0, 0);
/* 禁用所有 SGI/PPI 中断 */
GICR_WRITE32(cpu, GICR_ICENABLER0, ~((u32)(0)));
/* 清除所有 SGI/PPI 中断的激活态 */
GICR_WRITE32(cpu, GICR_ICACTIVER0, ~((u32)(0)));
/* 清楚所有 SGI/PPI 中断的挂起态 */
GICR_WRITE32(cpu, GICR_ICPENDR0, ~((u32)(0)));

GICR_WRITE32(cpu, GICR_ICFGR1, 0);

return;
}

初始化cpu interface

/* 初始化cpu interface */
static void gic_icc_init(void)
{
u32 sre;
int cpu = coreid();
/************ cpu interface configuration ***********/

/* Enable sysrem register */
read_sysreg(sre, ICC_SRE_EL2);

/* bit 3: EL1 accesses to ICC_SRE_EL1 do not trap to EL2.
* bit 0: The System register interface to the ICH_* registers
* and the EL1 and EL2 ICC_* registers is enabled for EL2
*/
write_sysreg(ICC_SRE_EL2, sre | (1 << 3) | 1);

/* make sure ICC_SRE_EL2.SRE already set to 1, must? */
isb();

read_sysreg(sre, ICC_SRE_EL1);

/* The System register interface for the current Security state is enabled. */
/* 使能EL1下对系统寄存器的访问*/
write_sysreg(ICC_SRE_EL1, sre | 1);

/* Set the idle priority as the priority mask to allow all unterrupts */
/* 设置所有 ICC的中断都可触发*/
write_sysreg(ICC_PMR_EL1, 0xFF);

/* Set binary points, only for Group 1 */
/* 设置所有中断可抢占 */
write_sysreg(ICC_BPR1_EL1, 0x7);

/* Set EOImode as split mode */
write_sysreg(ICC_CTLR_EL1, (1 << 1));

/* Enable SGIs/PPIs as Ns-Group 1 */
write_sysreg(ICC_IGRPEN1_EL1, 1);

return;
}

初始化virtual cpu interface

// 初始化虚拟cpu interface接口
static void gic_hyp_init(void)
{
u64 vtr;
/* Virtual Group 1 interrupts are enabled. */
write_sysreg(ICH_VMCR_EL2, (1<<1));
/* Virtual CPU interface operation enabled. */
write_sysreg(ICH_HCR_EL2, (1<<0));
/* The number of implemented List registers - 1 */
read_sysreg(vtr, ICH_VTR_EL2);
gic_max_lrs = ((vtr & 0x1F) + 1);

return;
}

7.3 EL2中断处理

//el2_sync.c
void el2_irq_proc(void)
{
u32 iar, irq;
/* 获取ICC_IAR1_EL1寄存器的值 */
gicv3_ops.get_irq(&iar);
/* 取出中断ID的值 0x3ff =0b 11 1111 1111 */
irq = iar & 0x3FF;

LOG_INFO("EL2 IRQ Trapped, irq number is %d\n", irq);

switch(irq) {
case UART_IRQ_LINE:
pl011_irq_handler();
break;
default:
LOG_WARN("Unknow IRQ under EL2\n");
break;
}

gicv3_ops.hyp_eoi(irq);
}
  • el2_irq_proc用于在el2处理中断,这里写一个demo只支持串口中断
  • 需要把el2_irq_proc中断处理函数写入异常向量表

在中断异常中,我们需要通过 ICC_IAR1_EL1 寄存器获取中断号,然后根据中断号调用具体的中断处理,比如这里使用 pl011_irq_handler 来处理 PL011 的输入,将键盘的输入回显到屏幕上。

虽然这边没有真正设计 GICv3 的虚拟化编程,但是这是先完成一个小任务,那就是我们先将 EL0/EL1 状态下发生的中断异常也让他路由到 EL2,这样一来我们所有的 None-Secure Group 1 的中断都可以由 Hypervisor 管理器来处理,并统一由它来分发给具体的 VCPU 处理。

这里我们需要配置 HCR_EL2 寄存器中的 FMO 和 IMO 位位完成其中断的路由:

void hyper_setup()
{
/*
HCR_TSC : 控制虚拟机的 SMC(Secure Monitor Call)指令是否陷入 Hypervisor
HCR_RW : 决定虚拟机的执行状态是 AArch64 还是 AArch32
HCR_VM : 开启或关闭 Stage-2 地址转换
HCR_FMO : 控制快速中断(FIQ)是否路由到 Hypervisor EL2
HCR_IMO : 控制物理中断(IRQ)是否路由到 Hypervisor EL2
*/
u64 hcr = HCR_TSC | HCR_RW | HCR_VM | HCR_FMO | HCR_IMO;
LOG_INFO("Setting hcr_el2 to 0x%x and enable stage 2 address translation\n");
write_sysreg(hcr_el2, hcr);

LOG_INFO("Setting Vector Base Address Register for EL2\n");
write_sysreg(vbar_el2, (u64)hyper_vector);

isb();
}

7.4 中断虚拟化

关于中断的虚拟化我们从guest os出发,假设此时guest os正处于运行的状态此时有一个串口中断来临,我们会去访问gic的寄存器从而进行中断的处理,在 GICv3 结构中是不提供 Distributor 和 Redistributor 的虚拟化能力的,GICv3 只提供了 CPU Interface 的虚拟化能力,所以 Guest OS 对 GICD_*和 GICR_*设备的访问就需要使用我们之前提到的 MMIO 能力,而 Guest OS 在访问 CPU Interface 时寄存器 ICC_时,硬件会自动帮我们映射成为 ICV_,这样一来 ICC_寄存器就可以被 Hypervisor 独享用来管理物理中断了。

img

  • 在 Guest OS 中如果访问 GIC Distributor 以及 GIC Ristributor 时会由于 IPA 没有物理内存映射导致陷入到 EL2 中,然后 EL2 的 Hypervisor 根据访问的地址找到对应的 MMIO 设备并软件模拟其 Read/Write;
  • 在 Guest OS 中如果访问 GIC CPU Interface 系统寄存器,则在 HCR_EL2.IMO/FMO 使能的情况下会被映射为访问 ICV_* 寄存器;
  • 在 Hypervisor 中我们通过访问 ICC_* 系统寄存器来处理物理中断;

7.4.1 Guest os中断处理

首先需要在Guest os中也去配置gic寄存器,这里和EL2中Hypervisor配置gic一样,通过**gic_v3_init**函数实现,将会在main函数中调用,然后会去配置串口中断,串口中断的中断号为33,为SPI中断,即需要使能33号中断

// guest/main.c
int vm_primary_init()
{
pl011_init();
print_logo();

write_sysreg(vbar_el1, (unsigned long)vectable);

/* test code for wakeup vcore 1 */
smc_call((u64)0xc4000003, (u64)1, (u64)_start);
/* 初始化gic控制器,会陷入EL2的mmio */
gic_v3_init();
/* 使能33号中断,边缘触发 */
guest_spi_config(UART_IRQ_LINE, GIC_EDGE_TRIGGER);
irq_enable;

while(1) {
printf("I am vm 1 on core %d\n", coreid());
for(int i=0; i < 100000000; i++);
}

return 0;
}

void guest_spi_config(u32 irq, u32 type)
{
gicv3_ops.set_affinity(irq, 0);
gicv3_ops.set_priority(irq, 0);
gicv3_ops.configure(irq, type);
gicv3_ops.unmask(irq);
}

/* Distributor 所有cpu共用 */
void gic_v3_init(void)
{
gic_dist_init();
gic_percpu_init();
}

#define GIC_LEVEL_TRIGGER (0) //
#define GIC_EDGE_TRIGGER (2) // 边沿触发

每个cpu核心都有自己的Restributor和CPU interface,因此在唤醒第二颗cpu核心时需要去使能,调用**gic_percpu_init**

int vm_secondary_init()
{
write_sysreg(vbar_el1, (unsigned long)vectable);
gic_percpu_init();
irq_enable;

while(1) {
printf("I am vm 1 on core %d\n", coreid());
for(int i=0; i < 100000000; i++);
}
}

vm_primary_init中通过如下指令设置了el1的异常向量表

write_sysreg(vbar_el1, (unsigned long)vectable);

其中el1的异常向量表如下:

.section ".text"

.global vectable
.balign 0x800
vectable:

b .
.balign 0x80
b .
.balign 0x80
b .
.balign 0x80
b .

.balign 0x80
b el1_sync
.balign 0x80
b el1_irq
.balign 0x80
b .
.balign 0x80
b .

.balign 0x80
b .
.balign 0x80
b .
.balign 0x80
b .
.balign 0x80
b .

.balign 0x80
b .
.balign 0x80
b .
.balign 0x80
b .
.balign 0x80
b .

el1_irq:
bl el1_sync
eret
  • 可以看见当在el1产生中断时会跳转到el1_irq,然后跳转到el1_sync处理
void el1_sync() {
u32 iar, irq;
gicv3_ops.get_irq(&iar);
irq = iar & 0x3ff;

printf("Guest EL1 IRQ Handler, irq is %d\n", irq);

switch(irq) {
case UART_IRQ_LINE:
pl011_irq_handler();
break;
default:
printf("Guest EL1 IRQ unknow irq number\n");
break;
}

gicv3_ops.guest_eoi(irq);
}

在这里会读取中断号然后判断是不是串口中断,如果是就进行处理

7.4.2 GIC模拟

在Xhyper中创建两个结构体来抽象Distributor/Redistributor

/* One irq configuration */
struct vgicv3_irq_config {
u8 priority;
u8 affinity;
u8 enabled;
u8 group;
};

/* virtual gic Distributor */
struct vgicv3_dist {
u32 nspis;
u8 enabled;
struct vgicv3_irq_config *spis; //虚拟SPI中断配置
spinlock_t lock;
};

/* virtual gic Redistributor*/
struct vgicv3_cpu {
u16 used_lr;
struct vgicv3_irq_config sgis[GIC_NSGI]; // 虚拟SGI中断配置
struct vgicv3_irq_config ppis[GIC_NPPI]; // 虚拟PPI中断配置
};
  • Redistributor管理着sgi和ppi中断,且每个cpu核心上都挂着一个Redistributor,因此我们需要在每创建一个vcpu时,对应创建一个虚拟的Redistributor
  • Distributor管理着SPI中断,所有的cpu核心共用,因此对于一个vm只需要创建一个虚拟的Distributor即可
struct vgicv3_dist *create_vgic_dist(struct vm *vm)
{
struct vgicv3_dist *vgic_dist = (struct vgicv3_dist *)xmalloc(sizeof(struct vgicv3_dist));
if(vgic_dist == NULL) {
abort("Unable to alloc vgic_dist, no memory");
}

/* 0 ~ 31 for SGIs and PPIs */
vgic_dist->nspis = gic_max_spi - 31;
vgic_dist->enabled = 0;
vgic_dist->spis = (struct vgicv3_irq_config *)xmalloc(vgic_dist->nspis);
if(vgic_dist->spis == NULL) {
abort("Unable to alloc vgic_dist->spis, no memory");
}

create_mmio_trap(vm, GICD_BASE, GICD_SIZE, vgicd_read, vgicd_write);
create_mmio_trap(vm, GICR_BASE, GICR_SIZE, vgicr_read, vgicr_write);

return vgic_dist;
}

/* Alloc and initialize a virtual gic Distributor */
struct vgicv3_cpu *create_vgic_cpu(int vcpuid)
{
struct vgicv3_cpu *vgic_cpu = (struct vgicv3_cpu *)xmalloc(sizeof(struct vgicv3_cpu));
if(vgic_cpu == NULL) {
abort("Unable to alloc vgic_cpu, no memory");
}

vgic_cpu->used_lr = 0;

for(struct vgicv3_irq_config *v = vgic_cpu->sgis; v < &vgic_cpu->sgis[GIC_NSGI]; v++) {
v->enabled = 0;
v->affinity = 1 << vcpuid;
v->group = 1;
}

for(struct vgicv3_irq_config *v = vgic_cpu->ppis; v < &vgic_cpu->ppis[GIC_NSGI]; v++) {
v->enabled = 0;
v->affinity = 1 << vcpuid;
v->group = 1;
}

return vgic_cpu;
}
  • 在vgicv3_dist时需要配置GICD和GICR的MMIO,这样EL1的VM在访问这两组寄存器的时候可以被陷入到EL2的异常处理中,并调用对应的函数来模拟对这些寄存器的读写,因此可以看见在vgicv3_dist 函数中调用了create_mmio_trap函数来配置产生异常时的读写函数
  • 总共有:vgicd_read 、vgicd_write、vgicr_read、vgicr_write四个函数用于模拟对GICD和GICR的读写操作

7.4.3 虚拟中断注入

这里的虚拟中断指的是由CPU interface管理的中断LPI,因为GICV3只提供了CPU interface的虚拟化能力,具体的中断注入流程如下:

  • step1: 外设中断到达 GIC,并通过 Physical CPU Interface 发送一个物理中断到 CPU

img

  • Step2:由于我们配置了 HCR_EL2.IMO=1,所以即使在 EL0/EL1 状态下发生的中断也会使 CPU 陷入 EL2,在 Hypversior 中我们通过 ICC_IAR1 响应中断,并获取中断的相关信息

img

  • Step3:Hypervisor 往 GIC List Register x 中注册一个虚拟 IRQ,设置初始状态位为 PENDING,设置其对应的物理中断号和虚拟中断号,在我们的设计中将虚拟中断号设置为和物理中断号相等(可以不相等)

img

Step4:当该 VCPU 被切换并运行到 EL1/ELO 的 Guest OS 时,GIC Virtual CPU Interface 会触发一个虚拟 IRQ 信号到 CPU

img

  • Step5:因为 VIRQ 的信号,CPU 会进入 Virtual IRQ Exception,Guest OS 通过 Virtual CPU Interface 完成 EOI/DIR 等操作

具体的代码如下,首先是Hypervisor收到物理中断,我们定义el1_irq_proc函数来处理来自EL1的中断:

/* 处理来自EL1的中断 */
void el1_irq_proc(void)
{
u32 iar, irq;
struct vcpu *vcpu;
read_sysreg(vcpu, tpidr_el2);
virq_enter(vcpu);

gicv3_ops.get_irq(&iar);
irq = iar & 0x3FF;

LOG_INFO("el1 irq proc\n");

gicv3_ops.guest_eoi(irq);

/* set pirq equal to virq */
virq_inject(vcpu, irq, irq);
}


/*
* 处理EL1下的中断结束,写入ICC_EOIR1_EL1寄存器,标志着结束
* 这个函数在EL2下被调用
*/
static void gic_guest_eoi(u32 irq)
{
write_sysreg(ICC_EOIR1_EL1, irq);
}
  • 在中断异常的处理函数中我们首先获取发生中断的中断号然后通过 guest_eoi 来完成运行优先级降权,在 Hypervisor 中我们需要设置 EOIMODE = 1,使得写入 ICC_EOIR1_EL1 只会完成优先级降权,但不是 Deactivation 中断,只有当 Guest OS 处理完虚拟中断后去 Deactivation 该虚拟中断时,才会把对应的物理中断给 Deactivation。
  • 在配置icc时会设置EOIMODE: write_sysreg(ICC_CTLR_EL1, (1 << 1));
  • 然后调用virq_inject来进行虚拟中断的注入
int virq_inject(struct vcpu *vcpu, u32 pirq, u32 virq)
{
struct vgicv3_cpu *vgic_cpu = vcpu->vgic_cpu;
u64 lr = gic_create_lr(pirq, virq);
int n = alloc_lr(vgic_cpu);
if(n >= 0) {
gic_write_list_reg(n, lr);
} else {
abort("No List Register");
}

return 0;
}
  • 我们首先通过 gic_create_lr 来构建需要写入 lr 寄存器的值,包括初始化其物理中断号,虚拟中断号等。然后我们找到一个未被使用的 lr 寄存器,并将上述初始化好的 lr 的值写入 lr 寄存器中
//写入ICH_LR<n>_EL2寄存器的数据
void gic_write_list_reg(int n, u64 val)
{
switch(n) {
case 0: write_sysreg(ICH_LR0_EL2, val); break;
case 1: write_sysreg(ICH_LR1_EL2, val); break;
case 2: write_sysreg(ICH_LR2_EL2, val); break;
case 3: write_sysreg(ICH_LR3_EL2, val); break;
case 4: write_sysreg(ICH_LR4_EL2, val); break;
case 5: write_sysreg(ICH_LR5_EL2, val); break;
case 6: write_sysreg(ICH_LR6_EL2, val); break;
case 7: write_sysreg(ICH_LR7_EL2, val); break;
case 8: write_sysreg(ICH_LR8_EL2, val); break;
case 9: write_sysreg(ICH_LR9_EL2, val); break;
case 10: write_sysreg(ICH_LR10_EL2, val); break;
case 11: write_sysreg(ICH_LR11_EL2, val); break;
case 12: write_sysreg(ICH_LR12_EL2, val); break;
case 13: write_sysreg(ICH_LR13_EL2, val); break;
case 14: write_sysreg(ICH_LR14_EL2, val); break;
case 15: write_sysreg(ICH_LR15_EL2, val); break;
default: abort("Unknow ICH LR number");
}
}