virtio event fd + 中斷 前后端通信機制 +class_init {vhost worker方式}(二)


1. set_guest_notifiers初始化流程
static void virtio_pci_bus_class_init(ObjectClass *klass, void *data){
k->set_guest_notifiers = virtio_pci_set_guest_notifiers;
}
 
 
2. guest_notifier的fdread函數初始化為virtio_queue_guest_notifier_read流程:
vhost_net_start
-> r = k->set_guest_notifiers(qbus->parent, total_queues * 2, true); //調用virtio_pci_set_guest_notifiers
-> virtio_pci_set_guest_notifiers
-> virtio_pci_set_guest_notifier
-> virtio_queue_set_guest_notifier_fd_handler
 
void virtio_queue_set_guest_notifier_fd_handler(VirtQueue *vq, bool assign, bool with_irqfd){
if (assign && !with_irqfd) {
         event_notifier_set_handler(&vq->guest_notifier,virtio_queue_guest_notifier_read);
     } 
}
 
-> qemu_set_fd_handler
-> qemu_set_fd_handler2 (將virtio_queue_guest_notifier_read設置為guest_notifier的fdread函數,並加入到iohandlers中)
 
 
 
void pci_set_irq(PCIDevice *pci_dev, int level)
{
    int intx = pci_intx(pci_dev);
    pci_irq_handler(pci_dev, intx, level);
}

 

 
/* 0 <= irq_num <= 3. level must be 0 or 1 */
static void pci_irq_handler(void *opaque, int irq_num, int level)
{
    PCIDevice *pci_dev = opaque;
    int change;

    change = level - pci_irq_state(pci_dev, irq_num);
    if (!change)
        return;

    pci_set_irq_state(pci_dev, irq_num, level);
    pci_update_irq_status(pci_dev);
    if (pci_irq_disabled(pci_dev))
        return;
    pci_change_irq_level(pci_dev, irq_num, change);
}

 

 
 
3. PCI的中斷處理函數初始化
PCIBus *i440fx_init(){
pci_bus_irqs(b, piix3_set_irq, pci_slot_get_pirq, piix3, PIIX_NUM_PIRQS); //設定bus->set_irq為piix3_set_irq
}
 
 
 
4. notify初始化流程 
virtio_pci_bus_class_init(){
k->notify = virtio_pci_notify;  /*notify注冊為virtio_pci_notify*/
}
 
 
5. 監聽事件FD的過程
在vhost_net_start中,已經將guest_notifier加入到了iohandlers中
 
main
-> main_loop
-> main_loop_wait
-> qemu_iohandler_fill() //將iohandlers中所有的fd和處理函數加入到監聽集合中
-> os_host_main_loop_wait
-> qemu_poll_ns //開始阻塞監聽,返回時候說明有監聽事件發生
 
 
 
6. Guest收包中斷過程
 
os_host_main_loop_wait
-> qemu_poll_ns返回
-> qemu_iohandler_poll 遍歷iohandlers對時間進行處理
-> 遍歷iohandlers,處理所有的event
-> ioh->fd_read(ioh->opaque); //調用fdread函數,也就是virtio_queue_guest_notifier_read
 
-> virtio_queue_guest_notifier_read
-> virtio_irq
-> virtio_notify_vector
-> k->notify(qbus->parent, vector); //調用virtio_pci_notify
 
-> virtio_pci_notify
-> pci_set_irq
-> pci_irq_handler
-> pci_change_irq_level
-> bus->set_irq //調用的是piix3_set_irq
 
-> piix3_set_irq
-> piix3_set_irq_level
-> piix3_set_irq_pic
-> qemu_set_irq //產生中斷

 

一. 概述

 
在上半部已經將GuestOS驅動與QEMU設備交互的過程描述了一下,描述的目的是便於理解Vhost-blk的工作原理,如果想從另外一個角度了解。
分享一個博文鏈接:http://blog.csdn.net/zhuriyuxiao/article/details/8824735 
結合這篇文章,應該可以更好的理解virtio-block的原理。
言歸正傳,上部分總結到,GuestOS中virtio-block驅動其實只是一個請求觸發,並且要一個請求處理結果,對於GuestOS virtio-blk驅動的對外接口如下:
1. virtio-blk驅動將IO請求通過virtio_queue同步給QEMU后,通過iowrite16寫一個pci地址。
2.等虛擬硬件處理完IO請求以后,將請求結果通過virtio_queue同步回來, 給GuestOS一個中斷,調用中斷處理函數,處理IO請求的結果就OK
既然virtio-blk對外接口我們能確定,就算使用vhost-blk,也要遵循上面的接口,才能讓GuestOS驅動正常運行。
后面就圍繞着vhost-blk如何完成這些工作描述它的工作原理。
 

二. Vhost-blk架構

 
按照慣例,先上圖:
如上圖,與上部分virtio-blk的架構圖有些區別,主要還是在handle_output到Disk的部分.
從GuestOS到kernel 和 kernel到GuestOS驅動 兩個黑色箭頭無論是否有vhost-blk都時一樣的,就是上面介紹的兩個接口。
值得關注的是在kernel部分, 有一個vhost-blk模塊,他是在驅動層(上半部分已經提到過)。
如果QEMU開啟vhost-blk,handle_output就會跳過vfs,fs等kernel層, 通過vhost-blk模塊直接將請求提交給硬件,所以要補充,vhost-blk開啟后,QEMU后端只能是block描述符,不能是一個文件,在vhost-blk內核模塊中,會檢查。
當vhost-blk執行完畢會返回到QEMU,但我用白色箭頭表示,意 思是vhost-blk將IO請求結果返回給GuestOS,並沒有真正等到內核態切回QEMU的用戶態再執行,而是直接從vhost-blk層就提交了中斷,至於大家好奇, 怎么從內核態就能通知QEMU用戶態程序觸發中斷給GuestOS呢,這里QEMU利用了一個巧妙的通知機制,慢慢為大家分享。
 
 

