0%

ARM64 LPI虚拟化基本逻辑

基本逻辑

LPI/vLPI硬件相关的逻辑可以参考这里。

host上不同的GIC版本对vGIC LPI的支持方式是不一样的,GICv3时,需要先trap到KVM,在
KVM里向虚机注入中断,GICv4.0及后续的GIC版本支持vLPI直接注入虚机。

虚拟机中的ITS设备是一个模拟出来的设备,对ITS相关资源的访问需要在KVM模拟。首先KVM
需要模拟ITS的MMIO寄存器,通过模拟的MMIO寄存器,guest ITS上各种配置要被真实的配置
到物理ITS上。

虚机中的ITS的各种table是放到IPA上的。软件通过ITS command访问device table、ITT以及
connection table,向虚机中的ITS发commnand(更新虚拟ITS commnand队列的写入指针)触发
KVM trap,KVM中模拟对应的ITS command,这里的模拟就是在host向物理ITS发ITS command,
把对应的配置下发到硬件。软件直接可以访问LPI config table,虚机中配置LPI config table
后会发INV命令清理硬件中对应的缓存,KVM模拟INV命令时读虚机LPI config table的对应
地址拿到对应中断的配置信息,然后更新对应的vLPI config table域段。

物理上看,完整的vLPI中断上报,需要提前把这个vLPI中断的相关信息配置到物理硬件里,
这些信息包括: 这个vLPI发出设备的device id、eventid(一般就是PCIe设备的BDF和MSI/MSI-X
cap里的data域段),vLPI的中断号,vLPI要发送到的vCPU,vCPU在哪个物理CPU上,当vCPU
不在位时是否需要触发的doorbell中断。从虚机的视角上看,vLPI的配置信息只有device id、
event id、中断号以及vCPU,虚机并不感知物理CPU。

KVM里模拟虚机ITS command,把vLPI的device id、event id、中断号以及vCPU配置到物理
ITS上。KVM在vCPU上下线时,把vCPU和物理CPU的映射关系配置给物理ITS。

ITS模拟

ARM KVM虚拟化的整体逻辑可以参考这里,其中有涉及ITS在KVM里的模拟逻辑。

因为是模拟ITS,所以并不要需要模拟行为当下生效,只要功能正确就好。比如,KVM模拟虚机
的MAPI时没有下ITS command,实际物理配置发生在kvm_vgic_v4_set_forwarding的流程里,
这个是在guest访问VF MSI/MSI-X BAR的时候,截获完成host的配置。

下面展开看下KVM里对ITS的各个命令都是怎么模拟的。KVM里为了模拟ITS,为ITS的device
table/ITT/collection table建立了对应的软件数据结构,为创建的每个虚拟中断也建立了
数据结构(vgic_irq)。对ITS comand的模拟基本上在改动这些数据结构中的内容,部分comand
会涉及到硬件配置的改动。这些数据结构估计大部分是GICv3用的,GICv4.x后vLPI可以直通
了,用的会比较少。

GICv3中断注入

todo: …

GICV4.x中断直通

针对vm fd的操作,本意是通过eventfd,在kvm中把虚机中断注入给虚机。对于支持中断直通
的场景,这里会执行如下irq_bypass_register_consumer,注册架构相关的consumer。这里
的consumer和如下vfio_pci_core_ioctl一起完成直通中断在物理GIC上的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* virt/kvm/kvm_main.c */
kvm_vm_ioctl
/* KVM_IRQFD */
+-> kvm_irqfd
+-> kvm_irqfd_assign
/*
* 注册irq_bypass_consumer,其中的回调是:
* add_producer: kvm_arch_irq_bypass_add_producer
* del_producer: kvm_arch_irq_bypass_del_producer
* stop: kvm_arch_irq_bypass_stop
* start: kvm_arch_irq_bypass_start
*
* 系统里有producers和consumers对应的链表,注册其实就是加到对应的链表里。
* 使用__connect函数可以把consumer和producer连接在一起,基本逻辑是consumer
* 调用add_producer,或者producer调用add_consumer。
*/
+-> irq_bypass_register_consumer

