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