7.1 GICV3
寄存器
7.2 GIC初始化
根据上面这张图可知,对GIC的初始化分为四个步骤
初始化Distributor,Distributor是所有core共有的
初始化Redistributor,每个core都有自己的Redistributor
初始化cpu interface,每个core都有自己的cpu interface
初始化virtual interface,每个core都有自己的virtual interface
初始化Distributor
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; GICD_WRITE32 (GICD_CTLR, 0 ); gic_dist_wait_for_rwp (); gicd_type = GICD_READ32 (GICD_TYPER); gic_irqs = ((gicd_type & 0x1F ) + 1 ) * 32 ; nr_regs = (gic_irqs + 31 ) / 32 ; for (i = 1 ; i < nr_regs; i++) { GICD_WRITE32 (GICD_IGROUPR (i), ~((u32)(0 ))); GICD_WRITE32 (GICD_ICENABLER (i), ~((u32)(0 ))); GICD_WRITE32 (GICD_ICACTIVER (i), ~((u32)(0 ))); GICD_WRITE32 (GICD_ICPENDR (i), ~((u32)(0 ))); } nr_regs = (gic_irqs + 15 ) / 16 ; for (i = 1 ; i < nr_regs; i++) { GICD_WRITE32 (GICD_ICFGR (i), 0 ); } gic_dist_wait_for_rwp (); GICD_WRITE32 (GICD_CTLR, (GICD_CTLR_ENABLE_G1A | GICD_CTLR_ENABLE_G1)); gic_dist_wait_for_rwp (); }
初始化Redistributor
static void gic_redist_init (void ) { u32 waker, sre; int cpu = coreid (); waker = GICR_READ32 (cpu, GICR_WAKER); GICR_WRITE32 (cpu, GICR_WAKER, (waker & ~(1 << 1 ))); while (GICR_READ32 (cpu, GICR_WAKER) & (1 << 2 )); GICR_WRITE32 (cpu, GICR_IGROUPR0, ~((u32)(0 ))); GICR_WRITE32 (cpu, GICR_IGRPMODR0, 0 ); GICR_WRITE32 (cpu, GICR_ICENABLER0, ~((u32)(0 ))); GICR_WRITE32 (cpu, GICR_ICACTIVER0, ~((u32)(0 ))); GICR_WRITE32 (cpu, GICR_ICPENDR0, ~((u32)(0 ))); GICR_WRITE32 (cpu, GICR_ICFGR1, 0 ); return ; }
初始化cpu interface
static void gic_icc_init (void ) { u32 sre; int cpu = coreid (); read_sysreg (sre, ICC_SRE_EL2); write_sysreg (ICC_SRE_EL2, sre | (1 << 3 ) | 1 ); isb (); read_sysreg (sre, ICC_SRE_EL1); write_sysreg (ICC_SRE_EL1, sre | 1 ); write_sysreg (ICC_PMR_EL1, 0xFF ); write_sysreg (ICC_BPR1_EL1, 0x7 ); write_sysreg (ICC_CTLR_EL1, (1 << 1 )); write_sysreg (ICC_IGRPEN1_EL1, 1 ); return ; }
初始化virtual cpu interface
static void gic_hyp_init (void ){ u64 vtr; write_sysreg (ICH_VMCR_EL2 , (1 <<1 )); write_sysreg (ICH_HCR_EL2 , (1 <<0 )); read_sysreg (vtr, ICH_VTR_EL2 ); gic_max_lrs = ((vtr & 0x1F ) + 1 ); return ; }
7.3 EL2中断处理 void el2_irq_proc (void ) { u32 iar, irq; gicv3_ops.get_irq (&iar); 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 () { 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 独享用来管理物理中断了。
在 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号中断
int vm_primary_init () { pl011_init (); print_logo (); write_sysreg (vbar_el1, (unsigned long )vectable); smc_call ((u64)0xc4000003 , (u64)1 , (u64)_start); gic_v3_init (); 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); } 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
struct vgicv3_irq_config { u8 priority; u8 affinity; u8 enabled; u8 group; }; struct vgicv3_dist { u32 nspis; u8 enabled; struct vgicv3_irq_config *spis; spinlock_t lock; }; struct vgicv3_cpu { u16 used_lr; struct vgicv3_irq_config sgis[GIC_NSGI]; struct vgicv3_irq_config ppis[GIC_NPPI]; };
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" ); } 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; } 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
Step2:由于我们配置了 HCR_EL2.IMO=1,所以即使在 EL0/EL1 状态下发生的中断也会使 CPU 陷入 EL2,在 Hypversior 中我们通过 ICC_IAR1 响应中断,并获取中断的相关信息
Step3:Hypervisor 往 GIC List Register x 中注册一个虚拟 IRQ,设置初始状态位为 PENDING,设置其对应的物理中断号和虚拟中断号,在我们的设计中将虚拟中断号设置为和物理中断号相等(可以不相等)
Step4:当该 VCPU 被切换并运行到 EL1/ELO 的 Guest OS 时,GIC Virtual CPU Interface 会触发一个虚拟 IRQ 信号到 CPU
Step5:因为 VIRQ 的信号,CPU 会进入 Virtual IRQ Exception,Guest OS 通过 Virtual CPU Interface 完成 EOI/DIR 等操作
具体的代码如下,首先是Hypervisor收到物理中断,我们定义el1_irq_proc函数来处理来自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); virq_inject (vcpu, irq, irq); } 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 寄存器中
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" ); } }