虚机中访问MSI-X BAR的时候,会导致虚机退出到KVM,KVM进一步退出到qemu的vfio设备的
模拟逻辑里,其中会针对vfio设备的fd调用如下ioctl,在host上配置对应中断的支持通路。
(todo: MSI cap的逻辑是怎么样的?)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
vfio_pci_core_ioctl
+-> vfio_pci_ioctl_set_irqs <--- VFIO_DEVICE_SET_IRQS
+-> vfio_pci_set_irqs_ioctl
+-> vfio_pci_set_msi_trigger <--- VFIO_PCI_MSI_IRQ_INDEX
| /*
| * ioctl参数VFIO_IRQ_SET_DATA_NONE和count为0,触发vf的MSI被disable。
| * 具体行为是:释放该vfio设备对应的每个MSI中断资源,其中包括尝试对
| * 之前分配host MSI中断的释放(free_irq),经过中断子系统,最后调用到
| * ITS驱动里的its_irq_domain_deactivate,最后会对host MSI中断发discard
| * 命令。需要注意的是,如果这时还是有vf上的中断在上报,vf的全部MSI被
| * disable这个行为可能导致对应的中断被discard掉。是否会导致这样的问题,
| * 主要控制因素在QEMU里,vfio只是接口提供者。
| */
+-> vfio_msi_disable
| +-> vfio_msi_set_vector_signal(..., fd = -1, ...)
| +-> free_irq
| ...
| +-> its_irq_domain_deactivate
| +-> its_send_discard
| /*
| * 根据ioctl的输入参数start和count,配置对应的MSI。vfio_msi_enable里
| * 会配置irq_type,所以对于配置过irq_type的MSI,其已经运行过vfio_msi_enable,
| * 所以直接做vfio_msi_set_block,这里估计是一个小的优化。
| */
+-> vfio_msi_set_block
| /* 其中,要配置一个中断时,先把之前的配置去掉 */
| +-> vfio_msi_set_vector_signal
| /* 分配中断 */
| +-> vfio_msi_alloc_irq
| /* 创建vfio里记录中断的数据结构 */
| +-> vfio_irq_ctx_alloc
| /* 获取对应的eventfd */
| +-> trigger = eventfd_ctx_fdget
| /*
| * 注册vf设备的中断,如果host是GICv3,那么vf的中断首先就会触发如下
| * vfio_msihandler中断处理函数。该函数触发一个eventfd的signal,
| * KVM收到这个signal会给vCPU注入中断。
| */
| +-> request_irq(irq, vfio_msihandler, 0, ctx->name, trigger)
|
| /* token: trigger, irq: irq, */
| +-> irq_bypass_register_producer
| /*
| * consumer和producer的token一样才执行__connect。这里是调用上面
| * 注册的consumer中的add_producer回调函数。add_producer前后会
| * 调用consumer stop/start,stop中挂起VM,start中reseume VM。
| */
| +-> __connect()
| +-> cons->stop -> cons->add_producer -> cons->start
|
+-> vfio_msi_enable
+-> ...

这里展开分析add_producer函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 注意这里的virq是如上request_irq里的irq,是vf在host上的中断 */
kvm_arch_irq_bypass_add_producer(kvm, virq, struct kvm_kernel_irq_routing_entry)
+-> kvm_vgic_v4_set_forwarding
/* 拿到虚拟中断对应的vgic_irq,保存在irq里,irq->host_irq为host irq */
+-> vgic_its_resolve_lpi(..., &irq)
/* drivers/irqchip/irq-gic-v4.c */
+-> its_map_vlpi(virq, map)
+-> irq_set_vcpu_affinity(irq, xxx)
...
+-> its_irq_set_vcpu_affinity <--- struct irq_chip its_irq_chip
+-> its_vlpi_map <--- MAP_VLPI
|
+-> its_map_vm <--- 如何确定物理CPU?
| +-> its_send_vmapp
|
+-> lpi_write_config
|
+-> its_send_discard <--- 去掉如上host irq在ITS里的map,所以这个
| 命令的参数是vf的物理dev_id/event_id
|
+-> its_send_vmapti

