首页 > Virtualization > QEMU/KVM下VCPU同步引发的死锁

QEMU/KVM下VCPU同步引发的死锁

Virtualization 2016-11-20

问题不是出在upstream的版本上,而是内部开发的一个产品。在KVM这个系统里面,很多时候牵一发动全身,某些改动貌似是正确的,其实则不然,内核的其他模块也是类似的系统化,Jike说的“正是内核的开发门槛很高才保证了内核当前的质量”也是有道理的。

进入正题,产品要求VCPU进入guest之前需要等待响应,当存在响应的时候才能进入guest,不存在响应就不能进入guest,怎么处理这个需求呢?一开始使用了等待队列,即wait_queue_head_t,配合wait_event_interruptible(wq, condition),只需要一直等即可,除非condition满足要求,此处的condition就是一个变相的响应,这个设计满足需求没有问题。

然而还有另外一个需求:VirtIO要截获对应虚拟PCI下所有的DMA操作,同时要获取当前VCPU的精确的EIP,那么只能在VCPU不运行的时候截获DMA操作,只能是每次DMA发生时将VCPU kick到QEMU进行处理。使用的函数是QEMU下的pause_all_vcpus。

void pause_all_vcpus(void)
{qemu_clock_enable(QEMU_CLOCK_VIRTUAL, false);
 CPU_FOREACH(cpu) {
 cpu->stop = true;
 qemu_cpu_kick(cpu);
 }

while (!all_vcpus_paused()) {
 qemu_cond_wait(&qemu_pause_cond, &qemu_global_mutex);
 CPU_FOREACH(cpu) {
 qemu_cpu_kick(cpu);

此时问题就出现了,如上面所示,每次pause_all_vcpus都是qemu_cpu_kick,而qemu_cpu_kick_thread是pthread_kill(cpu->thread->thread, SIG_IPI),当VCPU在进入guest之前等待响应时,pause_all_vcpus会发送一个中断给vcpu线程,而__wait_event_interruptible是响应中断的,存在中断的情况下,就直接返回了。

#define __wait_event_interruptible(wq, condition, ret) \
do { \
 DEFINE_WAIT(__wait); \
 \
 for (;;) { \
 prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE); \
 if (condition) \
 break; \
 if (!signal_pending(current)) { \
 schedule(); \
 continue; \
 } \
 ret = -ERESTARTSYS; \
 break; \
 } \
 finish_wait(&wq, &__wait); \
} while (0)

这样VPU进入guest前的响应就失效了。

那要怎么改动呢?提笔改成了wait_event(wq, condition) ,不响应任何外部中断。

读写信号量与实时进程阻塞挂死问题一样,小菜吃完,进入大餐了,改完之后发现还是有挂死的问题,这次用肉眼看代码是不行了,挂在了一次eventfd应用的地方,场景就是KVM通知QEMU完成一件事务,完成后通知给KVM完成,KVM继续工作,否则KVM不能进入到guest中运行,eventfd应用到此处也是非常合适的,问题则出在后面,KVM等待QEMU完成事务的通知,代码就是while(condition) schedule(),本质上和wait_event差不多,也不响应任何外部中断。

问题来了,当KVM使用eventfd_signal通知QEMU后就一直进入schedule状态,只有当condition满足条件时才出来,QEMU使用kvm_vm_ioctl通知KVM,而调用QEMU kvm_vm_ioctl的函数X则是qemu_set_fd_handler挂到IO thread下的,详细的参考QEMU下的eventfd机制及源代码分析,函数X最终通过epoll机制在aio_dispatch下的node->io_read(node->opaque)或node->io_write(node->opaque)调用,然而在aio_dispatch下先调用了aio_bh_poll,继而调用aio_bh_call(bh):

void aio_bh_call(QEMUBH *bh)
{
 bh->cb(bh->opaque);
}

通过aio_bh_new函数可以往上看,有无数的DMA操作调用这个函数,以virtio_blk_dma_restart_cb为例,它在virtio_blk_device_realize下注册给了VMChangeStateEntry *change,就是指当虚拟机状态发生变化后,会调用virtio_blk_dma_restart_cb,将当前还没来得及完成的DMA操作加入到AioContext中,然后使用qemu_bh_schedule调度完成,使用的就是virtio_blk_dma_restart_bh函数。

static void virtio_blk_dma_restart_cb(void *opaque, int running, RunState state)
{
 VirtIOBlock *s = opaque;

if (!s->bh) {
 s->bh = aio_bh_new(blk_get_aio_context(s->conf.conf.blk),virtio_blk_dma_restart_bh, s);
 qemu_bh_schedule(s->bh);
 }
}

这样看来aio_bh_call下执行的有可能是virtio_blk_dma_restart_bh类型的函数,这个函数就是使用virtio_blk_submit_multireq重新提交DMA操作,前面讲过,通过pause_all_vcpus使VCPU发生vm-exit后,然后截获DMA操作,然后KVM使用while(condition) schedule()等待QEMU的eventfd的通知,实际上此时QEMU的kick操作是得不到KVM响应的,而QEMU则要一直等待KVM退回到用户态。

qemu_cond_wait(&qemu_pause_cond, &qemu_global_mutex)

这样就形成了KVM vcpu thread等待QEMU,而QEMU IO thread等待KVM VCPU thread退回到QEMU的死锁状况。

那么问题怎么解决呢?不响应中断则会出现死锁,响应中断则会QEMU没有完成事务,KVM就会重新进入到guest。

回头看一下,等待的目标是什么,KVM不会重新进入到guest!

那么就没必要在此处死循环等待,只需要完成类似如下的代码即可:if (condtion) kvm_guest_enter();


QEMU/KVM下VCPU同步引发的死锁来自于OenHan,链接为:http://oenhan.com/qemu-kvm-vcpu-deadlock
更多阅读