三. IO的傳送流 

 
首先,因為上半部分已經為大家介紹過,virtio-blk是通過一個virtio_queue將IO請求同步給QEMU,當開啟vhost-blk后, QEMU又將virtio_queue的數據,解析到vhost_queue的結構體中,QEMU通過vhost_queue將IO請求同步 給host kernel的vhost_blk模塊中,然后vhost_blk將vhost_queue的IO請求解析成一個bio,submit_io提交一個IO請求給硬件,二話不說,再上個圖:
上面這個圖不需要更多語言描述,看不懂就私信給我,我是不會回的^_^
廣說原理也要有點依據,要依據,分享兩個函數:
 
Fall in kernel
vhost_blk_ioctl

Virtio Queue  ----> Vhost Queue
void vhost_virtqueue_start(struct vhost_dev *dev,
                                   struct VirtIODevice *vdev,
                                   struct vhost_virtqueue *vq,
                                   unsigned idx)

Qemu Vhost Queue ---> Kernel Vhost Queue
int vhost_virtqueue_set_addr(struct vhost_dev *dev,
                                       struct vhost_virtqueue *vq,
                                    unsigned idx, bool enable_log)
第一個函數,不用說 ,QEMU與vhost-blk都要通過ioctl來完成。
第二個函數和第三個函數,在QEMU中,看懂自然明白。
如果您確實把這三個函數看懂了,我再上個圖,你會更清晰。
這個圖的左半部就是virtio-blk的機制,當增加了vhost-blk,就增加了右半部。
GuestOS與QEMU之間有個VirtioQ,而QEMU與Vhost-blk之間有個VhostQ,VirtioQ通過QEMU轉化成VhostQ,用於與Vhost-Blk同步請求。
當Vhost將請求處理完,再將結果放到VhostQ中的used中后,通知QEMU給GuestOS發送一個中斷,Guest中斷處理函數的處理方法參考上半部分享。
那么要補充的是,VirtioQ與VhostQ的同步也只是地址的同步,並沒有數據的同步,所以VhostQ中的數據,也是VirtioQ中的數據。
目前為止,一個IO請求如何通過vhost-blk提交到硬件層,應該有個大致的了解。
上圖又引入兩個重要函數:
Host_kick與Guest_kick,在第四章為大家分享。
 

四. 重要函數

 

1. Vhost_blk重要函數

vhost_blk_handle_guest_kick:當一個IO請求同步到vhost_blk的vhost_queue中時,vhost-blk會調用此函數,將vhost_queue中的IO請求解析成一個bio,通過submit_io提交給硬件層。注意:這個函數在vhost_blk初始化的時候注冊給【work_poll->work->fn】
 
vhost_blk_handle_host_kick:當一個IO請求處理完畢后,會調用此函數,將IO請求的處理結果同步到vhost_queue中,也就是同步到virtio_queue中,並 通知QEMU觸發一個GuestOS的中斷,通知GuestOS調用中斷處理函數,處理IO返回的結果。注意:這 個函數在vhost_blk初始化的時候注冊給【blk->work->fn】
 
紅色部分是一個遺留問題,后面為大家講解如何在內核中通知用戶程序的QEMU觸發中斷的。
先看vhost-blk是如何調用這兩個函數的?引入了一個重要的內核線程【Vhost Worker thread】,再上個圖:
 
如上圖所示:首先紫色模塊vhost_dev_set_owner,創建一個vhost_worker的線程(調用kthread_create),但不是一直運行的,使用通過喚醒函數進行喚醒。
這個線程的主要工作就是不停的在work_list取出之前被注冊進來的work,調用work->fn函數,那整個流程如下:
1. 當IO請求被同步到vhost_queue中時,QEMU通過ioctl通知vhost_blk調用vhost_poll_start。
2. vhost_poll_start將poll->work通過vhost_work_queue注冊給work_list,並調用喚醒函數喚醒vhost_worker線程。
3. vhost_worker取出work_list頭的work,並調用work的fn,這個work->fn就是 vhost_blk_handle_guest_kick。
4. 當vhost_blk_handle_guest_kick后,會調用vhost_blk_req_done。
5. vhost_blk_req_done函數將blk->work通過vhost_work_queue添加到work_list的隊尾,並啟動進程。
6. 與第四步一樣,此時work->fn就是 vhost_blk_handle_host_kick。
 

2. QEMU中的重要函數

到目前位置,已經描述了Vhost-blk最重要的兩個函數,其中 vhost_blk_handle_host_kick在處理完請求后,需要通知QEMU向GuestOS發送一個中斷,那么這個通知機制是如何完成的呢?
請看下面兩個函數
virtio_queue_host_notifier_read:當GuestOS的virtio_blk把IO請求同步到virtio_queue中時,會調用此函數,此函數實際就是調用handle_output去處理IO請求。
virtio_queue_guest_notifier_read:當Vhost-blk處理完請求時,通過vhost_blk_handle_host_kick發送信號,讓Qemu調用此函數,為GuestOS發送一個中斷。
 
