加入星計劃,您可以享受以下權益:

  • 創(chuàng)作內容快速變現
  • 行業(yè)影響力擴散
  • 作品版權保護
  • 300W+ 專業(yè)用戶
  • 1.5W+ 優(yōu)質創(chuàng)作者
  • 5000+ 長期合作伙伴
立即加入
  • 正文
    • 01、virtio性能優(yōu)化技術的整體梳理
    • 02、vhost-net
    • 03、總結
  • 相關推薦
申請入駐 產業(yè)圖譜

從性能優(yōu)化的角度看virtio技術的演進和發(fā)展 (12)

2021/05/17
1016
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點資訊討論

01、virtio性能優(yōu)化技術的整體梳理

在前一次的virtio基礎篇中,我們介紹了virtio的基本原理,QEMU/KVM以及virtio的關系。我們還介紹了virtio設備的發(fā)現,初始化以及virtio-net從虛擬機中發(fā)送數據包的過程。

這一次的virtio進階篇會從性能優(yōu)化的角度,進一步介紹virtio技術演進和發(fā)展的過程。包括:

1). vhost-net技術(virtio-net后端的內核態(tài)實現);

2). vhost-user (virtio-net后端在用戶態(tài)的實現)以及與之緊密聯系的DPDK加速技術;

3). vDPA (固化在網卡中virtio-net后端的硬件實現)。

就像我們之前介紹的那樣,virtio的設計是分為前端和后端的。

在實現層面上,以virtio網絡為例子,在最初的virtio-net的實現中,virtio的前端驅動程序在虛擬機的內核空間運行,而virtio的后端驅動程序實現在QEMU中運行在用戶空間,前后端的通信機制是通過KVM(內核模塊)實現的。

以虛擬機向外部發(fā)送數據為例,虛擬機通過寫寄存器的方式通知外部的KVM,再進而通知virtio后端(實現在QEMU中)。在此基礎上,又逐步發(fā)展出了vhost-net,vhost-user和vDPA三種加速技術。

雖然三種技術的目的都是提升處理網絡數據包的能力,但是各自所采用的方法卻有很大區(qū)別:

1).vhost-net:virtio-net后端以內核模塊的方式實現,做為內核線程運行。和virtio-net的實現相比,減少了一次KVM到QEMU通信時的內核態(tài)/用戶態(tài)的切換,讓虛擬機的數據在內核態(tài)就把報文發(fā)送出去,進而提升性能;

2).vhost-user:為了進一步提升虛擬機網絡的性能,DPDK完全不再沿用Linux內核的網絡數據處理流程,基于Linux UIO技術在用戶態(tài)直接和網卡交互處理網絡數據,并且使用了內存大頁,綁定CPU核,NUMA親和性等方法進一步提升數據處理的性能。配合DPDK技術,virtio-net后端也在用戶態(tài)做了實現,這就是vhost-user;

3).vDPA:DPDK畢竟還是軟件加速方案,并且因為綁定CPU核導致獨占相應的CPU資源。雖然提高了網絡的性能,卻是通過犧牲服務器的計算資源做為代價的。于是,在這樣的背景下提出了vDPA (vhost Data Path Acceleration)技術。vDPA技術可以理解為virtio-net后端在網卡中的硬件實現,目前已經被一些網卡廠商支持。

在梳理了這三種技術和virtio-net之間的關系之后,我們接下來結合Linux內核,QEMU以及DPDK的代碼深入分析它們的具體實現。

02、vhost-net

接下來我們先結合QEMU和Linux內核的代碼回顧一下virtio-net的具體實現,之后再和vhost-net做一個對比,這樣才能更好的說明vhost-net的優(yōu)化思路。

●QEMU的vCPU線程以及IO處理

QEMU是一個多線程的用戶態(tài)程序。其中啟動虛擬機的vCPU線程的實現,可以參考代碼qemu-1.5.3/cpus.c中第1073行的qemu_init_vcpu函數。

可以看出,在確認KVM是被使能的情況下,會進一步調用qemu_kvm_start_vcpu函數,再調用qemu_thread_create函數創(chuàng)建執(zhí)行虛擬機的子線程(也可以叫做vCPU線程)。

由于需要支持多種硬件平臺,所以針對不同的體系結構的平臺,在qemu_thread_create函數內部做了封裝,實現底層平臺的上層封裝。

