jailhouse中断虚拟化机制

目标

分析基于jailhouse虚拟化的中断虚拟化机制。主要涉及虚拟中断、虚拟中断注入相关内容。学习和交流。

代码分析环境:

  • arm64架构
  • linux-5.10
  • jailhouse-0.12

分为以下几部分

  • 一 总体
    • jailhouse虚拟化概述
    • 硬件基础:GIC中断控制器概述
    • 中断虚拟化概述
  • 二 [流程1] 虚拟中断初始化
    • [流程1.1] 虚拟化监视器: 设置EL2中断向量表
    • [流程1.2] 虚拟化监视器: 通过ICH_HCR_EL2设置启用虚拟中断
    • [流程1.3] 虚拟机创建: 通过HCR_EL2设置对物理中断拦截
  • 三 [流程2] 虚拟中断注入
    • [流程2.1] EL2中断注入处理流程
      • [流程2.1.1] 本核中断注入
      • [流程2.1.2] 跨核中断注入
    • [流程2.2] 中断注入ICH_LRn_EL2寄存器操作
  • 三 附录
    • 虚拟中断相关寄存器
      • ICH_VTR_EL2
      • ICH_HCR_EL2
      • HCR_EL2
      • ICH_ELRSR_EL2
      • ICH_LRn_EL2

一 总体

jailhouse虚拟化概述

虚拟化,或虚拟机,并不是一个新的概念。它是“云计算”的技术基础。(但在16年前,它确实是一个比较新的技术。那时所有人都在讨论云计算,所有技术人员都在讨论虚拟化。情形大致和今天所有人都讨论AI一样)

jailhouse是一种虚拟化软件。提及虚拟化,更多的被想到的是qemu/kvm。但实际上,qemu只是虚拟化软件的一种,在应用上基本上统一了服务器虚拟化,更早的xen实际上已退出了竞争。但说到底,虚拟化技术的应用只是分场景,并无绝对的技术好与坏。qemu可能是一种比较好的服务器虚拟化,但是如果是嵌入式,qemu就不那么适用了。同理,linux是一种主导地位的服务器内核,但是同样在嵌入式linux无论如何提供不了高实时性要求的支持。

jailhouse是一种面向嵌入式的虚拟化,一个典型场景是基于虚拟化在单板上组建Linux和实时RTOS并行的双侧内核系统。在代码量功能复杂度上,jailhouse属于一种绝对轻量级的虚拟化,对比于kvm尤其如此。它的特点可先总结一下:它没有那么多的功能,但是在特定的方面,因为轻量实现其运行效率或者说性能远优于传统虚拟化。

虚拟化技术是一个大的范畴,至少包括有多个层面,包括:CPU虚拟化、中断虚拟化、内存虚拟化、IO虚拟化。中断虚拟化是虚拟化技术的一个方面。最后一点说明的是,本文讨论的jailhouse中断虚拟化机制,不完全是jailhouse的专属机制。中断虚拟化根本上是一种硬件机制,即使当单独去讨论qemu/kvm的中断虚拟化时,尽管实现有不同,在根本的原理上是一致的。

jailhouse虚拟化下虚拟机启动大体过程包括:

  1. 加载和启用jailhouse虚拟化监视器(Hypervisor)内核本身。
  2. 虚拟机启动前vcpu的初始化。

和传统虚拟化Xen不一样,jailhouse启动流程是引导加载Linux主系统,基于Linux动态完成jailhouse的加载和启用。在jailhouse加载和启用之后,虚拟化环境已建立,再以CPU分区方式启动新虚拟机,即执行虚拟机各vcpu的初始化流程。

jailhouse区别于kvm虚拟化的另一个重要方面在于它是所谓“静态分区”虚拟化,简单说就是没有vcpu的调度,虚拟机的CPU核心是绑定在一个物理核上的。这是它相比于kvm等传统服务器虚拟化技术低开销的关键。

硬件基础:GIC中断控制器

首先,arm64下对中断的管理是基于所谓“GIC中断控制器”硬件的,没有GIC,CPU不可能响应和处理中断。GIC根本上是一种设备,总体逻辑上和磁盘,网卡没有根本区别,独立于CPU存在,通过硬件连线和CPU连接。当然,相比于一般外设,它和CPU连接更紧密,从某种程度可以理解为cpu和GIC是一体的。

中断虚拟化,是虚拟化下基于GIC硬件实现的一种虚拟机中断工作机制。为实现中断虚拟化,虚拟化监视层(hypervisor)需要对GIC进行“配置”。需说明,对于GIC的配置包括很多层面,虚拟化层仅完成一方面的工作。对于中断源的配置,例如,对于某一个特定中断, 启用或禁用,配置类型属于边缘触发(edge triggered)或是电平触发(level triggered),中断优先级等这些仍然是由操作系统OS本身完成的。