這里必須要引入一個概念eventfd。
它是一個系統調用,它會返回一個描述符,描述符實際是一個對象,這個對象包含一個由內核維護的計數器。
當read這個描述符的時候,如果這個描述符的對象計數器大於0,read將會返回,並且將計數器清0。
當write這個描述符的時候,就是將一個uint64的數值寫到計數器中。
當poll這個描述符的時候,如果計數器大於0,poll會認為它是可讀的,如果計數器等於0,poll認為它是不可讀的。(select亦如此)
 
QEMU中有個輪詢io_handler的主循環,還是要貼一個鏈接:http://blog.csdn.net/zhuriyuxiao/article/details/8835593
簡而言之,這個主循環在poll這個io_handler列表中所有的描述符,當poll返回值為真的,就會調用該描述符對應的函數。
 
說回來,QEMU就是分別創建了一個host_notifier和一個guest_notifier兩個eventfd,並將這兩個eventfd綁定上面的兩個函數:
virtio_queue_host_notifier_read -> host_notifier
virtio_queue_guest_notifier_read -> guest_notifier
並將這兩個eventfd注冊到io_handler里面。
 
最關鍵的是,QEMU將host_notifier通過ioctl注冊給內核vhost-blk中的kick。將guest_notifer通過ioctl注冊給內核vhost-blk中的call。
 
到這,大家是不是看出點門道,當在內核vhost-blk中,給call的計數器做+1操作,QEMU中的guest_notifier就會被poll認為是可讀的,io_handler主循環中就會調用 virtio_queue_guest_notifier_read給GuestOS發中斷。
那么vhost-blk的kick又是干什么的呢?
每次vhost-blk啟動vhost_worker線程之前,都要檢測一下kick是不是可讀,如果可讀,才會啟動線程,處理vhost_queue中的IO請求,而在QEMU中的 virtio_queue_host_notifier_read函數中,調用handle_output之前,需要將host_notifier的計數器清零,言外之意,就是QEMU中的handle_output與vhost-blk的 vhost_blk_handle_guest_kick不能同時進行,因為他們都會操作vhost_queue,這是保證一個vhost_queue處理的完整性。
 
 

五. 總結

總結必不可少,那就是vhost-blk到底優化空間又多少,先把兩張老圖拿出來對比下:
 
第一張圖是沒有vhos-blk架構圖,紅色箭頭發出一次IO請求,要走完第三張圖的vfs層一直到Block Device層,而請求處理完畢后,要返回到QEMU,將IO請求的處理結果填充到Virtio_queue中,再觸發中斷,讓GuestOS中斷處理函數對IO請求的結果進行處理。
 
第二張圖是有vhost-blk的架構圖,與紅色箭頭處對應的是,直接跳過其他層,到Block Device Driver層,提交IO請求,當IO請求的結果到Vhost-blk驅動中,在block device driver層將請求結果填充到vhost_queue中,就通知QEMU觸發一次中斷,讓GuestOS中斷處理函數對IO請求結果進行處理。
 
也就是說開啟vhost-blk的IO路徑 從請求發出到請求結果返回,兩次都有縮短。
 
但是!!但是優化空間到底有多少?
在上文提到過,vhost-blk開啟后,QEMU后端必須是block設備(可以是邏輯卷LV),那么上面第三個圖,表示QEMU后端為一個Raw文件的IO路徑。
這樣是不在一個基准線上的競爭,假如不開啟vhost-blk,QEMU的virtio-blk后端為一個LV的話, 我們知道LVM是建立在硬盤和分區之上的一個邏輯層,接近於驅動層。
 
大家應該明白我的結論,我接觸QEMU不久,而且以上都是基於代碼分析出來的,沒有官網給出結論,所以有任何不同意見,歡迎提出來,大家一起把這個社區沒接受的 非官方的 大家都好奇的vhost-blk搞清楚。

 

 

 

 

 virtio_queue_set_guest_notifier_fd_handler

 

 

host是virtio的另一種方案,用於跳過qemu,減少qemu和內核之間上下文切換的開銷,對於網絡IO而言提升尤其明顯。vhost目前有兩種實現方案,內核態和用戶態,本文重點討論內核態的vhost

vhost內核模塊主要處理數據面的事情,控制面上還是交給qemu, 

 

 

下面來看下vhost的數據流,vhost與kvm模塊之間通過eventfd來實現,guest到host方向的kick event,通過ioeventfd實現,host到guest方向的call event,通過irqfd實現

 

host到guest方向

 

-> r = k->set_guest_notifiers(qbus->parent, total_queues * 2, true); //調用virtio_pci_set_guest_notifiers 1、-> virtio_pci_set_guest_notifiers --> kvm_vm_ioctl 2、 -> virtio_pci_set_guest_notifier-> virtio_queue_set_guest_notifier_fd_handler

1、有中斷irqfd通過kvm_vm_ioctl來設置kvm模塊的irqfd