比如POSIX平臺的實現代碼在qemu-1.5.3/util/qemu-thread-posix.c,Windows平臺的實現在qemu-1.5.3/util/qemu-thread-win32.c。


voidqemu_init_vcpu(void*_env){CPUArchState*env=_env;    CPUState *cpu = ENV_GET_CPU(env);
cpu->nr_cores=smp_cores;cpu->nr_threads=smp_threads;cpu->stopped=true;if(kvm_enabled()){qemu_kvm_start_vcpu(env);}elseif(tcg_enabled()){qemu_tcg_init_vcpu(cpu);}else{qemu_dummy_start_vcpu(env);}}
static void qemu_kvm_start_vcpu(CPUArchState *env){    CPUState *cpu = ENV_GET_CPU(env);
    cpu->thread = g_malloc0(sizeof(QemuThread));    cpu->halt_cond = g_malloc0(sizeof(QemuCond));    qemu_cond_init(cpu->halt_cond);    qemu_thread_create(cpu->thread, qemu_kvm_cpu_thread_fn, env,                       QEMU_THREAD_JOINABLE);    while (!cpu->created) {        qemu_cond_wait(&qemu_cpu_cond, &qemu_global_mutex);    }}

關于QEMU中vCPU的執(zhí)行(也就是運行虛擬機的線程)和外部IO的處理,可以參考下面的代碼qemu-1.5.3/cpus.c第739行的qemu_kvm_cpu_thread_fn函數。其中kvm_cpu_exec是運行虛擬機,qemu_kvm_wait_io_event是處理外部的IO操作,兩者都在一個大的死循環(huán)中。

結合從虛擬機內部發(fā)送數據的例子來說,virtio-net前端(虛擬機中的網卡驅動程序)寫寄存器進行IO操作的時候,會觸發(fā)VM_EXIT,之后KVM檢查退出的理由,并且進一步處理IO在執(zhí)行完IO操作之后,會再繼續(xù)執(zhí)行vCPU線程,就這樣一直循環(huán)下去。

static void *qemu_kvm_cpu_thread_fn(void *arg){    CPUArchState *env = arg;    CPUState *cpu = ENV_GET_CPU(env);    int r;
    qemu_mutex_lock(&qemu_global_mutex);    qemu_thread_get_self(cpu->thread);    cpu->thread_id = qemu_get_thread_id();    cpu_single_env = env;
    r = kvm_init_vcpu(cpu);    if (r < 0) {        fprintf(stderr, "kvm_init_vcpu failed: %sn", strerror(-r));        exit(1);    }
    qemu_kvm_init_cpu_signals(env);
    /* signal CPU creation */    cpu->created = true;    qemu_cond_signal(&qemu_cpu_cond);
    while (1) {        if (cpu_can_run(cpu)) {            r = kvm_cpu_exec(env);            if (r == EXCP_DEBUG) {                cpu_handle_guest_debug(env);            }        }        qemu_kvm_wait_io_event(env);    }    return NULL;}

●QEMU/KVM的通信機制

QEMU/KVM的通信機制是基于eventfd實現的。

eventfd是內核實現的線程通信機制,通過系統調用可以創(chuàng)建eventfd,它可以用于線程間或者進程間的通信,比如用于實現通知,等待機制。

內核也可以通過eventfd和用戶空間進程進行通信。eventfd的具體實現可以參考內核代碼linux-3.10.0-957.1.3.el7/fs/eventfd.c文件的第25行,主要的數據結構是eventfd_ctx。

通過代碼查看數據結構eventfd_ctx,可以看出eventfd的核心實現是在內核空間的一個64位的計數器。不同線程通過讀寫該eventfd通知或等待對方,內核也可以通過寫這個eventfd通知用戶程序。
在eventfd被創(chuàng)建之后,系統提供了對eventfd的讀寫操作的接口。寫eventfd的時候,會增加計數器的數值,并且喚醒wait_queue隊列;讀eventfd的時候,就是將計數器的數值置為0。


struct eventfd_ctx {    struct kref kref;    wait_queue_head_t wqh;    /*     * Every time that a write(2) is performed on an eventfd, the     * value of the __u64 being written is added to "count" and a     * wakeup is performed on "wqh". A read(2) will return the "count"     * value to userspace, and will reset "count" to zero. The kernel     * side eventfd_signal() also, adds to the "count" counter and     * issue a wakeup.     */    __u64 count;    unsigned int flags;};