先简要描述GIC结构和硬件中断基础。arm下GIC有多个硬件实现版本,以下以gicv3进行说明。GICv3整体结构包括三个部分:

  1. GICD分发器。
  2. GICR重分发器
  3. GIC CPU interface。CPU接口。

绘图3_1

图1 arm64 GIC控制器结构示意

上述第一和第二组件实现为IO内存访问,第三组件实现为CPU寄存器访问,具体为ICC。

同时,arm64下管理的硬件中断,按属性分为三类:

  1. SGI(或IPI)核间中断;中断号从0到15,共16个。
  2. PPI私有中断;中断号从16到31,共16个。
  3. SPI共享中断;中断号从32往后扩展。

第一类型核间中断,用于cpu核间相互激发的中断;第二类型私有中断,是中断源和本地核心绑定的中断,典型的是timer时钟中断;第三类型就是我们通常说的外设中断,irq。

对于不同类型的中断,GIC是通过不同结构去配置的。有如下分工:

  • GICD分发器负责配置第三类型共享中断。配置工作包括:启用、禁用中断源;设置中断源触发类型,边缘还是电平触发,中断优先级等。中断的路由
  • GICR重分器负责配置第一和第二类型中断,即核间中断和私有中断。(配置工作同上,除了中断路由)
  • GIC CPU接口负责决定本CPU的中断信号屏蔽,是否响应由GICD和GICR传递的任何中断信号,确认中断源信号,使完成中断等操作改变中断处理周期所处的状态。

为支持中断虚拟化,GIC提供了虚拟化扩展。主要是在中断控制器CPU接口多提供了一组虚拟CPU接口。虚拟CPU接口用以支持虚拟中断。

对应arm64下一个中断源分为四种状态:

  1. 中断未激活(inactive)。就是中断的初始状态,实际上就是没有中断发生。
  2. 中断悬起(pending)。中断信号产生了,但是还没有得到CPU确认。
  3. 中断激活(active)。悬起的中断得到CPU确认后,进入激活状态。表示CPU正在处理该中断源。
  4. 中断激活同时悬起(active and pending)。该状态表示,CPU在处理某一个中断源时,另外一个同型中断源信号又来了。

图,四种状态转换关系。

操作系统OS对于中断的管理任务,就是通过配置GIC的包括GICD、GICR、ICC_各组件,完成对使用设备的中断源配置。

中断虚拟化概述

中断虚拟化,提供了一种有别于物理中断的“虚拟中断”机制,使虚拟化下虚拟机进行基于虚拟中断的中断处理,而不是基于物理中断。为什么要这么做,在虚拟化下实施中断虚拟化?

根本原因是,虚拟化下引入了虚拟机的vcpu调度。同一时刻物理CPU上可能运行不同的虚拟机vcpu,某一时刻的物理中断信号无法确定性投递到指定虚拟机vcpu。例如有以下虚拟机配置。

物理 CPU 0
├── vCPU 0 (虚拟机 0)
├── vCPU 1 (虚拟机 1)
├── vCPU 2 (虚拟机 2)
└── vCPU 3 (虚拟机 3)
如果某一时刻产生一网卡irq信号,需要投递给虚拟机2。因为物理中断信号是投递给物理CPU的,这产生了两个问题:一是目标虚拟机运行在哪个CPU上?假如是运行在CPU0;二是把中断信号投递到CPU0上,虚拟机2一定能接收吗?不一定,因为虚拟化虚拟机vcpu是随时调度的,在这一时刻可能被调度走,cpu0上实际有可能在运行虚拟机1。

中断虚拟化对上述问题的解决途径是,基于硬件上提供的“虚拟中断”机制,只在CPU“刚好”被调度运行时,投递中断信号。

虚拟中断响应全过程有:

  1. 设备:产生某个硬件中断, GIC接收;
  2. GIC控制器:转送中断信号到物理CPU
  3. 虚拟化监视器:“截获”实际发生的物理中断, EL2响应;将物理中断信号“转换”成虚拟中断,写入vcpu中断注入寄存器;
  4. 在虚拟机vcpu由中断返回执行时,响应虚拟中断信号。

上述第三步,即所谓“中断注入”,它实际完成了向虚拟机虚拟中断信号的投递。需注意,中断虚拟化的一个关键特征是虚拟化监视器层对物理中断信号实施了“拦截”操作。硬件底层对虚拟机响应虚拟中断的关键是,正好在虚拟机vcpu返回到EL1级别时,直接由硬件控制产生一个针对虚拟机vcpu的中断信号,这个中断信号是只针对虚拟CPU接口的,不会由EL2拦截。以虚拟机视角(或称为客户机guest),虚拟中断信号等同于物理中断信号,虚拟机认为接收到了一个真实的物理中断;但对于宿主机视角,该中断信号是基于虚拟中断生成,非物理中断。