首先host處理used ring,然后判斷如果KVM_IRQFD成功設置,kvm模塊會通過irqfd把中斷注入guest。qemu是通過virtio_pci_set_guest_notifiers -> kvm_virtio_pci_vector_use -> kvm_virtio_pci_irqfd_use -> kvm_irqchip_add_irqfd_notifier -> kvm_irqchip_assign_irqfd最終調用kvm_vm_ioctl來設置kvm模塊的irqfd的,包含write fd和read fd(可選)

static int kvm_virtio_pci_vector_use(VirtIOPCIProxy *proxy, int nvqs)
{
    PCIDevice *dev = &proxy->pci_dev;
    VirtIODevice *vdev = virtio_bus_get_device(&proxy->bus);
    VirtioDeviceClass *k = VIRTIO_DEVICE_GET_CLASS(vdev);
    unsigned int vector;
    int ret, queue_no;
    MSIMessage msg;

    for (queue_no = 0; queue_no < nvqs; queue_no++) {
        if (!virtio_queue_get_num(vdev, queue_no)) {
            break;
        }
        vector = virtio_queue_vector(vdev, queue_no);
        if (vector >= msix_nr_vectors_allocated(dev)) {
            continue;
        }
        msg = msix_get_message(dev, vector);
        ret = kvm_virtio_pci_vq_vector_use(proxy, queue_no, vector, msg);
        if (ret < 0) {
            goto undo;
        }
        /* If guest supports masking, set up irqfd now.
         * Otherwise, delay until unmasked in the frontend.
         */
        if (k->guest_notifier_mask) {
            ret = kvm_virtio_pci_irqfd_use(proxy, queue_no, vector);
            if (ret < 0) {
                kvm_virtio_pci_vq_vector_release(proxy, vector);
                goto undo;
            }
        }
    }
    return 0;

undo:
    while (--queue_no >= 0) {
        vector = virtio_queue_vector(vdev, queue_no);
        if (vector >= msix_nr_vectors_allocated(dev)) {
            continue;
        }
        if (k->guest_notifier_mask) {
            kvm_virtio_pci_irqfd_release(proxy, queue_no, vector);
        }
        kvm_virtio_pci_vq_vector_release(proxy, vector);
    }
    return ret;
}

 

 

 

2、如果沒有設置irqfd,則guest notifier fd會通知到等待fd的qemu進程,進入注冊函數virtio_queue_guest_notifier_read,調用virtio_irq,最終調用到virtio_pci_notify

static void virtio_queue_guest_notifier_read(EventNotifier *n)
{
    VirtQueue *vq = container_of(n, VirtQueue, guest_notifier);
    if (event_notifier_test_and_clear(n)) {
        virtio_irq(vq);
    }
}

void virtio_irq(VirtQueue *vq)
{
    trace_virtio_irq(vq);
    vq->vdev->isr |= 0x01;
    virtio_notify_vector(vq->vdev, vq->vector);
}

static void virtio_notify_vector(VirtIODevice *vdev, uint16_t vector)
{
    BusState *qbus = qdev_get_parent_bus(DEVICE(vdev));
    VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);

    if (k->notify) {
        k->notify(qbus->parent, vector);
    }
}

static void virtio_pci_notify(DeviceState *d, uint16_t vector)
{
    VirtIOPCIProxy *proxy = to_virtio_pci_proxy_fast(d);

    if (msix_enabled(&proxy->pci_dev))
        msix_notify(&proxy->pci_dev, vector);
    else {
        VirtIODevice *vdev = virtio_bus_get_device(&proxy->bus);
        pci_set_irq(&proxy->pci_dev, vdev->isr & 1);
    }
}

 

1 技術簡介

1.1 virtio-net 簡介

virtio-net 在 guest 前端驅動 kick 后端驅動時,采用 I/O 指令方式退出到 host KVM。kvm 通過 eventfd_signal 喚醒阻塞的 qemu 線程。qemu 通過 vring 處理報文。qemu 把報文從用戶態傳送給 tap 口。

1.2 vhost-net 簡介

與 virtio-net 不同的是,eventfd_signal 喚醒的是內核 vhost_worker 進程。vhost_worker 從 vring 提取報文數據,然后發送給 tap。與 virtio-net 相比,vhost-net 處理數據在內核態,在發送到 tap 口的時候少了一次數據的拷貝。

1.3 ovs 轉發涉及的模塊概要

VM->VM 流程:

 

 

2 virtio-net.ko 前端驅動部分

2.1 guest->host 數據發送

當前端 virtio-net 有想發送的報文數據時將會 kick 后端,右面是前端 kick 后端的流程。前端調用 xmit_skb 發送數據,virtqueue_add_outbuf 是把 sk_buff 里的內容(frag[]數組)逐一的填入 scatterlist 數組中。這里可以理解成填寫分散聚合描述符表。

但前端和后端數據傳遞是通過 struct vring_desc 傳遞的,所以 virtqueue_add() 再把 struct scatterlist 里的數據填寫到 struct vring_desc 里。

struct vring_desc 這個數據結構的使用,后面我們再詳細說。

最后通過 vq->notify(&vq->vq) (vp_notify()) kick 后端,后續流程到了 kvm.ko 部分的第 4 小節。

2.2 guest->host 代碼流程

 

 

2.3 host->guest 數據發送

guest 通過 NAPI 接口的 virtnet_poll 接收數據,通過 virtqueue_get_buf_ctx 從 Vring 中獲取報文數據。再通過 receive_buf 把報文數據保存到 skb 中。

