01、virtio性能優(yōu)化技術(shù)的整體梳理
在前一次的virtio基礎(chǔ)篇中,我們介紹了virtio的基本原理,QEMU/KVM以及virtio的關(guān)系。我們還介紹了virtio設(shè)備的發(fā)現(xiàn),初始化以及virtio-net從虛擬機(jī)中發(fā)送數(shù)據(jù)包的過程。
這一次的virtio進(jìn)階篇會(huì)從性能優(yōu)化的角度,進(jìn)一步介紹virtio技術(shù)演進(jìn)和發(fā)展的過程。包括:
1). vhost-net技術(shù)(virtio-net后端的內(nèi)核態(tài)實(shí)現(xiàn));
2). vhost-user (virtio-net后端在用戶態(tài)的實(shí)現(xiàn))以及與之緊密聯(lián)系的DPDK加速技術(shù);
3). vDPA (固化在網(wǎng)卡中virtio-net后端的硬件實(shí)現(xiàn))。
就像我們之前介紹的那樣,virtio的設(shè)計(jì)是分為前端和后端的。
在實(shí)現(xiàn)層面上,以virtio網(wǎng)絡(luò)為例子,在最初的virtio-net的實(shí)現(xiàn)中,virtio的前端驅(qū)動(dòng)程序在虛擬機(jī)的內(nèi)核空間運(yùn)行,而virtio的后端驅(qū)動(dòng)程序?qū)崿F(xiàn)在QEMU中運(yùn)行在用戶空間,前后端的通信機(jī)制是通過KVM(內(nèi)核模塊)實(shí)現(xiàn)的。
以虛擬機(jī)向外部發(fā)送數(shù)據(jù)為例,虛擬機(jī)通過寫寄存器的方式通知外部的KVM,再進(jìn)而通知virtio后端(實(shí)現(xiàn)在QEMU中)。在此基礎(chǔ)上,又逐步發(fā)展出了vhost-net,vhost-user和vDPA三種加速技術(shù)。
雖然三種技術(shù)的目的都是提升處理網(wǎng)絡(luò)數(shù)據(jù)包的能力,但是各自所采用的方法卻有很大區(qū)別:
1).vhost-net:virtio-net后端以內(nèi)核模塊的方式實(shí)現(xiàn),做為內(nèi)核線程運(yùn)行。和virtio-net的實(shí)現(xiàn)相比,減少了一次KVM到QEMU通信時(shí)的內(nèi)核態(tài)/用戶態(tài)的切換,讓虛擬機(jī)的數(shù)據(jù)在內(nèi)核態(tài)就把報(bào)文發(fā)送出去,進(jìn)而提升性能;
2).vhost-user:為了進(jìn)一步提升虛擬機(jī)網(wǎng)絡(luò)的性能,DPDK完全不再沿用Linux內(nèi)核的網(wǎng)絡(luò)數(shù)據(jù)處理流程,基于Linux UIO技術(shù)在用戶態(tài)直接和網(wǎng)卡交互處理網(wǎng)絡(luò)數(shù)據(jù),并且使用了內(nèi)存大頁,綁定CPU核,NUMA親和性等方法進(jìn)一步提升數(shù)據(jù)處理的性能。配合DPDK技術(shù),virtio-net后端也在用戶態(tài)做了實(shí)現(xiàn),這就是vhost-user;
3).vDPA:DPDK畢竟還是軟件加速方案,并且因?yàn)榻壎–PU核導(dǎo)致獨(dú)占相應(yīng)的CPU資源。雖然提高了網(wǎng)絡(luò)的性能,卻是通過犧牲服務(wù)器的計(jì)算資源做為代價(jià)的。于是,在這樣的背景下提出了vDPA (vhost Data Path Acceleration)技術(shù)。vDPA技術(shù)可以理解為virtio-net后端在網(wǎng)卡中的硬件實(shí)現(xiàn),目前已經(jīng)被一些網(wǎng)卡廠商支持。
在梳理了這三種技術(shù)和virtio-net之間的關(guān)系之后,我們接下來結(jié)合Linux內(nèi)核,QEMU以及DPDK的代碼深入分析它們的具體實(shí)現(xiàn)。
02、vhost-net
接下來我們先結(jié)合QEMU和Linux內(nèi)核的代碼回顧一下virtio-net的具體實(shí)現(xiàn),之后再和vhost-net做一個(gè)對(duì)比,這樣才能更好的說明vhost-net的優(yōu)化思路。
●QEMU的vCPU線程以及IO處理
QEMU是一個(gè)多線程的用戶態(tài)程序。其中啟動(dòng)虛擬機(jī)的vCPU線程的實(shí)現(xiàn),可以參考代碼qemu-1.5.3/cpus.c中第1073行的qemu_init_vcpu函數(shù)。
可以看出,在確認(rèn)KVM是被使能的情況下,會(huì)進(jìn)一步調(diào)用qemu_kvm_start_vcpu函數(shù),再調(diào)用qemu_thread_create函數(shù)創(chuàng)建執(zhí)行虛擬機(jī)的子線程(也可以叫做vCPU線程)。
由于需要支持多種硬件平臺(tái),所以針對(duì)不同的體系結(jié)構(gòu)的平臺(tái),在qemu_thread_create函數(shù)內(nèi)部做了封裝,實(shí)現(xiàn)底層平臺(tái)的上層封裝。
比如POSIX平臺(tái)的實(shí)現(xiàn)代碼在qemu-1.5.3/util/qemu-thread-posix.c,Windows平臺(tái)的實(shí)現(xiàn)在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);
}
}
關(guān)于QEMU中vCPU的執(zhí)行(也就是運(yùn)行虛擬機(jī)的線程)和外部IO的處理,可以參考下面的代碼qemu-1.5.3/cpus.c第739行的qemu_kvm_cpu_thread_fn函數(shù)。其中kvm_cpu_exec是運(yùn)行虛擬機(jī),qemu_kvm_wait_io_event是處理外部的IO操作,兩者都在一個(gè)大的死循環(huán)中。
結(jié)合從虛擬機(jī)內(nèi)部發(fā)送數(shù)據(jù)的例子來說,virtio-net前端(虛擬機(jī)中的網(wǎng)卡驅(qū)動(dòng)程序)寫寄存器進(jìn)行IO操作的時(shí)候,會(huì)觸發(fā)VM_EXIT,之后KVM檢查退出的理由,并且進(jìn)一步處理IO在執(zhí)行完IO操作之后,會(huì)再繼續(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的通信機(jī)制
QEMU/KVM的通信機(jī)制是基于eventfd實(shí)現(xiàn)的。
eventfd是內(nèi)核實(shí)現(xiàn)的線程通信機(jī)制,通過系統(tǒng)調(diào)用可以創(chuàng)建eventfd,它可以用于線程間或者進(jìn)程間的通信,比如用于實(shí)現(xiàn)通知,等待機(jī)制。
內(nèi)核也可以通過eventfd和用戶空間進(jìn)程進(jìn)行通信。eventfd的具體實(shí)現(xiàn)可以參考內(nèi)核代碼linux-3.10.0-957.1.3.el7/fs/eventfd.c文件的第25行,主要的數(shù)據(jù)結(jié)構(gòu)是eventfd_ctx。
通過代碼查看數(shù)據(jù)結(jié)構(gòu)eventfd_ctx,可以看出eventfd的核心實(shí)現(xiàn)是在內(nèi)核空間的一個(gè)64位的計(jì)數(shù)器。不同線程通過讀寫該eventfd通知或等待對(duì)方,內(nèi)核也可以通過寫這個(gè)eventfd通知用戶程序。
在eventfd被創(chuàng)建之后,系統(tǒng)提供了對(duì)eventfd的讀寫操作的接口。寫eventfd的時(shí)候,會(huì)增加計(jì)數(shù)器的數(shù)值,并且喚醒wait_queue隊(duì)列;讀eventfd的時(shí)候,就是將計(jì)數(shù)器的數(shù)值置為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通信機(jī)制的實(shí)現(xiàn)是基于ioeventfd實(shí)現(xiàn),對(duì)eventfd又做了一次封裝。具體的實(shí)現(xiàn)代碼在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的系統(tǒng)虛擬化環(huán)境中,針對(duì)每個(gè)虛擬機(jī),vhost-net會(huì)生成一個(gè)名為"vhost-[pid]"的內(nèi)核線程,這里的"pid"即QEMU進(jìn)程(也稱作"hypervisor process")的PID。該內(nèi)核線程替代了QEMU的等待輪詢工作。與virtio-net的實(shí)現(xiàn)不同的是,eventfd_signal 喚醒的是內(nèi)核vhost-net創(chuàng)建的內(nèi)核線程。這個(gè)內(nèi)核線程負(fù)責(zé)從虛擬隊(duì)列中提取報(bào)文數(shù)據(jù),然后發(fā)送給 tap接口。
與virtio-net的實(shí)現(xiàn)相比,vhost-net的內(nèi)核線程在內(nèi)核態(tài)處理網(wǎng)絡(luò)數(shù)據(jù),在發(fā)送到tap接口的時(shí)候少了一次數(shù)據(jù)拷貝。具體的實(shí)現(xiàn)代碼在:linux-3.10.0-957.1.3.el7/drivers/vhost/vhost.c的vhost_dev_set_owner函數(shù),在第486行調(diào)用kthread_create函數(shù)創(chuàng)建內(nèi)核線程調(diào)用vhost_worker函數(shù)進(jìn)行輪詢。
/* 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的對(duì)比圖
03、總結(jié)
總結(jié)一下:虛擬機(jī)virtio前端驅(qū)動(dòng)程序發(fā)送通知的函數(shù)最終是執(zhí)行I/O寫指令。在QEMU/KVM環(huán)境中,虛擬機(jī)執(zhí)行I/O指令,會(huì)觸發(fā)VMExit。在KVM的VMExit代碼中會(huì)判斷退出的原因,I/O操作對(duì)應(yīng)的處理函數(shù)是handle_io()。接下來KVM是通過eventfd的機(jī)制通知virtio-net后端程序來處理。接下來的處理流程,使用virtio-net和vhost-net就不一樣了。
Ø使用virtio-net的情況:KVM會(huì)使用eventfd的通知機(jī)制,通知用戶態(tài)的QEMU程序模擬外部I/O事件,所以這里有一次從內(nèi)核空間到用戶空間的切換。
Ø使用vhost-net的情況:KVM會(huì)使用eventfd的通知機(jī)制,通知vhost-net的內(nèi)核線程,這樣就直接在vhost內(nèi)核模塊(vhost-net.ko)內(nèi)處理。所以,和virtio-net相比就減少了一次從內(nèi)核態(tài)到用戶態(tài)的切換,從而提升網(wǎng)絡(luò)IO的性能。
由于vhost-user和vDPA都和DPDK緊密相關(guān),所以我們將在virtio進(jìn)階篇(2/2)中集中介紹。