以上,对于虚拟中断注入的全过程,可理解为一个“信件投递”的过程,虚拟机,准确说是虚拟CPU相当于维护了一个“邮箱”。宿主机把虚拟中断“寄存”在邮箱里,在虚拟机虚拟CPU返回运行的第一时间,从邮箱里取出虚拟中断信号,识别该信号。简单说,虚拟中断注入的设计出发点在于“寄存”中断信号。这是中断虚拟化的第一个关键特征。

中断虚拟化的另外一个方面,在于提供了虚拟机对于GIC中断控制器的直接操作;因为虚拟机是需要操作GIC控制器的,传统虚拟化方法并不支持虚拟机直接操作GIC中断控制器,此时需要采用设备模拟的方法,对于中断控制器每一个操作例如ACK确认和EIO完成操作都需要EL2虚拟化监视器去做模拟。但是arm64下,GIC提供了虚拟化扩展,提供了虚拟机可以直接操作的中断控制器虚拟CPU接口,该接口独立于中断控制器物理CPU。通过提供中断控制器虚拟机独立操作接口,降低了中断虚拟化的控制器读写操作开销。这是中断虚拟化的第二个关键特征。

中断虚拟化的全过程包含两个大的流程:

  1. 虚拟中断初始化。
  2. 虚拟中断注入

流程1可认为是一个配置过程,配置目标包括虚拟化专用寄存器和GIC相关。流程2是虚拟中断的工作过程。需注意,对于虚拟中断的配置,是基于CPU核心的初始化进行的,这是说,如果系统上有8个核,则需依次按核心做8次操作。

二 [流程1] 虚拟中断初始化

虚拟机虚拟中断的初始化流程, 是在jailhouse本身加载及虚拟机启动流程中进行的。且是按照每CPU核心进行设置的。

[流程1.1] 虚拟化监视器: 设置EL2中断向量表

该流程发生在jailhouse加载和启用中。代码流程起始于 arch_entry @hypervisor/arch/arm64/entry.S。发生在基于主Linux系统的向虚拟机的动态转换中。总体完成设置虚拟化监视器内核的中断向量表。主要代码有
arch_entry:

	// ... ...

	/* install bootstrap_vectors */
	ldr	x1, =bootstrap_vectors						// → ***位置1***
	virt2phys x1

	/* choose opcode */
	mov	x0, #LINUX_HVC_SET_VECTORS
	cmp	x18, #HYP_STUB_ABI_LEGACY
	b.ne	1f
	mov	x0, #LINUX_HVC_SET_VECTORS_LEGACY
1:
	hvc	#0

	hvc	#0	/* bootstrap vectors enter EL2 at el2_entry */
	b	.	/* we don't expect to return here */

	/* the bootstrap vector returns us here in physical addressing */
el2_entry:
	mrs	x1, esr_el2
	lsr	x1, x1, #26
	cmp	x1, #0x16
	b.ne	.		/* not hvc */

	/* init bootstrap page tables */
	bl	init_bootstrap_pt

	/* enable temporary mmu mapings for early initialization */
	adr	x0, bootstrap_pt_l0
	adr	x30, 1f		/* set lr manually to ensure... */
	phys2virt x30		/* ...that we return to a virtual address */
	b	enable_mmu_el2
1:
	/* install the final vectors */
	adr	x1, hyp_vectors								// → ***位置2***
	msr	vbar_el2, x1

	// ... ...

该实现主要包括,在代码位置1,EL1级别时建立临时向量表bootstrap_vectors,后触发hvc指令主动陷入EL2。在EL2中启用MMU后,在代码位置2中,设置最终向量表hyp_vectors。

hyp_vectors向量表结构如下所示

	.align 11
hyp_vectors:
	ventry	.
	ventry	.
	ventry	.
	ventry	.

	handle_vmexit arch_el2_abt
	ventry	.
	ventry	.
	ventry	.

	handle_vmexit arch_handle_trap
	handle_vmexit irqchip_handle_irq
	ventry	.
	ventry	.

	handle_vmexit arch_handle_trap
	handle_vmexit irqchip_handle_irq
	ventry	.
	ventry	.

上述向量表第3节,arch_handle_trap和irqchip_handle_irq分别用于响应“截获”自EL1的异常和中断信号。虚拟中断的投递过程,即起始于irqchip_handle_irq。

[流程1.2] 虚拟化监视器: 通过ICH_HCR_EL2设置启用虚拟中断

该流程仍属于虚拟化监视器加载过程。在完成中断向量表设置后。有以下调用链

