qemu-kvm的irqfd機制
irqfd機制與ioeventfd機制類似,其基本原理都是基於eventfd。
ioeventfd機制為Guest提供了向qemu-kvm發送通知的快捷通道,對應地,irqfd機制提供了qemu-kvm向Guest發送通知的快捷通道。
irqfd機制將一個eventfd與一個全局中斷號聯系起來,當向這個eventfd發送信號時,就會導致對應的中斷注入到虛擬機中。
QEMU注冊irqfd
與ioeventfd類似,irqfd在使用前必須先初始化一個EventNotifier對象(利用event_notifier_init函數初始化),初始化EventNotifier對象完成之后獲得了一個eventfd。
向kvm發送注冊中斷irqfd請求
獲得一個eventfd之后,QEMU通過kvm_irqchip_add_irqfd_notifier_gsi=>kvm_irqchip_assign_irqfd構造kvm_irqchip結構,並向kvm發送ioctl(KVM_IRQFD).
static int kvm_irqchip_assign_irqfd(KVMState *s, int fd, int rfd, int virq,
bool assign)
{
struct kvm_irqfd irqfd = {
.fd = fd,
.gsi = virq,
.flags = assign ? 0 : KVM_IRQFD_FLAG_DEASSIGN,
};
if (rfd != -1) {
irqfd.flags |= KVM_IRQFD_FLAG_RESAMPLE;
irqfd.resamplefd = rfd;
}
if (!kvm_irqfds_enabled()) {
return -ENOSYS;
}
return kvm_vm_ioctl(s, KVM_IRQFD, &irqfd);
}
在kvm_irqchip_assign_irqfd中,首先構造了一個kvm_irqfd結構的變量irqfd,其中fd為之前初始化的eventfd,gsi是全局系統中斷,flags中定義了是向kvm注冊irqfd(flags==0)還是解除注冊irqfd(KVM_IRQFD_FLAG_DEASSIGN,也就是flags==1)。flags的bit1(KVM_IRQFD_FLAG_RESAMPLE)表明該中斷是否為電平觸發。
- KVM_IRQFD_FLAG_RESAMPLE相關信息
當中斷處於沿觸發模式時,irqfd->fd連接kvm中中斷芯片(irqchip)的gsi管腳,也由irqfd->fd負責中斷的toggle,以及對用戶空間的handler的觸發。
當中斷處於電平觸發模式時,同樣irqfd->fd連接kvm中中斷芯片的gsi管腳,當中斷芯片收到一個EOI(end of interrupt)重采樣信號時,gsi進行電平翻轉,對用戶空間的通知由irqfd->resample_fd完成(resample_fd也是一個eventfd)。
kvm_irqchip_assign_irqfd最后調用kvm_vm_ioctl(s, KVM_IRQFD, &irqfd),向kvm請求注冊包含上面構造的kvm_irqfd信息的irqfd。
kvm注冊irqfd
收到ioctl(KVM_IRQFD)之后,kvm首先獲取傳入的數據結構kvm_irqfd的信息,然后調用kvm_irqfd函數。
case KVM_IRQFD: {
struct kvm_irqfd data;
r = -EFAULT;
if (copy_from_user(&data, argp, sizeof(data)))
goto out;
r = kvm_irqfd(kvm, &data);
break;
}
在kvm_irqfd中,首先分辨傳入的kvm_irqfd結構中的flags的bit0要求的是進行irqfd注冊還是解除irqfd的注冊。如果是前者,則調用kvm_irqfd_assign。
kvm_irqfd(struct kvm *kvm, struct kvm_irqfd *args)
{
if (args->flags & ~(KVM_IRQFD_FLAG_DEASSIGN | KVM_IRQFD_FLAG_RESAMPLE))
return -EINVAL;
if (args->flags & KVM_IRQFD_FLAG_DEASSIGN)
return kvm_irqfd_deassign(kvm, args);
return kvm_irqfd_assign(kvm, args);
}
kvm_irqfd_assign的分析中省略定義了CONFIG_HAVE_KVM_IRQ_BYPASS和水平中斷的注冊情況。這樣分析便於理清irqfd的注冊框架。
在kvm_irqfd_assign中,首先申請了一個kvm_kernel_irqfd結構類型的變量irqfd,並為之分配空間,之后對irqfd的各子域進行賦值。
irqfd = kzalloc(sizeof(*irqfd), GFP_KERNEL_ACCOUNT);
if (!irqfd)
return -ENOMEM;
irqfd->kvm = kvm;
irqfd->gsi = args->gsi;
INIT_LIST_HEAD(&irqfd->list);
INIT_WORK(&irqfd->inject, irqfd_inject);
INIT_WORK(&irqfd->shutdown, irqfd_shutdown);
seqcount_init(&irqfd->irq_entry_sc);
kvm_kernel_irqfd結構中有2個work_struct,inject和shutdown,分別負責觸發中斷和關閉中斷,這兩個work_struct各自對應的操作函數分別為irqfd_inject和irqfd_shutdown。
kvm_irq_assign調用init_waitqueue_func_entry將irqfd_wakeup函數注冊為irqfd中等待隊列entry激活時的處理函數。這樣任何寫入該irqfd對應的eventfd的行為都將導致觸發這個函數。
然后kvm_irq_assign利用init_poll_funcptr將irqfd_ptable_queue_proc函數注冊為irqfd中的poll table的處理函數。irqfd_ptable_queue_proc會將poll table中對應的wait queue entry加入到waitqueue中去。
/*
* Install our own custom wake-up handling so we are notified via
* a callback whenever someone signals the underlying eventfd
*/
init_waitqueue_func_entry(&irqfd->wait, irqfd_wakeup);
init_poll_funcptr(&irqfd->pt, irqfd_ptable_queue_proc);
kvm_irq_assign接着判斷該eventfd是否已經被其它中斷使用。
list_for_each_entry(tmp, &kvm->irqfds.items, list) {
if (irqfd->eventfd != tmp->eventfd)
continue;
/* This fd is used for another irq already. */
ret = -EBUSY;
spin_unlock_irq(&kvm->irqfds.lock);
goto fail;
}
kvm_irq_assign以irqfd->pt為參數,調用eventfd的poll函數,也就是eventfd_poll,后者會調用poll_wait函數,也就是之前為poll table注冊的irqfd_ptable_queue_proc函數。irqfd_ptable_queue_proc將irqfd->wait加入到了eventfd的wqh等待隊列中。這樣,當有其它進程或者內核對eventfd進行write時,就會導致eventfd的wqh等待隊列上的對象函數得到執行,也就是irqfd_wakeup函數。
這里只討論有數據,即flgas中的EPOLLIN置位時,會調用kvm_arch_set_irq_inatomic進行中斷注入。
kvm_arch_set_irq_inatomic
=> kvm_set_msi_irq
=> kvm_irq_delivery_to_apic_fast
如果kvm_arch_set_irq_inatomic無法注入中斷(即非MSI中斷或非HV_SINT中斷),那么就調用irqfd->inject,即調用irqfd_inject函數。
static void irqfd_inject(struct work_struct *work)
{
struct kvm_kernel_irqfd *irqfd =
container_of(work, struct kvm_kernel_irqfd, inject);
struct kvm *kvm = irqfd->kvm;
if (!irqfd->resampler) {
kvm_set_irq(kvm, KVM_USERSPACE_IRQ_SOURCE_ID, irqfd->gsi, 1,
false);
kvm_set_irq(kvm, KVM_USERSPACE_IRQ_SOURCE_ID, irqfd->gsi, 0,
false);
} else
kvm_set_irq(kvm, KVM_IRQFD_RESAMPLE_IRQ_SOURCE_ID,
irqfd->gsi, 1, false);
}
在irqfd_inject函數中,如果該irqfd配置的中斷為邊沿觸發,則調用2次kvm_set_irq,形成一個中斷脈沖,以便kvm中的中斷芯片(irqchip)能夠感知到這個中斷。如果該irqfd配置的中斷為電平觸發,則調用一次kvm_set_irq,將中斷拉至高電平,使irqchip感知到,電平觸發的中斷信號拉低動作會由后續的irqchip的EOI觸發。
總結
irqfd基於eventfd機制,qemu中將一個gsi(全局系統中斷號)與eventfd捆綁后,向kvm發送注冊irqfd請求,kvm收到請求后將帶有gsi信息的eventfd加入到與irqfd有關的等待隊列中,一旦有進程向該eventfd寫入,等待隊列中的元素就會喚醒,並調用相應喚醒函數(irqfd_wakeup)向Guest注入中斷,而注入中斷這一步驟相關知識與特定的中斷芯片如PIC、APIC有關。