QEMU/KVM下VCPU同步引发的死锁
问题不是出在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
链接为:https://oenhan.com/qemu-kvm-vcpu-deadlock