entry [@hypervisor/arch/arm64/entry.S]
└─ init_early
└─ cpu_init [@hypervisor/setup.c]
	└─ arch_cpu_init [@hypervisor/arch/arm64/setup.c]
		└─ arm_cpu_init [@hypervisor/arch/arm-common/setup.c]
			└─ irqchip_cpu_init = gicv3_cpu_init [@hypervisor/arch/arm-common/irqchip.c]
└─ init_late

主要实现函数为gicv3_cpu_init @hypervisor/arch/arm-common/gic-v3.c。关键代码局部如下

static int gicv3_cpu_init(struct per_cpu *cpu_data)
{
	unsigned int mnt_irq = system_config->platform_info.arm.maintenance_irq;
	unsigned long redist_addr = system_config->platform_info.arm.gicr_base;
	unsigned long redist_size = GIC_V3_REDIST_SIZE;
	void *redist_base = gicr_base;
	unsigned long gicr_ispendr, gicr_isacter;
	unsigned int n;
	void *gicr;
	u64 typer, mpidr;
	u32 pidr, aff;
	u32 cell_icc_ctlr, cell_icc_pmr, cell_icc_igrpen1;
	u32 ich_vtr;
	u32 ich_vmcr;
	
	
	// ... ...

	/* Ensure all IPIs and the maintenance PPI are enabled. */
	//  → ***位置1***
	gicr = redist_base + GICR_SGI_BASE;
	mmio_write32(gicr + GICR_ISENABLER, 0x0000ffff | (1 << mnt_irq));

	arm_read_sysreg(ICC_CTLR_EL1, cell_icc_ctlr);
	arm_write_sysreg(ICC_CTLR_EL1, ICC_CTLR_EOImode);

	arm_read_sysreg(ICC_PMR_EL1, cell_icc_pmr);
	arm_write_sysreg(ICC_PMR_EL1, ICC_PMR_DEFAULT);

	arm_read_sysreg(ICC_IGRPEN1_EL1, cell_icc_igrpen1);
	arm_write_sysreg(ICC_IGRPEN1_EL1, ICC_IGRPEN1_EN);

	arm_read_sysreg(ICH_VTR_EL2, ich_vtr);
	gic_num_lr = (ich_vtr & 0xf) + 1;
	gic_num_priority_bits = (ich_vtr >> 29) + 1;

	gicv3_clear_pending_irqs();

	ich_vmcr = (cell_icc_pmr & ICC_PMR_MASK) << ICH_VMCR_VPMR_SHIFT;
	if (cell_icc_igrpen1 & ICC_IGRPEN1_EN)
		ich_vmcr |= ICH_VMCR_VENG1;
	if (cell_icc_ctlr & ICC_CTLR_EOImode)
		ich_vmcr |= ICH_VMCR_VEOIM;
	arm_write_sysreg(ICH_VMCR_EL2, ich_vmcr);

	/* After this, the cells access the virtual interface of the GIC. */
	arm_write_sysreg(ICH_HCR_EL2, ICH_HCR_EN); 		// 	→ ***位置2***
	
	
	// ... ...	
}

从代码位置1,该函数先完成:

  • 通过GICR设置启用所有核间(IPI)和私有(PPI)中断
  • 通过ICC_CTLR_EL1设置EOI模式为1,即虚拟机对中断控制器的EOI操作,仅引起priority drop优先级降级,不引起对中断状态清激活()。
  • 通过ICC_PMR_EL1设置默认的虚拟机中断屏蔽
  • 通过ICC_IGRPEN1_EL1启用对组1中断的响应。
  • 通过ICH_VTR_EL2获得中断注入用的列表寄存器的基本信息,包括可用列表寄存器个数,可用的优先级位数。

代码位置2,最后的关键操作是通过ICH_HCR_EL2设定启用虚拟中断。在该操作之后,虚拟机于本核上可以响应由中断注入投递的虚拟中断信号。简单说,启用了中断注入。

[流程1.3] 虚拟机创建: 通过HCR_EL2设置对物理中断拦截

虚拟中断配置的最后一步是设置中断拦截,通过HCR_EL2寄存器。HCR_EL2是arm64下虚拟中断控制的最核心寄存器,用于虚拟化全局控制。

这一步做完以后,虚拟中断的中断注入全过程可以正常工作了。此处先回顾一下虚拟中断响应全过程:
1.物理中断发生 → 2.EL2拦截进入el2中断向量表 → 3. 虚拟化监视器做转换后中断注入 → 4. 虚拟机响应注入中断

本流程首先在虚拟化监视器加载过程设置初始值,后在虚拟机vcpu重置引导前继承该值完成最终写入。

虚拟化监视器加载过程HCR_EL2。arch_cpu_init [@hypervisor/arch/arm64/setup.c]