QEMU/KVM通信機制的實現是基于ioeventfd實現,對eventfd又做了一次封裝。具體的實現代碼在linux-3.10.0-957.1.3.el7/virt/kvm/eventfd.c第642行。

/* * -------------------------------------------------------------------- * ioeventfd: translate a PIO/MMIO memory write to an eventfd signal. * * userspace can register a PIO/MMIO address with an eventfd for receiving * notification when the memory has been touched. * -------------------------------------------------------------------- */
struct _ioeventfd {    struct list_head     list;    u64                  addr;    int                  length;    struct eventfd_ctx  *eventfd;    u64                  datamatch;    struct kvm_io_device dev;    u8                   bus_idx;    bool                 wildcard;};

在QEMU/KVM的系統虛擬化環(huán)境中,針對每個虛擬機,vhost-net會生成一個名為"vhost-[pid]"的內核線程,這里的"pid"即QEMU進程(也稱作"hypervisor process")的PID。該內核線程替代了QEMU的等待輪詢工作。與virtio-net的實現不同的是,eventfd_signal 喚醒的是內核vhost-net創(chuàng)建的內核線程。這個內核線程負責從虛擬隊列中提取報文數據,然后發(fā)送給 tap接口。

與virtio-net的實現相比,vhost-net的內核線程在內核態(tài)處理網絡數據,在發(fā)送到tap接口的時候少了一次數據拷貝。具體的實現代碼在:linux-3.10.0-957.1.3.el7/drivers/vhost/vhost.c的vhost_dev_set_owner函數,在第486行調用kthread_create函數創(chuàng)建內核線程調用vhost_worker函數進行輪詢。

/* Caller should have device mutex */long vhost_dev_set_owner(struct vhost_dev *dev){    struct task_struct *worker;    int err;     /* Is there an owner already? */    if (vhost_dev_has_owner(dev)) {        err = -EBUSY;        goto err_mm;    }     /* No owner, become one */    dev->mm = get_task_mm(current);    worker = kthread_create(vhost_worker, dev, "vhost-%d", current->pid);    if (IS_ERR(worker)) {        err = PTR_ERR(worker);        goto err_worker;    }     dev->worker = worker;    wake_up_process(worker);    /* avoid contributing to loadavg */     err = vhost_attach_cgroups(dev);    if (err)        goto err_cgroup;     err = vhost_dev_alloc_iovecs(dev);    if (err)        goto err_cgroup;     return 0;err_cgroup:    kthread_stop(worker);    dev->worker = NULL;err_worker:    if (dev->mm)        mmput(dev->mm);    dev->mm = NULL;err_mm:    return err;}EXPORT_SYMBOL_GPL(vhost_dev_set_owner);

圖1 vhost-net和virtio-net的對比圖

 

03、總結

總結一下:虛擬機virtio前端驅動程序發(fā)送通知的函數最終是執(zhí)行I/O寫指令。在QEMU/KVM環(huán)境中,虛擬機執(zhí)行I/O指令,會觸發(fā)VMExit。在KVM的VMExit代碼中會判斷退出的原因,I/O操作對應的處理函數是handle_io()。接下來KVM是通過eventfd的機制通知virtio-net后端程序來處理。接下來的處理流程,使用virtio-net和vhost-net就不一樣了。

Ø使用virtio-net的情況:KVM會使用eventfd的通知機制,通知用戶態(tài)的QEMU程序模擬外部I/O事件,所以這里有一次從內核空間到用戶空間的切換。

Ø使用vhost-net的情況:KVM會使用eventfd的通知機制,通知vhost-net的內核線程,這樣就直接在vhost內核模塊(vhost-net.ko)內處理。所以,和virtio-net相比就減少了一次從內核態(tài)到用戶態(tài)的切換,從而提升網絡IO的性能。

由于vhost-user和vDPA都和DPDK緊密相關,所以我們將在virtio進階篇(2/2)中集中介紹。

相關推薦

登錄即可解鎖
  • 海量技術文章
  • 設計資源下載
  • 產業(yè)鏈客戶資源
  • 寫文章/發(fā)需求
立即登錄

專業(yè)的Linux技術社區(qū)和Linux操作系統學習平臺,內容涉及Linux內核,Linux內存管理,Linux進程管理,Linux文件系統和IO,Linux性能調優(yōu),Linux設備驅動以及Linux虛擬化和云計算等各方各面.