這樣目的端就成功接收了來自源端的報文。

2.4 host->guest 代碼流程

 

 

3 kvm.ko 部分

3.1 eventfd 注冊

 

 

由上圖可見 eventfd 的注冊是在 qemu 中發起的。qemu 調用 kvm 提供的系統調用。

3.2 eventfd 通知流程

eventfd 一半的用法是用戶態通知用戶態,或者內核態通知用戶態。例如 virtio-net 的實現是 guest 在 kick host 時采用 eventfd 通知的 qemu,然后 qemu 在用戶態做報文處理。但 vhost-net 是在內核態進行報文處理,guest 在 kick host 時采用 eventfd 通知的是內核線程 vhost_worker。所以這里的用法就跟常規的 eventfd 的用法不太一樣。

下面介紹 eventfd 通知的使用。

eventfd 核心數據結構:

  1. struct eventfd_ctx {
  2. struct kref kref;
  3. wait_queue_head_t wqh;
  4. __u64 count;
  5. unsigned int flags;
  6. };

eventfd 的數據結構其實就是包含了一個等待隊列頭。當調用 eventfd_signal 函數時就是喚醒 wgh 上等待隊列。

  1. __u64 eventfd_signal(struct eventfd_ctx *ctx, __u64 n)
  2. {
  3. unsigned long flags;
  4. spin_lock_irqsave(&ctx->wqh.lock, flags);
  5. if (ULLONG_MAX - ctx->count < n)
  6. n = ULLONG_MAX - ctx->count;
  7. ctx->count += n;
  8. if (waitqueue_active(&ctx->wqh))
  9. wake_up_locked_poll(&ctx->wqh, POLLIN);
  10. spin_unlock_irqrestore(&ctx->wqh.lock, flags);
  11. return n;
  12. }
  13. #define wake_up_locked_poll(x, m) \
  14. __wake_up_locked_key((x), TASK_NORMAL, (void *) (m))
  15. void __wake_up_locked_key(struct wait_queue_head *wq_head, unsigned int mode, void *key)
  16. {
  17. __wake_up_common(wq_head, mode, 1, 0, key, NULL);
  18. }
  19. static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode,
  20. int nr_exclusive, int wake_flags, void *key,
  21. wait_queue_entry_t *bookmark)
  22. {
  23. ...
  24. list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
  25. unsigned flags = curr->flags;
  26. int ret;
  27. if (flags & WQ_FLAG_BOOKMARK)
  28. continue;
  29. ret = curr->func(curr, mode, wake_flags, key); /* 調用vhost_poll_wakeup */
  30. if (ret < 0)
  31. break;
  32. if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
  33. break;
  34. if (bookmark && (++cnt > WAITQUEUE_WALK_BREAK_CNT) &&
  35. (&next->entry != &wq_head->head)) {
  36. bookmark->flags = WQ_FLAG_BOOKMARK;
  37. list_add_tail(&bookmark->entry, &next->entry);
  38. break;
  39. }
  40. }
  41. return nr_exclusive;
  42. }
  43. static int vhost_poll_wakeup(wait_queue_entry_t *wait, unsigned mode, int sync,
  44. void *key)
  45. {
  46. struct vhost_poll *poll = container_of(wait, struct vhost_poll, wait);
  47. if (!((unsigned long)key & poll->mask))
  48. return 0;
  49. vhost_poll_queue(poll);
  50. return 0;
  51. }
  52. void vhost_poll_queue(struct vhost_poll *poll)
  53. {
  54. vhost_work_queue(poll->dev, &poll->work);
  55. }
  56. void vhost_work_queue(struct vhost_dev *dev, struct vhost_work *work)
  57. {
  58. if (!dev->worker)
  59. return;
  60. if (!test_and_set_bit(VHOST_WORK_QUEUED, &work->flags)) {
  61. /* We can only add the work to the list after we're
  62. * sure it was not in the list.
  63. * test_and_set_bit() implies a memory barrier.
  64. */
  65. llist_add(&work->node, &dev->work_list); /* 添加到 dev->work_list)*/
  66. wake_up_process(dev->worker); /* 喚醒vhost_worker線程 */
  67. }
  68. }

這里有一個疑問,就是 vhost_worker 什么時候加入到 eventfd 的 wgh 字段的,__wake_up_common 函數里 curr->func 又是什么時候被設置成 vhost_poll_wakeup 函數的呢?請看下一節。

3.3 eventfd 與 vhost_worker 綁定

vhost.ko 創建了一個字符設備,vhost_net_open 在打開這個設備文件的時候會調用 vhost_net_open 函數。這里為 vhost_dev 設備進行初始化。

  1. static int vhost_net_open(struct inode *inode, struct file *f)
  2. {
  3. ...
  4. dev = &n->dev;
  5. vqs[VHOST_NET_VQ_TX] = &n->vqs[VHOST_NET_VQ_TX].vq;
  6. vqs[VHOST_NET_VQ_RX] = &n->vqs[VHOST_NET_VQ_RX].vq;
  7. n->vqs[VHOST_NET_VQ_TX].vq.handle_kick = handle_tx_kick;
  8. n->vqs[VHOST_NET_VQ_RX].vq.handle_kick = handle_rx_kick;
  9. ...
  10. vhost_poll_init(n->poll + VHOST_NET_VQ_TX, handle_tx_net, POLLOUT, dev);
  11. vhost_poll_init(n->poll + VHOST_NET_VQ_RX, handle_rx_net, POLLIN, dev);
  12. f->private_data = n;
  13. return 0;
  14. }
  15. void vhost_poll_init(struct vhost_poll *poll, vhost_work_fn_t fn,
  16. unsigned long mask, struct vhost_dev *dev)
  17. {
  18. init_waitqueue_func_entry(&poll->wait, vhost_poll_wakeup); /* 給curr->fn賦值 vhost_poll_wakeup */
  19. init_poll_funcptr(&poll->table, vhost_poll_func); /* 給poll_table->_qproc賦值vhost_poll_func */
  20. poll->mask = mask;
  21. poll->dev = dev;
  22. poll->wqh = NULL;
  23. vhost_work_init(&poll->work, fn); /* 給 work->fn 賦值為handle_tx_net和handle_rx_net */
  24. }