int arch_cpu_init(struct per_cpu *cpu_data)
{
	// → ***位置1***
	unsigned long hcr = HCR_VM_BIT | HCR_IMO_BIT | HCR_FMO_BIT
				| HCR_TSC_BIT | HCR_TAC_BIT | HCR_RW_BIT;
	int err;

	/* switch to the permanent page tables */
	enable_mmu_el2(paging_hvirt2phys(cpu_data->pg_structs.root_table));

	err = arm_cpu_init(cpu_data);
	if (err)
		return err;

	// ... ...

	/* Setup guest traps */
	// → ***位置2***
	arm_write_sysreg(HCR_EL2, hcr);
	
	// ... ...
}

上述代码位置1设置HCR_EL2寄存器初值,此处重要的是HCR_IMO_BIT标志位,该标志控制物理irq中断截获,路由到EL2。(HCR_EL2寄存器具体信息参见附录)。最终在代码位置2,完成寄存器写入。

后续在虚拟机启动时进入arm_cpu_reset @hypervisor/arch/arm64/control.c。完成最终的HCR_EL2设定

void arm_cpu_reset(unsigned long pc, bool aarch32)
{

	// ... ...
/* wipe timer registers */
	arm_write_sysreg(CNTP_CTL_EL0, 0);
	arm_write_sysreg(CNTP_CVAL_EL0, 0);
	arm_write_sysreg(CNTP_TVAL_EL0, 0);
	arm_write_sysreg(CNTV_CTL_EL0, 0);
	arm_write_sysreg(CNTV_CVAL_EL0, 0);
	arm_write_sysreg(CNTV_TVAL_EL0, 0);

	arm_read_sysreg(HCR_EL2, hcr_el2);	// → ***位置1***
	if (aarch32) {
		arm_write_sysreg(SPSR_EL2, RESET_PSR_AARCH32);
		hcr_el2 &= ~HCR_RW_BIT;
	} else {
		arm_write_sysreg(SPSR_EL2, RESET_PSR_AARCH64);
		hcr_el2 |= HCR_RW_BIT;
	}
	arm_write_sysreg(HCR_EL2, hcr_el2);		// → ***位置2***

	arm_write_sysreg(ELR_EL2, pc);
	
	// ... ...	
}

位置1中,继承了虚拟化监视器加载时的初始值,进一步在arm64下加入HCR_RW_BIT,完成最终写入。

另外值得注意,开启HCR_IMO_BIT物理irq路由后,虚拟机对GICv3 cpu接口ICC_寄存器的访问自动重定向到ICV_寄存器。该过程是硬件自动完成的。与之对比,gicv2要求客户机中断子系统代码改变cpu接口访问地址,而gicv3提供了自动重定向功能,客户机相应部分代码不需要变动。这提供了代码迁移适配便利性。

二 [流程2] 虚拟中断注入

如上所述,jailhosue拦截了虚拟机的所有物理irq中断。基于此,其虚拟中断注入全过程包括: EL2中断处理框架响应拦截中断,做实际中断注入的准备操作(流程2.1); 通过硬件寄存器完成中断注入的底层操作(流程2.2)。

先总体描述中断注入的底层操作。中断注入的底层操作,是虚拟中断信号投递至虚拟机运行所在CPU的过程,这是一个通过寄存器操作的过程。底层关键操作是基于gicv3中断控制器虚拟化扩展的ICH_LRn_EL2寄存器(参见附录),包含多个寄存器实例,每个实例表示一个虚拟中断。因此可以理解,同一时刻投递到虚拟机vcpu的中断信号可以有多个,而不是一个,这符合真实物理中断的情形。

gicv3控制器对虚拟中断底层的关键处理是

需注意到,列表寄存器的有限的,arm64下通常只有8组。因此可能会出现需要注入中断,但寄存器实例已用满不够用的情况,这种情况下,需要软件配合处理。

[流程2.1] EL2中断注入处理流程

jailhouse的EL2中断处理需处理两个类型的中断注入:
  1. 本核中断(Local interrupt)注入
  2. 跨核中断注入,即非本核中断。

绘图3_2
图2 本核中断注入整体流程示意

绘图3_3

图3 跨核中断注入整体流程示意

本核中断,实际上就是指非核间中断,普通的irq中断。如前所述,jailhouse是一种静态分区虚拟化,默认无虚拟设备,设备实际上是直通模式。因此本核中断是主要过程,表示一个针对虚拟机某CPU核的irq中断。远程中断实际上是核间中断。

[流程2.1.1] 本核中断注入

对于本核中断,有如下基本流程:

irqchip_handle_irq [hypervisor/arch/arm-common/irqchip.c]
└─ arch_handle_phys_irq [hypervisor/arch/arm-common/control.c]
└─ └─ irqchip.inject_irq = gicv3_inject_irq [hypervisor/arch/arm-common/gic-v3.c]

arch_handle_phys_irq是处理包括私有ppi中断和irq中断的入口。代码如下

