2018-01-18
其實在之前的文章中已經簡要介紹了VHOST中通過irqfd通知guest,但是並沒有對irqfd的具體工作機制做深入分析,本節簡要對irqfd的工作機制分析下。這里暫且不討論具體中斷虛擬化的問題,因為那是另一個內容,這里僅僅討論vhost如何使用中斷的方式對guest進行通知,這里答案就是IRQFD。
在vhost的初始化過程中,qemu會通過ioctl系統調用和KVM交互,注冊guestnotifer,見kvm_irqchip_assign_irqfd函數。qemu中對irqfd的定義如下
struct kvm_irqfd { __u32 fd; __u32 gsi; __u32 flags; __u8 pad[20]; };
該結構是32個字節對齊的,fd是irqfd綁定的eventfd的描述符,gsi是給irqfd綁定的一個 全局中斷號,flag紀錄標志位,可以判定本次請求是irqfd的注冊還是消除。qemu把該結構傳遞給KVM,KVM中做了什么處理呢?
在kvm_vm_ioctl函數中的case KVM_IRQFD分支,這里把kvm_irqfd復制到內核中,調用了kvm_irqfd函數。經過對flags的判斷,如果發現是注冊的話,就調用kvm_irqfd_assign函數,並把kvm_irqfd結構作為參數傳遞進去,該函數是注冊irqfd的核心函數。在此之前,先看下內核中對於irqfd對應的數據結構
struct _irqfd { /* Used for MSI fast-path */ struct kvm *kvm; wait_queue_t wait; /* Update side is protected by irqfds.lock */ struct kvm_kernel_irq_routing_entry __rcu *irq_entry; /* Used for level IRQ fast-path */ int gsi; struct work_struct inject; /* The resampler used by this irqfd (resampler-only) */ struct _irqfd_resampler *resampler; /* Eventfd notified on resample (resampler-only) */ struct eventfd_ctx *resamplefd; /* Entry in list of irqfds for a resampler (resampler-only) */ struct list_head resampler_link; /* Used for setup/shutdown */ struct eventfd_ctx *eventfd; struct list_head list;//對應KVM中的irqfd鏈表 poll_table pt; struct work_struct shutdown; };
irqfd在內核中必然屬於某個KVM結構體,因此首個字段便是對應KVM結構體的指針;在KVM結構體中也有對應的irqfd的鏈表;第二個是wait,是一個等待隊列對象,irqfd需要綁定一個eventfd,且在這個eventfd上等待,當eventfd有信號是,就會處理該irqfd。目前暫且忽略和中斷路由相關的字段;GSI正是用戶空間傳遞過來的全局中斷號;indect和shutdown是兩個工作隊列對象;pt是一個poll_table對象,作用后面使用時在進行描述。接下來看kvm_irqfd_assign函數代碼,這里我們分成三部分:准備階段、對resamplefd的處理,對普通irqfd的處理。第二種我們暫時忽略
第一階段:
irqfd = kzalloc(sizeof(*irqfd), GFP_KERNEL); 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); /*得到eventfd對應的file結構*/ file = eventfd_fget(args->fd); if (IS_ERR(file)) { ret = PTR_ERR(file); goto fail; } /*得到eventfd對應的描述符*/ eventfd = eventfd_ctx_fileget(file); if (IS_ERR(eventfd)) { ret = PTR_ERR(eventfd); goto fail; } /*進行綁定*/ irqfd->eventfd = eventfd;
這里的任務比較明確,在內核為irqfd分配內存,設置相關字段。重要的是對於前面提到的inject和shutdown兩個工作隊列綁定了處理函數。然后根據用戶空間傳遞進來的fd,解析得到對應的eventfd_ctx結構,並和irqfd進行綁定。
第三階段:
/* * 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); //poll函數中會調用,即eventfd_poll中,這里僅僅是綁定irqfd_ptable_queue_proc函數和poll_table init_poll_funcptr(&irqfd->pt, irqfd_ptable_queue_proc); spin_lock_irq(&kvm->irqfds.lock); ret = 0; /*檢查針對當前eventfd是否已經存在irqfd與之對應*/ 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路由表*/ irq_rt = rcu_dereference_protected(kvm->irq_routing, lockdep_is_held(&kvm->irqfds.lock)); /*更新kvm相關信息,主要是irq路由項目*/ irqfd_update(kvm, irqfd, irq_rt); /*調用poll函數,把irqfd加入到eventfd的等待隊列中 eventfd_poll*/ events = file->f_op->poll(file, &irqfd->pt); /*把該irqfd加入到虛擬機kvm的鏈表中*/ list_add_tail(&irqfd->list, &kvm->irqfds.items); /* * Check if there was an event already pending on the eventfd * before we registered, and trigger it as if we didn't miss it. */ /*如果有可用事件,就執行注入,把中斷注入任務加入到系統全局工作隊列*/ if (events & POLLIN) schedule_work(&irqfd->inject); spin_unlock_irq(&kvm->irqfds.lock); /* * do not drop the file until the irqfd is fully initialized, otherwise * we might race against the POLLHUP */ fput(file); return 0;
第三階段首先設置了irqfd中等待對象的喚醒函數irqfd_wakeup,然后用init_poll_funcptr對irqfd中的poll_table進行初始化,主要是綁定一個排隊函數irqfd_ptable_queue_proc,其實這兩步也可以看做是准備工作的一部分,不過由於第二部分的存在,只能安排在第三部分。接下來遍歷KVM結構中的irqfd鏈表,檢查是否有irqfd已經綁定了本次需要的eventfd,言外之意是一個eventfd只能綁定一個irqfd。如果檢查沒有問題,則會調用irqfd_update函數更新中斷路由表項目。並調用VFS的poll函數對eventfd進行監聽,並把irqfd加入到KVM維護的鏈表中。如果發現現在已經有可用的信號(可用事件),就立刻調用schedule_work,調度irqfd->inject工作對象,執行中斷的注入。否則,中斷的注入由系統統一處理。具體怎么處理呢?先看下poll函數做了什么,這里poll函數對應eventfd_poll函數
static unsigned int eventfd_poll(struct file *file, poll_table *wait) { struct eventfd_ctx *ctx = file->private_data; unsigned int events = 0; unsigned long flags; /*執行poll_table中的函數,把irqfd加入eventfd的到等待隊列中*/ poll_wait(file, &ctx->wqh, wait); spin_lock_irqsave(&ctx->wqh.lock, flags); if (ctx->count > 0) events |= POLLIN;//表明現在可以read if (ctx->count == ULLONG_MAX) events |= POLLERR; if (ULLONG_MAX - 1 > ctx->count) events |= POLLOUT;//現在可以write spin_unlock_irqrestore(&ctx->wqh.lock, flags); return events; }
該函數的可以分成兩部分,首先是通過poll_wait函數執行poll_table中的函數,即前面提到的irqfd_ptable_queue_proc,該函數把irqfd中的wait對象加入到eventfd的等待隊列中。這樣irqfd和eventfd的雙向關系就建立起來了。接下來的任務是判斷eventfd的當前狀態,並返回結果。如果count大於0,則表示現在可讀,即有信號;如果count小於ULLONG_MAX-1,則表示目前該描述符還可以接受信號觸發。當然這種情況絕大部分是滿足的。那么在evnetfd上等待的對象什么時候會被處理呢?答案是當有操作試圖給evnetfd發送信號時,即eventfd_signal函數
__u64 eventfd_signal(struct eventfd_ctx *ctx, __u64 n) { unsigned long flags; spin_lock_irqsave(&ctx->wqh.lock, flags); if (ULLONG_MAX - ctx->count < n) n = ULLONG_MAX - ctx->count; ctx->count += n; /*mainly judge if wait is empty,如果等待隊列不為空,則進行處理*/ if (waitqueue_active(&ctx->wqh)) wake_up_locked_poll(&ctx->wqh, POLLIN); spin_unlock_irqrestore(&ctx->wqh.lock, flags); return n; }
函數首要任務是向對應的eventfd傳送信號,實質就是增加count值。因為此時count值一定大於0,即狀態可用,則檢查等待隊列中時候有等待對象,如果有,則調用wake_up_locked_poll函數進行處理
#define wake_up_locked_poll(x, m) \ __wake_up_locked_key((x), TASK_NORMAL, (void *) (m)) void __wake_up_locked_key(wait_queue_head_t *q, unsigned int mode, void *key) { __wake_up_common(q, mode, 1, 0, key); } static void __wake_up_common(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, int wake_flags, void *key) { wait_queue_t *curr, *next; list_for_each_entry_safe(curr, next, &q->task_list, task_list) { unsigned flags = curr->flags; if (curr->func(curr, mode, wake_flags, key) && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive) break; } }
具體處理過程就是遍歷等待隊列上所有等待對象,並執行對應的喚醒函數。針對irqfd的喚醒函數前面已經提到,是irqfd_wakeup,在該函數中會對普通中斷執行schedule_work(&irqfd->inject);這樣對於irqfd注冊的inject工作對象處理函數就會得到執行,於是,中斷就會被執行注入。到這里不妨在看看schedule_work發生了什么
static inline bool schedule_work(struct work_struct *work) { return queue_work(system_wq, work); }
原來如此,該函數把工作對象加入到內核全局的工作隊列中,接下來的處理就有內核自身完成了。
從給eventfd發送信號,到中斷注入的執行流大致如下所示:
以馬內利:
參考資料:
- qemu2.7源碼
- Linux 內核3.10.1源碼