qemu 使用 ioctl 系統調用 VHOST_SET_VRING_KICK 時會把 eventfd 的 struct file 指針付給 pollstart 和 pollstop,同時調用 vhost_poll_start()

  1. long vhost_vring_ioctl(struct vhost_dev *d, int ioctl, void __user *argp)
  2. {
  3. ...
  4. case VHOST_SET_VRING_KICK:
  5. if (copy_from_user(&f, argp, sizeof f)) {
  6. r = -EFAULT;
  7. break;
  8. }
  9. eventfp = f.fd == -1 ? NULL : eventfd_fget(f.fd);
  10. if (IS_ERR(eventfp)) {
  11. r = PTR_ERR(eventfp);
  12. break;
  13. }
  14. if (eventfp != vq->kick) {
  15. pollstop = (filep = vq->kick) != NULL;
  16. pollstart = (vq->kick = eventfp) != NULL;
  17. } else
  18. filep = eventfp;
  19. break;
  20. ...
  21. if (pollstart && vq->handle_kick)
  22. r = vhost_poll_start(&vq->poll, vq->kick);
  23. ...
  24. }
  25. int vhost_poll_start(struct vhost_poll *poll, struct file *file)
  26. {
  27. unsigned long mask;
  28. int ret = 0;
  29. if (poll->wqh)
  30. return 0;
  31. mask = file->f_op->poll(file, &poll->table); /* 執行eventfd_poll */
  32. if (mask)
  33. vhost_poll_wakeup(&poll->wait, 0, 0, (void *)mask);
  34. if (mask & POLLERR) {
  35. vhost_poll_stop(poll);
  36. ret = -EINVAL;
  37. }
  38. return ret;
  39. }
  40. static unsigned int eventfd_poll(struct file *file, poll_table *wait)
  41. {
  42. struct eventfd_ctx *ctx = file->private_data;
  43. unsigned int events = 0;
  44. u64 count;
  45. poll_wait(file, &ctx->wqh, wait);
  46. 。。。
  47. }
  48. static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
  49. {
  50. if (p && p->_qproc && wait_address)
  51. p->_qproc(filp, wait_address, p); /* 調用vhost_poll_func */
  52. }
  53. static void vhost_poll_func(struct file *file, wait_queue_head_t *wqh,
  54. poll_table *pt)
  55. {
  56. struct vhost_poll *poll;
  57. poll = container_of(pt, struct vhost_poll, table);
  58. poll->wqh = wqh;
  59. add_wait_queue(wqh, &poll->wait);
  60. }

關鍵數據結構關系如下圖:

 

 

3.4 guest->host 的通知流程(喚醒 vhost_worker 線程)

Kick host 的原理是通過 io 指令實現的。前端執行 io 指令,就會發生 vm exit。KVM 捕捉到 vm exit 會去查詢退出原因,由於是 io 指令,所以執行對應的 handle_io 處理。handle_io() 從 exit_qualification 中得到 io 操作地址。kvm_fast_pio_out() 會根據 io 操作的地址找到對應的處理函數。第 1 小節 eventfd 注冊的流程可知,kvm_fast_pio_out() 最終會調用 eventfd 對應的回調函數 ioeventfd_write()。再根據第 3 小節可知 eventfd 最終會喚醒 vhost_worker 內核進程

 

 

流程進入 vhost.ko 的第3小節。

3.5 host 給 guest 注入中斷

到目前位置,發送給 guest 的報文已經准備好了。通過注入中斷通知 guest 接收報文。這里要為虛機的 virtio-net 設備模擬一個 MSI 中斷,並且准備了中斷向量號。調用 vmx_deliver_posted_interrupt 給目的 VCPU 線程所在的物理核注入終端。

流程將跳轉到 virtio-net.ko 前端驅動的第3小節。

3.6 host 給 guest 注入中斷代碼流程 ------------------------非host方式通過virtio_notify_irqfd

 

 

 

4 vhost.ko 部分

前面有提到 vhost_worker 線程被喚醒后將執行 vhost_poll_init() 函數這冊的 handle_tx_net 和 handle_rx_net 函數。