bool arch_handle_phys_irq(u32 irqn, unsigned int count_event)
{
	struct public_per_cpu *cpu_public = this_cpu_public();

	if (irqn == system_config->platform_info.arm.maintenance_irq) {
		cpu_public->stats[JAILHOUSE_CPU_STAT_VMEXITS_MAINTENANCE] +=
			count_event;
		irqchip_inject_pending(); 	// ***位置1***

		return true;
	}

	cpu_public->stats[JAILHOUSE_CPU_STAT_VMEXITS_VIRQ] += count_event;
	irqchip_set_pending(cpu_public, irqn);   // ***位置2***

	return false;
}

irqn表示中断号,正常下是一个针对虚拟机CPU的irq中断, 走代码位置2,irqchip_set_pending。
该函数本身既可以完成本地注入和远程注入操作。但是在普通设备中断流程下,仅使用本地注入。
代码位置1,是维护中断的处理过程。

irqchip_set_pending @hypervisor/arch/arm-common/irqchip.c


void irqchip_set_pending(struct public_per_cpu *cpu_public, u16 irq_id)
{
	struct pending_irqs *pending = &cpu_public->pending_irqs;
	bool local_injection = (this_cpu_public() == cpu_public);
	const u16 sender = this_cpu_id();
	unsigned int new_tail;

	// ***位置1***
	if (local_injection && irqchip.inject_irq(irq_id, sender) != -EBUSY)
		return;

	// ... ...

	if (local_injection)
		irqchip.enable_maint_irq(true);  	// ***位置2***
	else
		// ... ..
}

正常情况下,在代码位置1直接调用注入底层操作inject_irq = gicv3_inject_irq, 完成虚拟中断注入操作。但可能出现列表不用的情况,继续走下面流程。

jailhouse对这种情况的处理,是设置了一个挂在每CPU描述结构struct public_per_cpu的pending_irqs队列。未能本次完成注入的irq暂存在此队列中,等待后续时机。具体时机是在“维护中断”完成的。维护中断是虚拟化一种特殊的中断,由硬件根据设定条件自动激发,针对中断注入,可配置条件为当列表寄存器空闲时激发。在代码位置2,即启用针对该条件的维护中断。

.enable_maint_irq = gicv3_enable_maint_irq,
gicv3_enable_maint_irq @hypervisor/arch/arm-common/gic-v3.c

static void gicv3_enable_maint_irq(bool enable)
{
	u32 hcr;

	arm_read_sysreg(ICH_HCR_EL2, hcr);
	if (enable)
		hcr |= ICH_HCR_UIE;
	else
		hcr &= ~ICH_HCR_UIE;
	arm_write_sysreg(ICH_HCR_EL2, hcr);
}

可见,具体是通过ICH_HCR_EL2寄存器的UIE控制位。

维护中断响应的过程是,如上所示,是arch_handle_phys_irq的代码位置1, 由irqchip_inject_pending(@hypervisor/arch/arm-common/irqchip.c)函数完成

void irqchip_inject_pending(void)
{
	struct pending_irqs *pending = &this_cpu_public()->pending_irqs;
	u16 irq_id, sender;

	while (pending->head != pending->tail) {
		irq_id = pending->irqs[pending->head];
		sender = pending->sender[pending->head];

		if (irqchip.inject_irq(irq_id, sender) == -EBUSY) {
			irqchip.enable_maint_irq(true);
			return;
		}

		memory_barrier();
		pending->head = (pending->head + 1) % MAX_PENDING_IRQS;
	}

	irqchip.enable_maint_irq(false);
}

函数irqchip_inject_pending就是完成将本核累积的待注入中断,全部遍历完成注入。如果因资源不足再次注入失败,还会再次调用irqchip.enable_maint_irq启用维护中断在列表空时通知。注入成功,会在末尾禁用维护中断的在列表空时通知。

以上,就是本地中断注入的过程。除此之外,还需处理跨核中断注入的过程。

[流程2.1.2] 跨核中断注入

首先,虚拟化下虚拟机系统IPI中断的处理具有一个长的流程, 比普通中断接收流程更长。 jailhouse下总体过程包括:
  1. 先是虚拟机系统内部,基于本核,通过GIC CPU接口寄存器ICC_SGI1R_EL1写入尝试触发IPI,中断号为irq_id。注意,如上该操作会被自动重定向到ICV_SGI1R_EL1寄存器;
  2. 该操作被捕获陷入EL2;进入jailhouse的跨核中断的处理。基于EL2,激发一个中断号为1,表示中断注入的跨核中断到目标核
  3. 目标核虚拟cpu退出陷入EL2响应该中断。注入irq_id虚拟中断到虚拟机中。
  4. 虚拟CPU返回后,目标虚拟cpu最终响应源端虚拟cpu发射的核间中断irq_id。

