virtIO之VHOST工做原理簡析

2017-07-19node


1、前言 linux

以前有分析過虛擬化環境下virtIO的實現,virtIO相關於傳統的虛擬IO在性能方面的確提升了很多,可是按照virtIO虛擬網卡爲例,每次虛擬機接收數據包的時候,數據包從linux bridge通過tap設備發送到用戶空間,這是一層數據的複製而且伴有內核到用戶層的切換,而在用戶空間virtIO後端驅動把數據寫入到虛擬機內存後還須要退到KVM中,從KVM進入虛擬機,又增長了一次模式的切換。在IO比較頻繁的狀況下,會形成模式切換次數過多從而下降性能。而vhost便解決了這個問題。把後端驅動從qemu中遷移到內核中,做爲一個獨立的內核模塊存在,這樣在數據到來的時候,該模塊直接監聽tap設備,在內核中直接把數據寫入到虛擬機內存中,而後通知虛擬機便可,這樣就和qemu解耦和,減小了模式切換次數和數據複製次數,提升了性能。下面介紹下vhost的初始化流程。後端

2、 總體框架網絡

介紹VHOST主要從三個部分入手:vHOST內核模塊,qemu部分、KVM部分。而qemu部分主要是virtIO部分。本節不打算分析具體的工做代碼,由於基本原理和VIRTIO相似,且要線性的描述vhost也並不是易事。框架

一、vHOST 內核模塊ide

vhost內核模塊主要是把virtiO後端驅動的數據平面遷移到了內核中,而控制平面還在qemu中,因此就須要一些列的註冊把相關信息記錄在內核中,如虛擬機內存佈局,設備關聯的eventfd等。雖然KVM中有虛擬機的內存佈局,可是因爲vhost並不是在KVM中,而是單獨的一個內核模塊,因此須要qemu單獨處理。且目前vhost只支持網絡部分,塊設備等其餘部分尚不支持。內核中兩個文件比較重要:vhost.c和vhost-net.c。其中前者實現的是脫離具體功能的vhost核心實現,後者實現網絡方面的功能。內核模塊加載主要是初始化vhost-net,起始於vhost_net_init(vhost/net.c)函數

static const struct file_operations vhost_net_fops = {
    .owner          = THIS_MODULE,
    .release        = vhost_net_release,
    .unlocked_ioctl = vhost_net_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl   = vhost_net_compat_ioctl,
#endif
    .open           = vhost_net_open,
    .llseek        = noop_llseek,
};

函數表中vhost_net_open和vhost_net_ioctl兩個函數須要注意,簡單來說,前者初始化,後者控制,固然是qemu經過ioctl進行控制。那麼初始化主要是初始化啥呢?oop

主要有vhost_net(抽象表明vhost net部分)、vhost_dev(抽象的vhost設備),vhost_virtqueue。基本初始化的流程咱們就不介紹,感興趣能夠參考代碼,一個VM即一個qemu進程只有一個vhost-net和一個vhost-dev,而一個vhost-dev能夠關聯多個vhost_virtqueue。通常而言vhost_virtqueue做爲一個結構嵌入到具體實現的驅動中,就網絡而言vhost_virtqueue嵌入到了vhost_net_virtqueue。初始化最重要的任務就是初始化vhost_poll。在vhost_net_open的尾部,有以下兩行代碼佈局

vhost_poll_init(n->poll + VHOST_NET_VQ_TX, handle_tx_net, POLLOUT, dev);
vhost_poll_init(n->poll + VHOST_NET_VQ_RX, handle_rx_net, POLLIN, dev);

在分析函數代碼以前,先看下vhost_poll結構性能

struct vhost_poll {
    poll_table                table;
    wait_queue_head_t        *wqh; wait_queue_t wait; struct vhost_work work; unsigned long mask; struct vhost_dev *dev; };

結合上篇poll機制的文章,這些字段就不難理解,table是包含一個函數指針,在驅動的poll函數中被調用,主要用於把當前進程加入到等待隊列。wqh是一個等待隊列頭。wait是一個等待實體,其包含一個函數做爲喚醒函數,vhost_work是poll機制處理的核心任務,參考上面就是處理網絡數據包,其中有函數指針指向用戶設置的處理函數,這裏就是handle_tx_net和handle_rx_net,mask指定什麼狀況下進行處理,主要是POLL_IN和POLL_OUT,dev就指向依附的vhost-dev設備。結合這些介紹分析vhost_poll_init就無壓力了。