4.1 vhost_worker 線程創建

  1. long vhost_dev_set_owner(struct vhost_dev *dev)
  2. {
  3. ...
  4. /* No owner, become one */
  5. dev->mm = get_task_mm(current);
  6. worker = kthread_create(vhost_worker, dev, "vhost-%d", current->pid);
  7. if (IS_ERR(worker)) {
  8. err = PTR_ERR(worker);
  9. goto err_worker;
  10. }
  11. dev->worker = worker;
  12. wake_up_process(worker); /* avoid contributing to loadavg */
  13. err = vhost_attach_cgroups(dev);
  14. if (err)
  15. goto err_cgroup;
  16. err = vhost_dev_alloc_iovecs(dev);
  17. if (err)
  18. goto err_cgroup;
  19. ...
  20. }

讓 vhost-dev 的 worker 指向剛創建出的 worker 線程。

4.2 vhost_worker 實現

  1. static int vhost_worker(void *data)
  2. {
  3. struct vhost_dev *dev = data;
  4. struct vhost_work *work, *work_next;
  5. struct llist_node *node;
  6. mm_segment_t oldfs = get_fs();
  7. set_fs(USER_DS);
  8. use_mm(dev->mm);
  9. for (;;) {
  10. /* mb paired w/ kthread_stop */
  11. set_current_state(TASK_INTERRUPTIBLE);
  12. if (kthread_should_stop()) {
  13. __set_current_state(TASK_RUNNING);
  14. break;
  15. }
  16. node = llist_del_all(&dev->work_list); /*vhost_work_queue 添加 */
  17. if (!node)
  18. schedule();
  19. node = llist_reverse_order(node);
  20. /* make sure flag is seen after deletion */
  21. smp_wmb();
  22. llist_for_each_entry_safe(work, work_next, node, node) {
  23. clear_bit(VHOST_WORK_QUEUED, &work->flags);
  24. __set_current_state(TASK_RUNNING);
  25. work->fn(work); /* 由vhost_poll_init賦值 handle_tx_net和handle_rx_net*/
  26. if (need_resched())
  27. schedule();
  28. }
  29. }
  30. unuse_mm(dev->mm);
  31. set_fs(oldfs);
  32. return 0;
  33. }

從代碼可以看到在循環的開始部分是摘除 dev->work_list 鏈表中的頭表項。這里如果鏈表為空則返回 NULL,如果鏈表不為空則返回頭結點。如果鏈表為空則調用 schedule() 函數 vhost_worker 進程進入阻塞狀態,等待被喚醒。

當 vhost_worker 被喚醒后將執行 fn 函數,對於 vhost-net 將被賦值為 handle_tx_net 和 handle_rx_net

4.3 從 guest->host 方向的發送報文函數 handle_tx_net

