QEMU-KVM中的VFIO-MSI機制
當Guest的bios-kernel通過寫入vfio-device的配置空間,來配置msi、msi-x時(也就是向msi/msi-x的capability structure中寫入msi/msi-x enable bit),就會調用提前注冊好的處理函數,即vfio_pci_write_config。
void vfio_pci_write_config(PCIDevice *pdev,
uint32_t addr, uint32_t val, int len)
其中pdev是vfio-device設備,addr是待寫入的設備地址,val是待寫入的數據,len是數據長度(bytes).
如果填入該函數中的數據為能夠正確配置MSI、MSI-X功能的數據,那么就會調用vfio_msi_enable和vfio_msix_enable.
MSI初始化
vfio_msi_enable的主要代碼路徑如下:
staic void vfio_msi_enable(vdev)
{
// 獲取PCIDevice的msi-capability中設置的"可分配的vector數量"
vdev->nr_vectors = msi_nr_vectors_allocated(&vdev->pdev);
// 分配vectors所需占用的空間
vdev->msi_vectors = g_new0(VFIOMSIVector, vdev->nr_vectors);
for (i = 0; i < vdev->nr_vectors; i++) { // 遍歷該設備上所有的msi vector
vector = &vdev->msi_vectors[i];
vector->virq = -1;
vector->use = true;
// 初始化vector->interrupt對應的eventfd,但不激活
event_notifier_init(&vector->interrupt, 0)
/*注冊該eventfd對應的讀callback,當該eventfd可讀時,觸發vfio_msi_interrupt.
* vfio_msi_interrupt中,首先獲取msi消息內容,然后通過寫地址的方法發送msi中斷
*/
qemu_set_fd_handler(event_notifier_get_fd(&vector->interrupt),
vfio_msi_interrupt, NULL, vector);
------------------------以上為qemu中的eventfd機制,即eventfd toggle觸發vfio_msi_interrupt-------------
/* 1. 向kvm添加vector對應的msi的irq_routing_table的entry。
* 2. 將eventfd與irq關聯,並在kvm中注冊eventfd的poll,invoke函數。
* 3. 如果支持PI,還會注冊一個consumer,consumer.add_producer函數會
* 更新producer->host_irq對應的IOMMU->IRTE
*/
vfio_add_kvm_msi_virq(vdev, vector, i, false);
--------------------------以上為向kvm中關聯qemu的eventfd,並與對應的處理函數綁定過程-----------------------
}
/* 1. 構造irq_set,包含了設備的irq數量,irq類型,irq對應的eventfd的fd集合等。
* 然后ioctl(VFIO_DEVICE_SET_IRQS)
* 2. 為vdev分配irq數量的中斷,並將這些中斷類型確定為msi或msix.
* 3. 為傳入的所有fds(eventfd)注冊trigger函數(vfio_msihandler),以及注冊producer.
* producer的注冊會導致與前面注冊的consumer連接。
* vfio_msihandler的調用會toggle eventfd,進而引起producer狀態變化,最終導致consumer狀態變化。
*/
vfio_enable_vectors(vdev, false);
}
以下是vfio_msi_enable函數中相關函數的框架分析。
vfio_add_kvm_msi_virq(VFIOPCIDevice *vdev, VFIOMSIVector *vector,
int vector_n, bool msix)
{
// 初始化vector->kvm_interrupt對應eventfd,但不激活
event_notifier_init(&vector->kvm_interrupt, 0)
// 向kvm的irq_routing_table中為該vector添加一個entry
virq = kvm_irqchip_add_msi_route(kvm_state, vector_n, &vdev->pdev);
//
kvm_irqchip_add_irqfd_notifier_gsi(kvm_state, &vector->kvm_interrupt,
NULL, virq)
}
kvm_irqchip_add_msi_route(KVMState *s, int vector, PCIDevice *dev)
{
// 獲取msi消息內容
msg = pci_get_msi_message(dev, vector);
// 找到kvm的gsi_bitmap中最低的尚未使用的bit 即找到一個尚未使用的最低irq index
virq = kvm_irqchip_get_virq(s);
{
// 一段構造一個routing_entry(kroute)的代碼
}
kvm_add_routing_entry(s, &kroute);
kvm_arch_add_msi_route_post(&kroute, vector, dev); // 通知更新了msi entry
kvm_irqchip_commit_routes(s); // 調用ioctl(KVM_SET_GSI_ROUTING)
return virq;
}
int kvm_irqchip_add_irqfd_notifier_gsi(KVMState *s, EventNotifier *n,
EventNotifier *rn, int virq)
{
return kvm_irqchip_assign_irqfd(kvm_state, &vector->kvm_interrupt, NULL, virq, true);
}
kvm_irqchip_assign_irqfd(KVMState *s, EventNotifier *event,
EventNotifier *resample, int virq,
bool assign)
{
fd = fd_of(event);
rfd = -1;
struct kvm_irqfd irqfd = {
.fd = fd,
.gsi = virq,
.flags = 0,
};
kvm_vm_ioctl(s, KVM_IRQFD, &irqfd);
}
// kvm中
kvm_irqfd(kvm_irqfd args)
{
return kvm_irqfd_assign(kvm, 0);
}
kvm_irqfd_assign(struct kvm *kvm, struct kvm_irqfd *args)
{
irqfd->gsi = args->gsi;
INIT_WORK(&irqfd->inject, irqfd_inject); // 初始化irq對應的callabck-irqfd_inject
f = fdget(args->fd);
eventfd = eventfd_ctx_fileget(f.file);
irqfd->eventfd = eventfd;
// 注冊監視eventfd變動的被動通知函數
init_waitqueue_func_entry(&irqfd->wait, irqfd_wakeup);
// 注冊主動poll該eventfd的查詢函數
init_poll_funcptr(&irqfd->pt, irqfd_ptable_queue_proc);
// 更新系統中的irqfd
irqfd_update(kvm, irqfd);
/* 注冊一個irq的consumer. 調用該consumer的.add_producer成員會導致更新irq對應的host_irq的IOMMU-IRTE */
#ifdef CONFIG_HAVE_KVM_IRQ_BYPASS
if (kvm_arch_has_irq_bypass()) {
irqfd->consumer.token = (void *)irqfd->eventfd;
irqfd->consumer.add_producer = kvm_arch_irq_bypass_add_producer;
irqfd->consumer.del_producer = kvm_arch_irq_bypass_del_producer;
irqfd->consumer.stop = kvm_arch_irq_bypass_stop;
irqfd->consumer.start = kvm_arch_irq_bypass_start;
ret = irq_bypass_register_consumer(&irqfd->consumer);
if (ret)
pr_info("irq bypass consumer (token %p) registration fails: %d\n",
irqfd->consumer.token, ret);
}
#endif
}
vfio_enable_vectors
=> // 構造irq_set,包含了設備的irq數量,irq類型,irq對應的eventfd的fd結合等
=> ret = ioctl(vdev->vbasedev.fd, VFIO_DEVICE_SET_IRQS, irq_set);
=> vfio_pci_set_irqs_ioctl
=> vfio_pci_set_msi_trigger
=> vfio_msi_enable // 為vdev分配irq數量的中斷,並將這些中斷類型確定為msi或msix
=> vfio_msi_set_block // 為傳入的所有fds(eventfd)注冊trigger函數(vfio_msihandler),以及producer
// vfio_msihandler的調用會toggle eventfd,進而引起producer狀態變化
MSI-X初始化
vfio_msi_enable在Guest寫MSI-X Capability Structure的Enable Bit時觸發。
vfio_msix_enbale函數的主要代碼如下:
/* 對0號vector進行enable並迅速釋放vector0,這樣
* 就會讓vfio設備處於沒有vector enabled的狀態,但
* MSI-X capability 被使能,這種狀態是Guest驅動需
* 要的狀態.
*/
static void vfio_msix_enable(VFIOPCIDevice *vdev)
{
/* 分配msix的vectors所需的空間 */
vdev->msi_vectors = g_new0(VFIOMSIVector, vdev->msix->entries);
vfio_msix_vector_do_use(&vdev->pdev, 0, NULL, NULL);
vfio_msix_vector_release(&vdev->pdev, 0);
msix_set_vector_notifiers(&vdev->pdev, vfio_msix_vector_use,
vfio_msix_vector_release, NULL);
}
/* 由於傳入的vector號碼為0,msg和handler均為NULL,所以該函數的功能實際變為:
* 1. 為vector->interrupt注冊eventfd
* 2. 為vector0對應的eventfd注冊trigger函數(vfio_msihandler)以及producer
* 目的就是使VFIO設備的MSIX的Enable bit為1.
*/
static int vfio_msix_vector_do_use(PCIDevice *pdev, unsigned int nr=0,
MSIMessage *msg=NULL, IOHandler *handler=NULL)
{
vector = &vdev->msi_vectors[nr];// 獲取msi_vector的地址
if (!vector->use) { // 第一次進入該函數時,該vector->use肯定為False
vector->vdev = vdev;
vector->virq = -1;
if (event_notifier_init(&vector->interrupt, 0)) { // 初始化vector對應的notifier(fd)(qemu)
error_report("vfio: Error: vfio_enable_vectorsevent_notifier_init failed");
}
vector->use = true;
msix_vector_use(pdev, nr); // vdev->msix_entry_used[nr]++
}
/* 將vector->interrupt對應的eventfd的讀觸發handler設備handler,寫觸發handler設置為0
* 事實上傳入的handler為0,所以該eventfd沒有讀/寫觸發handler。
*/
qemu_set_fd_handler(event_notifier_get_fd(&vector->interrupt),
handler, NULL, vector);
----------------------------------vfio_enable_vectors()的內容----------------------------
/* 1. 構造irq_set,包含了設備的irq數量=1,irq類型(msix),irq的flags等。
* 然后ioctl(VFIO_DEVICE_SET_IRQS),在ioctl中:
* 2. 為vdev分配irq數量的中斷,並將這些中斷類型確定為msix.
* 3. 為傳入的所有fd(eventfd)注冊trigger函數(vfio_msihandler),以及注冊producer.
* vfio_msihandler的調用會toggle eventfd.
*/
ret = vfio_enable_vectors(vdev, true);
/* 禁用PBA模擬 */
clear_bit(nr, vdev->msix->penmsix_msgding);
if (find_first_bit(vdev->msix->pending,
vdev->nr_vectors) == vdev->nr_vectors) {
memory_region_set_enabled(&vdev->pdev.msix_pba_mmio, false);
trace_vfio_msix_pba_disable(vdev->vbasedev.name);
}
}
/* 由於0號vector在前面只初始化了qemu的eventfd而不是內核的irqfd,所以在這個函數里什么也不用做其實 */
static void vfio_msix_vector_release(PCIDevice *pdev, unsigned int nr)
{
vector = &vdev->msi_vectors[nr];
}
/* 將設備的所有msix_vector對應的use_noifier,release_notifier,poll_notifier都進行記錄 ,方便之后調用 */
int msix_set_vector_notifiers(PCIDevice *dev= vdev->pdev,
MSIVectorUseNotifier use_notifier=vfio_msix_vector_use,
MSIVectorReleaseNotifier release_notifier=vfio_msix_vector_release,
MSIVectorPollNotifier poll_notifier=NULL)
{
dev->msix_vector_use_notifier = use_notifier;
dev->msix_vector_release_notifier = release_notifier;
dev->msix_vector_poll_notifier = poll_notifier;
return 0;
}
以上,MSI-X中斷的功能被Enable,Message Control的最高bit置了1.
接下來是當Guest對MSI-X Table中的Vector-msg地址-msg數據進行寫入時,具體的處理情況。
由於MSI-X Table存儲在某個BAR中,而BAR又分為可mmap的BAR和不可mmap的BAR,現在一般都是用可mmap的BAR作為MSI-X Table的存儲BAR。
Guest配置MSIX Table:
=> 由於MMIO Exit而退到QEMU
=> address_space_rw
=> address_space_write
=> flatview_write
...(一系列的調用)
之后調用到:
// 通過下面這些調用,會對某vector對應的notifier進行調用,即vfio_msix_vector_do_use
// 目的是對某vector對應的中斷鏈路進行一次初始化
=> msix_table_mmio_write
=> msix_handle_mask_update
=> msix_fire_vector_notifier
=> vfio_msix_vector_use
=> vfio_msix_vector_do_use
/* POST STATE: msix-msg已經從MSIX-Table中提取了出來,nr對應vector號碼,handler為vfio_msi_interrupt
* vfio_msi_interrupt會被注冊為該vector對應的處理函數,當該vector對應的eventfd toggle時,
* 就會發送一個MSI message
*/
static int vfio_msix_vector_do_use(PCIDevice *pdev, unsigned int nr,
MSIMessage *msg, IOHandler *handler=vfio_msi_interrupt)
{
if (!vector->use) { // 如果是vector0,那么在之前的第一次調用該函數過程中已經初始化了對應eventfd,use為1
// 其它vector需要初始化一個eventfd
vector->vdev = vdev;
vector->virq = -1;
if (event_notifier_init(&vector->interrupt, 0)) { // 初始化vector對應的notifier(fd)
error_report("vfio: Error: event_notifier_init failed");
}
vector->use = true;
msix_vector_use(pdev, nr);
}
// 設置用戶空間(qemu)的eventfd對應的handler
qemu_set_fd_handler(event_notifier_get_fd(&vector->interrupt),
handler, NULL, vector);
/*
* Attempt to enable route through KVM irqchip,
* default to userspace handling if unavailable.
*/
if (vector->virq >= 0) { // 因為是初始化階段,所有vector都沒有申請kernel的eventfd,因此virq肯定為-1
if (!msg) {
vfio_remove_kvm_msi_virq(vector);
} else {
vfio_update_kvm_msi_virq(vector, *msg, pdev);
}
} else {
if (msg) { // 這次msg不為NULL,因此調用vfio_add_kvm_msi_virq
/*
* 1. 向kvm添加vector對應的msi的irq_routing_table的entry。
* 2. 將eventfd與irq關聯,並在kvm中注冊eventfd的poll,invoke函數。
* 3. 如果支持PI,還會注冊一個consumer,consumer.add_producer函數會
* 更新producer->host_irq對應的IOMMU->IRTE
*/
vfio_add_kvm_msi_virq(vdev, vector, nr, true);
}
}
/* 由於在之前的Enable-MSIX時,已經將vdev->nr_vectors+1了,因此這里if條件不滿足*/
if (vdev->nr_vectors < nr + 1) { // * Enable-MSIX時,這里的nr=0,vdev->nr_vectors=0
vfio_disable_irqindex(&vdev->vbasedev, VFIO_PCI_MSIX_IRQ_INDEX); // * 禁用INTx
vdev->nr_vectors = nr + 1;
ret = vfio_enable_vectors(vdev, true);
if (ret) {
error_report("vfio: failed to enable vectors, %d", ret);
}
}else {
Error *err = NULL;
int32_t fd;
if (vector->virq >= 0) { // vfio_add_kvm_msi_virq中將vector->virq置為了正值
fd = event_notifier_get_fd(&vector->kvm_interrupt); // 獲取vector對應的kernel的eventfd
} else {
fd = event_notifier_get_fd(&vector->interrupt);
}
/* 1. 構造irq_set,包含了設備的irq數量=1,irq類型(msix),irq的flags等。
* 然后ioctl(VFIO_DEVICE_SET_IRQS),在ioctl中:
* 2. 為vdev分配irq數量的中斷,並將這些中斷類型確定為msix.
* 3. 為傳入的所有fd(eventfd)注冊對應的Host IRQ,trigger函數(vfio_msihandler),以及producer.
* vfio_msihandler的調用會toggle eventfd.
*/
vfio_set_irq_signaling(&vdev->vbasedev,
VFIO_PCI_MSIX_IRQ_INDEX, nr,
VFIO_IRQ_SET_ACTION_TRIGGER, fd, &err)
}
總結
在Guest配置MSI/MSI-X功能時,需要對設備的配置空間進行寫操作,而VFIO設備在初始化時,就注冊了Guest寫操作對應的callback,即vfio_pci_write_config。
該函數中,對於MSI/MSI-X的大致配置路徑如下圖所示。經過一系列的條件檢查,最終對VFIO設備的MSI、MSI-X配置會落實到vfio_msi_enable()和vfio_msix_enable()上。

MSI配置
在qemu配置msi中斷時,有3種情況:
- 如果只是qemu單獨運行guest,沒有kvm,那么qemu會用自身的eventfd機制,每當需要發送中斷時,就讀eventfd,導致調用
vfio_msi_interrupt,進而發送中斷。 - 如果qemu+kvm運行guest,那么qemu會注冊eventfd+irqfd,使vfio設備的msi中斷在觸發之后,改變eventfd的狀態,進而觸發kvm中的irq_routing中的某entry,最終通過Local APIC注入到Guest中。
- 在2的情況下,如果kvm支持irq_bypass feature,且CPU支持posted Interrupt,那么會將設備的在vfio設備的msi中斷觸發后,會改變eventfd的狀態,然后導致host kernel中與msi中斷綁定的host_irq觸發,進而觸發IOMMU的中斷重定向機制(發送一個Posted Interrupt),最終將中斷發送到Guest內部。

MSI-X配置
相比於msi,msi-x多了一步提前使能硬件設備的msi-x功能的工作,其它步驟與msi大同小異。

