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