看下vhost_poll_init函數的代碼

void vhost_poll_init(struct vhost_poll *poll, vhost_work_fn_t fn,
             unsigned long mask, struct vhost_dev *dev)
{
    init_waitqueue_func_entry(&poll->wait, vhost_poll_wakeup);
    init_poll_funcptr(&poll->table, vhost_poll_func);
    poll->mask = mask;
    poll->dev = dev;
    poll->wqh = NULL;
    /*設置處理函數*/
    vhost_work_init(&poll->work, fn);
}

代碼來看很簡單,意義須要解釋下,每一個vhost_net_virtqueue都有本身的vhost_poll,該poll是監控數據的核心機制,而現階段僅僅是初始化。vhost_poll_wakeup是自定義的等待隊列喚醒函數,在對某個描述符poll的時候會把vhost_poll加入到對應描述符的等待隊列中,而該函數就是描述符有信號時的喚醒函數,喚醒函數中會驗證當前信號是否知足vhost_poll對應的請求掩碼,若是知足調用vhost_poll_queue->vhost_work_queue,該函數以下

void vhost_work_queue(struct vhost_dev *dev, struct vhost_work *work)
{
    unsigned long flags;

    spin_lock_irqsave(&dev->work_lock, flags);
    if (list_empty(&work->node)) {
        /*把vhost_work加入到設備的工做鏈表,該鏈表會在後臺線程中遍歷處理*/
        list_add_tail(&work->node, &dev->work_list);
        work->queue_seq++;
        /*喚醒工做線程*/    
        wake_up_process(dev->worker);
    }
    spin_unlock_irqrestore(&dev->work_lock, flags);
}

該函數會把vhost_work加入到設備的工做隊列,而後喚醒vhost後臺線程vhost_worker,vhost_worker會遍歷設備的工做隊列,調用work->fn即以前咱們註冊的處理函數handle_tx_net和handle_rx_net,這樣數據包就獲得了處理。

 vhost_net_ioctl控制信息

 vhost控制接口經過一系列的API指定相應的操做,下面列舉一部分

 VHOST_GET_FEATURES

VHOST_SET_FEATURES

這兩個用於獲取設置vhost一些特性

 VHOST_SET_OWNER  //設置vhost後臺線程,主要是建立一個線程綁定到vhost_dev,而線程的處理函數就是vhost_worker

 VHOST_RESET_OWNER  //重置OWNER

 VHOST_SET_MEM_TABLE   //設置guest內存佈局信息

 VHOST_NET_SET_BACKEND    //

 VHOST_SET_VRING_KICK  //設置guest notify  guest->host

 VHOST_SET_VRING_CALL  //設置host notify    host->guest

二、qemu部分

前面介紹的都是內核的任務,而內核是爲用戶提供服務的,除了vhost內核模塊加載時候主動執行一些初始化函數,後續的都是由qemu中發起請求,內核纔去響應。這也正是qemu維持控制平面的表現之一。qemu中相關代碼的介紹不介紹太多,只給出相關主線,感興趣能夠自行參考。這裏咱們主要經過qemu討論下host和guest的通知機制,即irqfd和IOeventfd的初始化。先介紹下irqfd和IOeventfd的任務。

irqfd是KVM爲host通知guest提供的中斷注入機制,vhost使用此機制通知客戶機某些任務已經完成,須要客戶機着手處理。而IOevnetfd是guest通知host的方式,qemu會註冊一段IO地址區間,PIO或者MMIO,這段地址區間的讀寫操做都會發生VM-exit,繼而在KVM中處理。詳細內容下面介紹

irqfd的初始化流程以下:

virtio_net_class_init

  virtio_net_device_init   virtio-net.c

    virtio_init   virtio.c

      virtio_vmstate_change

        virtio_set_status

          virtio_net_set_status

            virtio_net_vhost_status

              vhost_net_start

                virtio_pci_set_guest_notifiers   //爲guest_notify設置eventfd

                  kvm_virtio_pci_vector_use

                    kvm_virtio_pci_irqfd_use

                      kvm_irqchip_add_irqfd_notifier

                        kvm_irqchip_assign_irqfd

                          kvm_vm_ioctl(s, KVM_IRQFD, &irqfd);  //向kvm發起ioctl請求