代码调用路径有:


arch_handle_trap [hypervisor/arch/arm64/traps.c]
└─handle_sysreg 
└─└─gicv3_handle_sgir_write [hypervisor/arch/arm-common/gic-v3.c]
└─└─└─gic_handle_sgir_write [hypervisor/arch/arm-common/irqchip.c]
└─└─└─└─irqchip_set_pending 

irqchip_set_pending  [hypervisor/arch/arm-common/irqchip.c]
└─irqchip_send_sgi 
└─└─irqchip.send_sgi = gicv3_send_sgi [hypervisor/arch/arm-common/gic-v3.c]

gic_handle_sgir_write (@hypervisor/arch/arm-common/gic-v3.c)

void gic_handle_sgir_write(struct sgi *sgi)
{
	struct public_per_cpu *cpu_public = this_cpu_public();
	unsigned int cpu, target;
	u64 cluster;

	if (sgi->routing_mode == 2)
		/* Route to the caller itself */
		irqchip_set_pending(cpu_public, sgi->id);
	else
		for_each_cpu(cpu, this_cell()->cpu_set) {
			if (sgi->routing_mode == 1) {
				/* Route to all (cell) CPUs but the caller. */
				if (cpu == cpu_public->cpu_id)
					continue;
			} else {
				target = irqchip_get_cpu_target(cpu);
				cluster = irqchip_get_cluster_target(cpu);

				/* Route to target CPUs in cell */
				if ((sgi->cluster_id != cluster) ||
				    !(sgi->targets & target))
					continue;
			}
			// ***位置2***
			irqchip_set_pending(public_per_cpu(cpu), sgi->id);
		}
}

上述sgi->routing_mode表示gicv3对于核间中断的路由模式(详细暂略);一般情况下,走到位置2,通过调用irqchip_set_pending发起远程注入。

void irqchip_set_pending(struct public_per_cpu *cpu_public, u16 irq_id)
{
	struct pending_irqs *pending = &cpu_public->pending_irqs;
	bool local_injection = (this_cpu_public() == cpu_public);
	const u16 sender = this_cpu_id();
	unsigned int new_tail;


	// ... ...

	spin_lock(&pending->lock);

	new_tail = (pending->tail + 1) % MAX_PENDING_IRQS;

	// ***位置1***
	/* Queue space available? */
	if (new_tail != pending->head) {
		pending->irqs[pending->tail] = irq_id;
		pending->sender[pending->tail] = sender;
		/*
		 * Make the entry content is visible before updating the tail
		 * index.
		 */
		memory_barrier();
		pending->tail = new_tail;
	}

	spin_unlock(&pending->lock);

	if (local_injection)
		// ... ...
	else
		// ***位置2***
		irqchip_send_sgi(cpu_public->cpu_id, SGI_INJECT);
}

如上irqchip_set_pending完成对实际目标中断的pending_irqs队列入队,最后执行一个通过SGI_INJECT中断号的触发。注意此时,本核和目标核不一致,因此是走远程核注入过程. 最终是调用到上述代码位置4,irqchip_send_sgi,传入SGI的。

#define SGI_INJECT	0
#define SGI_EVENT	1

注意,EL2级别的主动触发,只使用0 和1号中断,对于虚拟机系统实际的目标核间中断是写入到pending_irqs队列,待目标核做一次遍历注入。

gicv3_send_sgi @hypervisor/arch/arm-common/gic-v3.c

static void gicv3_send_sgi(struct sgi *sgi)
{
	u64 val;
	u16 targets = sgi->targets;

	if (sgi->routing_mode == 2)
		targets = 1 << phys_processor_id();

	val = (MPIDR_TO_SGIR_AFFINITY(sgi->cluster_id, 3)
	    | MPIDR_TO_SGIR_AFFINITY(sgi->cluster_id, 2)
	    | MPIDR_TO_SGIR_AFFINITY(sgi->cluster_id, 1)
	    | (targets & ICC_SGIR_TARGET_MASK)
	    | (sgi->id & 0xf) << ICC_SGIR_IRQN_SHIFT);

	if (sgi->routing_mode == 1)
		val |= ICC_SGIR_ROUTING_BIT;

	dsb(ish);

	// 位置2 
	arm_write_sysreg(ICC_SGI1R_EL1, val);
	isb();
}

代码位置2,gicv3_send_sgi函数主要是在EL2激发了一次中断号为SGI_INJECT=0的核间中断。
后续在目标核响应SGI_INJECT的流程包括:

irqchip_handle_irq [hypervisor/arch/arm-common/irqchip.c]
└─ arch_handle_sgi [hypervisor/arch/arm-common/control.c]
└─└─ irqchip_inject_pending [hypervisor/arch/arm-common/irqchip.c]

最后调用到irqchip_inject_pending,和本核注入最后调用的函数一样,完成注入操作。

[流程2.2] 中断注入寄存器ICH_LRn_EL2操作

硬件底层,中断注入的中心寄存器是ICH_LRn_EL2。中心函数是gicv3_inject_irq @hypervisor/arch/arm-common/gic-v3.c


static int gicv3_inject_irq(u16 irq_id, u16 sender)
{
	unsigned int n;
	int free_lr = -1;
	u32 elsr;
	u64 lr;

	arm_read_sysreg(ICH_ELSR_EL2, elsr);
	for (n = 0; n < gic_num_lr; n++) {
		if ((elsr >> n) & 1) {
			/* Entry is invalid, candidate for injection */
			if (free_lr == -1)
				free_lr = n;
			continue;
		}

		lr = gicv3_read_lr(n);

		if ((u32)lr == irq_id)
			return -EEXIST;
	}

	if (free_lr == -1)
		/* All list registers are in use */
		return -EBUSY;

	lr = irq_id;
	/* Only group 1 interrupts */
	lr |= ICH_LR_GROUP_BIT;
	lr |= ICH_LR_PENDING;
	if (!is_sgi(irq_id)) {
		lr |= ICH_LR_HW_BIT;
		lr |= (u64)irq_id << ICH_LR_PHYS_ID_SHIFT;
	}
	/* GICv3 doesn't support the injection of the calling CPU ID */

	gicv3_write_lr(free_lr, lr);

	return 0;
}

该函数主要完成:

  1. 通过ICH_ELSR_EL2寄存器,遍历所有注入列表寄存器,确认可用的列表寄存器索引。
  2. 调用gicv3_write_lr通过ICH_LRn_EL2寄存器完成目标虚拟中断注入过程。

至此,中断注入全部流程完毕。

附录

虚拟中断相关寄存器

虚拟中断控制相关寄存器可分为两类: 一是ICH_开头的,属于GICv3中断控制器虚拟化扩展;二是HCR_EL2,这种虚拟化全局控制寄存器,和中断控制器没有关联。

ICH_VTR_EL2

有

  • 虚拟中断控制器类型信息寄存器 (Interrupt Controller VGIC Type Register)
  • 只读。报告当前GIC实现支持的中断虚拟化特性。

ICH_HCR_EL2

有

  • 中断控制器虚拟化监视层控制寄存器(Interrupt Controller Hyp Control Register)

该寄存器64位,主要段意义如下表

起始bit:长度bit 字段标识 含义
0:1 En 是否启用虚拟CPU操作接口
1:1 UIE 是否中断注入列表寄存器为空或仅有1个合法项时激发维护中断信号
10:1 TC 是否产生陷入对EL1访问系统寄存器的操作
11:1 TALL0 是否产生陷入对EL1针对组0中断的访问ICC_, ICV_系统寄存器的操作
12:1 TALL1 是否产生陷入对EL1针对组1中断的访问ICC_, ICV_系统寄存器的操作

HCR_EL2

有

  • 虚拟化监视器配置寄存器(Hypervisor Configuration Register)

寄存器主要位段:

起始bit:长度bit 字段标识 含义
0:1 vm 虚拟化启用。启用二阶页表转换
4:1 imo 是否启用物理irq中断路由到EL2
5:1 fmo 是否启用物理fiq中断路由到EL2
7:1 vi 使在本cpu上虚拟中端悬起
13:1 twe 控制wfe指令是否产生向监管层自陷
14:1 twi 控制wfi指令是否产生向监管层自陷
19:1 tsc 控制smc指令是否产生向监管层自陷
21:1 tacr 控制对Auxiliary Control寄存器的访问是否产生向监管层自陷
34:1 e2h 控制宿主系统是否可运行在el2。v8.2的特性

ICH_ELRSR_EL2

有

  • 获取中断控制器列表寄存器空闲的状态(Interrupt Controller Empty List Register Status Register)
  • 提供虚拟中断列表寄存器空闲查询。
    寄存器主要位段:
起始bit:长度bit 字段标识 含义
0:16 status 显示各列表寄存器的空闲状态

上述

  • status: 0,表示非空闲;1,表示空闲。

ICH_LRn_EL2

有

  • 中断控制器列表寄存器 (Interrupt Controller List Registers)
  • 控制虚拟中断上下文

寄存器主要位段:

起始bit:长度bit 字段标识 含义
0:31 vintid 表示虚拟中断标识
32:13 pintid 物理中断id
48:8 priority 指示虚拟中断优先级
62:2 state 指示虚拟中断状态

有

  • vintid: 非按位,整体表示数字
  • pintid: 必要性。
  • state: arm64四种状态之一。
posted @ 2026-08-04 20:08  happysc  阅读(29)  评论(0)    收藏  举报