handle_tx_net 的代碼邏輯比較短,里面直接調用了 tun.ko 的接口函數發送報文。流程走到了 tun.ko 章節的第1小節。

  1. static void handle_tx_net(struct vhost_work *work)
  2. {
  3. struct vhost_net *net = container_of(work, struct vhost_net,
  4. poll[VHOST_NET_VQ_TX].work);
  5. handle_tx(net);
  6. }
  7. static void handle_tx(struct vhost_net *net)
  8. {
  9. ...
  10. for (;;) {
  11. ...
  12. /* TODO: Check specific error and bomb out unless ENOBUFS? */
  13. err = sock->ops->sendmsg(sock, &msg, len); /* tup.c中定義 tup_sendmsg ()*/
  14. if (unlikely(err < 0)) {
  15. ...
  16. }
  17. out:
  18. mutex_unlock(&vq->mutex);
  19. }

4.4 guest->host 代碼流程

 

 

4.5 從 host->guest 方向的接收

vhost-worker 進程調用 handle_rx_netvhost_add_used_and_signal_n 負責從 vring 中接收報文,vhost_signal 函數通知 guest 報文的到來。目前都是通過注入中斷的方式通知 guest。 流程將跳轉到 kvm.ko 的第5小節。

4.6 host->guest 代碼流程

host->guest 方向:

 

 

5 tun.ko 部分

5.1 報文發送處理流程

 

 

tun 模塊首先通過調用 __napi_schedue() 接口去掛起 NET_RX_SOFTIRQ 軟中斷的,並且調度的是 sd->backlog 這個 struct napi。然后在 tun_rx_batched() 函數在使能中斷下半部時會調用 do_softirq(),從而執行剛剛掛起的 NET_RX_SOFTIRQ 對應的 net_rx_action 軟中斷響應函數 net_rx_acitonnet_rx_action 會執行 sd->backlog 對應的 napi 接口函數。process_backlog 是內核的 netdev 在初始化時在每 CPU 變量中填入的 struct napi_struct 結構體。最后從 process_backlog 執行到 openvswitch 注冊的 hook 函數 netdev_frame_hook (openvswitch.ko 第 2小節)。

流程將跳轉到 openvswitch.ko 第3小節。

5.2 process_backlog 的注冊

  1. static int __init net_dev_init(void)
  2. {
  3. int i, rc = -ENOMEM;
  4. for_each_possible_cpu(i) { /* 遍歷各個CPU的每CPU變量 */
  5. struct work_struct *flush = per_cpu_ptr(&flush_works, i);
  6. struct softnet_data *sd = &per_cpu(softnet_data, i); /* sd是個每CPU變量 */
  7. INIT_WORK(flush, flush_backlog);
  8. skb_queue_head_init(&sd->input_pkt_queue);
  9. skb_queue_head_init(&sd->process_queue);
  10. INIT_LIST_HEAD(&sd->poll_list);
  11. sd->output_queue_tailp = &sd->output_queue;
  12. #ifdef CONFIG_RPS
  13. sd->csd.func = rps_trigger_softirq;
  14. sd->csd.info = sd;
  15. sd->cpu = i;
  16. #endif
  17. sd->backlog.poll = process_backlog; /* 定義napi_struct的poll函數 */
  18. sd->backlog.weight = weight_p;
  19. }
  20. if (register_pernet_device(&loopback_net_ops))
  21. goto out;
  22. if (register_pernet_device(&default_device_ops))
  23. goto out;
  24. open_softirq(NET_TX_SOFTIRQ, net_tx_action); /* 設置軟中斷NET_TX_SOFTIRQ的響應函數 */
  25. open_softirq(NET_RX_SOFTIRQ, net_rx_action); /*設置軟中斷NET_RX_SOFTIRQ的響應函數 */
  26. rc = cpuhp_setup_state_nocalls(CPUHP_NET_DEV_DEAD, "net/dev:dead",
  27. NULL, dev_cpu_dead);
  28. WARN_ON(rc < 0);
  29. rc = 0;
  30. out:
  31. return rc;
  32. }

6 openvswitch 部分

openvswitch.ko 作為 openvswitch 的一個內核模塊內核態報文的接收和轉發。通過給 tun 設備掛接 hook 函數,來處理 tun 接收和發送的報文。在創建虛機時給虛機分配的 vnet 口會暴露給 host,我們一般通過 xml 文件指定到橋入那個 ovs 網橋。在橋入的時候,用戶態代碼通過 netlink 與 openvswitch.ko 進行通信。把 vnet 口橋入 ovs 網橋時會給 vnet 這個設備掛 netdev_frame_hook 鈎子函數。

當 ovs 添加一個 vport 時會通過 netlink 發送到 openvswitch.ko,openvswitch 注冊的 netlink 處理函數負責處理相關命令。

  1. static struct genl_ops dp_vport_genl_ops[] = {
  2. { .cmd = OVS_VPORT_CMD_NEW,
  3. .flags = GENL_UNS_ADMIN_PERM, /* Requires CAP_NET_ADMIN privilege. */
  4. .policy = vport_policy,
  5. .doit = ovs_vport_cmd_new /* OVS_VPORT_CMD_NEW消息的 */
  6. },
  7. { .cmd = OVS_VPORT_CMD_DEL,
  8. .flags = GENL_UNS_ADMIN_PERM, /* Requires CAP_NET_ADMIN privilege. */
  9. .policy = vport_policy,
  10. .doit = ovs_vport_cmd_del
  11. },
  12. { .cmd = OVS_VPORT_CMD_GET,
  13. .flags = 0, /* OK for unprivileged users. */
  14. .policy = vport_policy,
  15. .doit = ovs_vport_cmd_get,
  16. .dumpit = ovs_vport_cmd_dump
  17. },
  18. { .cmd = OVS_VPORT_CMD_SET,
  19. .flags = GENL_UNS_ADMIN_PERM, /* Requires CAP_NET_ADMIN privilege. */
  20. .policy = vport_policy,
  21. .doit = ovs_vport_cmd_set,
  22. },
  23. };
  24. struct genl_family dp_vport_genl_family __ro_after_init = {
  25. .hdrsize = sizeof(struct ovs_header),
  26. .name = OVS_VPORT_FAMILY,
  27. .version = OVS_VPORT_VERSION,
  28. .maxattr = OVS_VPORT_ATTR_MAX,
  29. .netnsok = true,
  30. .parallel_ops = true,
  31. .ops = dp_vport_genl_ops,
  32. .n_ops = ARRAY_SIZE(dp_vport_genl_ops),
  33. .mcgrps = &ovs_dp_vport_multicast_group,
  34. .n_mcgrps = 1,
  35. .module = THIS_MODULE,
  36. };
  37. static struct genl_family *dp_genl_families[] = {
  38. &dp_datapath_genl_family,
  39. &dp_vport_genl_family,
  40. &dp_flow_genl_family,
  41. &dp_packet_genl_family,
  42. &dp_meter_genl_family,
  43. };
  44. static int __init dp_register_genl(void)
  45. {
  46. int err;
  47. int i;
  48. for (i = 0; i < ARRAY_SIZE(dp_genl_families); i++) {
  49. err = genl_register_family(dp_genl_families[i]); 注冊netlink處理函數
  50. if (err)
  51. goto error;
  52. }
  53. return 0;
  54. error:
  55. dp_unregister_genl(i);
  56. return err;
  57. }

6.2 netdev_frame_hook 函數的注冊

 

 

6.3 ovs 對報文的轉發流程

OVS 首先通過 key 值找到對應的流表,然后轉發到對應的端口。這篇文章的重點是講解 vhost 的流程,OVS 具體流程並不是我們的講解的重點。所以這方面有什么疑問請大家自行搜索一下 OVS 的資料。

這段代碼的大體目的就是找到目的虛機所在的端口,也就是目的虛機所在的 vnet 端口。

 

 

流程跳轉到內核部分第1小節。

7 內核部分

7.1 發送報文喚醒目的端的 vhost-worker 進程

內核的發送函數 __dev_queue_xmit 將會找到 vnet 設備對應的等待隊列,並喚醒等待隊列里對應的進程。這里將喚醒的進程就是 vhost_worker 進程了。

流程跳轉到 vhost.ko 的第5小節。

7.2 代碼流程

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM