virtIO之VHOST工作原理簡析


2017-07-19


一、前言 

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

二、 整體框架

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

1、vHOST 內核模塊

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進行控制。那么初始化主要是初始化啥呢?

主要有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

2、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);

3、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源碼


免責聲明!

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



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