(todo: vLPI pending配置enable的分析,应该在active domain)
(todo: 虚机里的GICv3的table和host上的vtable的对应关系)

vCPU上下线逻辑

vCPU上线中关于vLPI的调用流程如下:

1
2
3
4
5
6
7
8
9
10
11
kvm_sched_out
+-> kvm_arch_vcpu_put
...
+-> kvm_vgic_put
+-> vgic_v3_put
+-> vgic_v4_put <-- 没有直通在这里返回
+-> its_make_vpe_non_resident
+-> its_send_vpe_cmd <-- DESCHEDULE_VPE
+-> irq_set_vcpu_affinity
+-> its_vpe_set_vcpu_affinity <-- irq_chip its_vpe_irq_chip
+-> its_vpe_deschedule <-- 配置GICR_VPENDBASER invalid

如上its_vpe_deschedule具体到硬件,主要配置GICR_VPENDBASER invalid,表示没有上线
的vPE,同时还进行一些硬件状态的同步操作。

vCPU下线中关于vLPI的调用流程如下,核心处理逻辑在vgic_v4_load。
kvm_sched_in -> kvm_arch_vcpu_load -> kvm_vgic_load -> vgic_v3_load -> vgic_v4_load

1
2
3
4
5
6
7
8
9
10
11
vgic_v4_load
+-> irq_set_affinity
...
+-> its_vpe_set_affinity <-- chip->irq_set_affinity, its_vpe_irq_chip
...
+-> its_send_vmovp

+-> its_make_vpe_resident
...
+-> its_vpe_set_vcpu_affinity <-- SCHEDULE_VPE
+-> todo: GICR_VPROPBASER/GICR_VPENDBASER

vCPU在一个物理CPU上线时,vCPU在线的物理CPU可能发生变化,这时就需要用ITS VMOVP更新
硬件中对应的信息。同时更新GICR_VPROPBASER/GICR_VPENDBASER,为后面vLPI直通做准备。

一个例子

上帝视角看下当虚机系统里linux内核调用enable_irq时整个调用流程是怎么样的。经过如下
调用:enable_irq -> __enable_irq -> irq_startup,irq_startup里两个最主要的操作就
是__irq_startup和irq_setup_affinity,前者使能中断,后者配置中断和core的亲和性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/* 配置中断使能 */
__irq_startup
+-> enable_irq
+-> unmask_irq
/*
* 直接找见最底层的PCIe设备的irq_unmask回调,内核这里有变动,以前的版本
* 是its_unmask_msi_irq
*/
+-> pci_irq_unmask_msix
+-> cond_unmask_parent
/* irq-gic-v3-its.c: its_irq_chip的its_unmask_irq */
+-> its_unmask_irq
+-> lpi_update_config
/*
* 对于虚机或者只有host的情况,就是直接配置LPI config table的
* 对应位置,具体位置是从config table的8192 byte起,以hwirq为
* 索引,每个中断占一个byte。
*/
+-> lpi_write_config
/*
* guest上执行这个命令会trap到KVM里面模拟,kvm里的执行流程是:
*
* vgic_its_cmd_handle_inv
* -> update_lpi_config
* -> its_prop_update_vlpi
* -> irq_set_vcpu_affinity 具体为its_irq_chip里的回调
* -> its_vlpi_prop_update
* ...
* kvm直接读guest里的LPI config table对应中断的配置,在host中
* 同步到vLPI config table。
*/
+-> its_send_inv
/* guest里访问MSIX BAR触发退出到qemu里处理 */
+-> pci_msix_unmask

/* 配置中断和core的关系 */
irq_setup_affinity
+-> its_set_affinity
/*
* guest里movi命令触发kvm里对其的模拟,模拟的逻辑是使用its_map_vlpi建立
* virq到vCPU的映射关系。
*/
+-> its_send_movi