IOeventfd工做流程以下:

virtio_ioport_write
  virtio_pci_start_ioeventfd
    virtio_pci_set_host_notifier_internal  //
      memory_region_add_eventfd
        memory_region_transaction_commit
          address_space_update_topology
            address_space_update_ioeventfds
              address_space_add_del_ioeventfds
                eventfd_add=kvm_mem_ioeventfd_add kvm_all.c
                  kvm_set_ioeventfd_mmio
                    kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &iofd);

三、KVM部分

 KVM部分實現對上面ioctl的響應,在kvm_main.c的kvm_vm_ioctl裏面,先看KVM_IRQFD的處理

 kvm_irqfd->kvm_irqfd_assign,kvm_irqfd_assign函數比較長,咱們主要介紹下核心功能

函數在內核生成一個_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;
    poll_table pt;
    struct work_struct shutdown;
};

kvm是關聯的虛擬機,wait是一個等待隊列對象,容許irqfd等待某個信號,irq_entry是中斷路由表,屬於中斷虛擬化部分,本節不做介紹。gsi是全局的中斷號,很重要。inject是一個工做對象,resampler是確認中斷處理的,不作考慮。eventfd是其關聯的evnetfd,這裏就是guestnotifier.在kvm_irqfd_assign函數中,給上面inject和shutdown都關聯了函數

INIT_WORK(&irqfd->inject, irqfd_inject);
INIT_WORK(&irqfd->shutdown, irqfd_shutdown);

 這些函數實現了irqfd的簡單功能,前者實現了中斷的注入,後者禁用irqfd。irqfd初始化好後,對於irqfd關聯用戶空間傳遞的eventfd,以後忽略中間的resampler之類的處理,初始化了irqfd等待隊列的喚醒函數irqfd_wakeup和核心poll函數irqfd_ptable_queue_proc,接着就調用irqfd_update更新中斷路由項目,中斷虛擬化的代碼單獨開一篇文章講解,下面就該調用具體的poll函數了,這裏是file->f_op->poll(file, &irqfd->pt);,實際對應的就是eventfd_poll函數,裏面會調用poll_table->_qproc,即irqfd_ptable_queue_proc把irqfd加入到描述符的等待隊列中,能夠看到這裏吧前面關聯的eventfd加入到了poll列表,當該eventfd有狀態時,喚醒函數irqfd_wakeup就獲得調用,其中經過工做隊列調度irqfd->inject,這樣irqfd_inject獲得執行,中斷就被注入,具體能夠參考vhost_add_used_and_signal_n函數,在從guest接收數據完畢就會調用該函數通知guest。

 IOEVENTFD

內核裏面起始於kvm_ioeventfd->kvm_assign_ioeventfd,這裏相對於上面就比較簡單了,主要是註冊一個IO設備,綁定一段IO地址區間,給設備分配操做函數表,其實就兩個函數

static const struct kvm_io_device_ops ioeventfd_ops = {
    .write      = ioeventfd_write,
    .destructor = ioeventfd_destructor,
};

 

 而當guest內部完成某個操做,如填充好了skbuffer後,就須要通知host,此時在guest內部最終就歸結於對設備的寫操做,寫操做會形成VM-exit繼而陷入到VMM中進行處理,PIO直接走的IO陷入,而MMIO須要走EPT violation的處理流程,最終就調用到設備的寫函數,這裏就是ioeventfd_write,看下該函數的實現

static int
ioeventfd_write(struct kvm_io_device *this, gpa_t addr, int len,
        const void *val)
{
    struct _ioeventfd *p = to_ioeventfd(this);

    if (!ioeventfd_in_range(p, addr, len, val))
        return -EOPNOTSUPP;

    eventfd_signal(p->eventfd, 1);
    return 0;
}

 

 實現很簡單,就是判斷地址是否在該段Io地址區間內,若是在就調用eventfd_signal,給該段IOeventfd綁定的eventfd一個信號,這樣在該eventfd上等待的對象就會獲得處理。

以馬內利!

參考資料:

linux3.10.1源碼

KVM源碼

qemu源碼

相關文章
相關標籤/搜索