QEMU线程模型
最近在阅读李强编著的《QEMU/KVM源码解析与应用》这本书来学习Linux内核虚拟化相关知识,通过读书笔记的方式来提炼和归纳书中重要的知识点。本文主要内容是关于QEMU线程模型的介绍。
关注微信公众号:Linux内核拾遗
1 QEMU线程模型简介
QEMU-KVM架构中,一个QEMU进程代表一个虚拟机。QEMU会有若干个线程,其中对于每个CPU会创建一个线程,还有其他的线程,如VNC线程、I/O线程、热迁移线程等。QEMU线程模型如下图所示:
- I/O线程:传统上,I/O线程指的是QEMU主事件循环所在的线程它会不断监听各种I/O事件;现在的I/O线程通常是指块设备层面的单独用来处理I/O事件的线程。
- VCPU线程:每一个CPU都会有一个线程,通常叫作VCPU线程,其主要的执行函数是kvm_cpu_exec。
- QEMU为了完成其他功能还会有一些辅助线程,如热迁移时候的migration线程、支持远程连接的VNC和SPICE线程等。
2 QEMU大锁
这里补充一下原书中没有怎么详细介绍的QEMU大锁。
QEMU在模拟CPU的时候会使用到一些锁来保证多线程的正确性。其中,QEMU的大锁(big lock)是指QEMU中的一个全局锁,用于保护整个虚拟机的状态,防止多线程竞争。
QEMU线程模型通常使用QEMU大锁进行同步,获取锁的函数为qemu_mutex_lock_iothread,解锁函数为qemu_mutex_unlock_iothread。实际上随着演变,现在这两个函数已经变成宏了。
很多场合都需要BQL,比如:
- os_host_main_loop_wait在有fd返回事件时,在进行事件处理之前需要调用qemu_mutex_lock_iothread获取BQL。
- VCPU线程在退出到QEMU进行一些处理的时候也会获取BQL。
在QEMU中,由于大部分代码都被保护在大锁的范围内,这就导致了QEMU的多线程性能受到了一定的限制。因为只有一个线程可以获得大锁,其他线程必须等待该线程释放大锁才能继续执行,这样就会导致多线程并发执行时的效率降低。
为了提高QEMU的多线程性能,开发者们已经在不断努力地将代码分解为更小的锁颗粒度,以减少大锁的使用。同时,他们也在探索一些其他的优化技术,比如使用更高效的锁实现,以及使用无锁算法等。
3 QEMU线程介绍
3.1 VCPU线程
3.1.1 CPU具现
介绍VCPU线程之前,这里先补充一下CPU具现的概念。
在QEMU中,CPU具现(CPU realization)是指将虚拟CPU的架构实现为一个软件模拟器的过程。这个软件模拟器能够模拟真实硬件CPU的指令集、寄存器、缓存等特性,并且能够在不同的CPU架构之间进行转换,使得虚拟机能够运行在不同的CPU架构上。
QEMU中的CPU具现是一个非常重要的概念,因为它决定了QEMU的虚拟化能力和性能。QEMU支持多种CPU具现,包括x86、ARM、MIPS、PowerPC等。每种CPU具现都有其自己的实现方式和性能特征。
QEMU中的CPU具现还包括一些针对特定CPU架构的优化。例如,对于x86架构,QEMU支持KVM(Kernel-based Virtual Machine)加速,可以利用硬件虚拟化技术提高虚拟机的性能。对于ARM架构,QEMU支持TCG(Tiny Code Generator)加速,可以将虚拟指令动态翻译成宿主机的本地指令,提高虚拟机的执行效率。
总之,QEMU中的CPU具现是QEMU能够模拟各种CPU架构的基础,也是QEMU虚拟化能力和性能的关键所在。
3.1.2 VCPU线程创建过程
QEMU虚拟机的VCPU对应于宿主机上的一个线程,通常叫作VCPU线程。
在x86_cpu_realizefn函数中进行CPU具现的时候会调用qemu_init_vcpu函数来创建VCPU线程。qemu_init_vcpu根据加速器的不同,会调用不同的函数来进行VCPU的创建,对于KVM加速器来说,这个函数是qemu_kvm_start_vcpu:
// cpus.c
static void qemu_kvm_start_vcpu(CPUState* cpu) {
char thread_name[VCPU_THREAD_NAME_SIZE];
...
qemu_thread_create(cpu->thread, thread_name, qemu_kvm_cpu_thread_fn,
cpu, QEMU_THREAD_JOINABLE);
}
qemu_thread_create调用了pthread_create来创建VCPU线程。VCPU线程用来执行虚拟机的代码,其线程函数是qemu_kvm_cpu_thread_fn。
3.2 VNC线程
在main函数中,会调用vnc_init_func对VNC模块进行初始化,经过vnc_display_init->vnc_start_worker_thread的调用最终创建VNC线程,VNC线程用来与VNC客户端进行交互。
// ui/vnc-jobs.c
void vnc_start_worker_thread(void) {
VncJobQueue* q;
...
q = vnc_queue_init();
qemu_thread_create(&q->thread, "vnc_worker", vnc_worker_thread_fn, q
QEMU_THREAD_DETACHED);
queue = q; /* Set global queue */
}
3.3 I/O线程
设备模拟过程中可能会占用QEMU的大锁,所以如果是用磁盘类设备进行读写,会导致占用该锁较长时间。为了提高性能,会将这类操作单独放到一个线程中去。
QEMU抽象出了一个新的类型TYPE_IOTHREAD,可以用来进行I/O线程的创建。比如virtio块设备在其对象实例化函数中添加了一个link属性,其对应的连接对象为一个TYPE_IOTHREAD。
// hw/block/virtio-blk.c
static void virtio_blk_instance_init(Object* obj) {
VirtIOBlock* s = VIRTIO_BLK(obj);
object_property_add_link(obj, "iothread", TYPE_IOTHREAD,
(OBject**)&s->conf.iothread,
qdev_prop_allow_set_link_before_realize,
OBJ_PROP_LINK_UNREF_ON_RELEASE, NULL);
device_add_bootindex_property(obj, &s->conf.conf.bootindex,
"bootindex", "/disk@0,0",
DEVICE(obj), NULL);
}
当进行数据面的读写时,就可以使用这个iothread进行。
4 总结
如同Linux内核中的大锁,BQL会对QEMU虚拟机的性能造成很大影响。
早期的QEMU代码在握有BQL时做的事情很多,QEMU多线程的主要动力是减少QEMU主线程的运行时间,QEMU在进行一些设备模拟的时候,VCPU线程会退出到QEMU,抢占QEMU大锁,如果这个时候有其他线程占据大锁,再做长时间的工作就会导致VCPU被长时间挂起,所以将一些没有必要占据QEMU大锁的任务放到单独线程进行处理就能够增加VCPU的运行时间,这也是QEMU社区在多线程方向的努力方向,即尽量将任务从QEMU大锁中拿出来。
参考文献
- QEMU/KVM源码解析与应用 - 李强
关注微信公众号:Linux内核拾遗