Skip to the content.

QEMU 中的锁

之所以使用 lock ,是因为存在共享的资源。

Big QEMU Lock

使用 Big QEMU Lock (下面简称为 BQL) 是因为设备的模拟是串行的。 比如 pic 中断控制器在 QEMU 中描述在 hw/intc/i8259.c 中, pic 的状态保存在 PICCommonState 中,多个 vCPU thread 访问 pic 的时候, 那就需要靠 BQL 来实现互斥,只能逐个调用 pic 的模拟函数,也就是 pic_ioport_read / pic_ioport_write 。 如果一个 vCPU 在执行 pic_ioport_write,另一个 vCPU 在 pic_ioport_read 的时候,其获取的状态可能错误的中间状态。

回忆一下,QEMU 中的线程和事件循环 中 QEMU 的执行模型:

按照这种指导思想可以很容易确认下面的位置的 BQL 的使用:

BQL Advanced Topic

但是实际上,BQL 的使用位置要上面多一点,这些是高级话题,可以暂时跳过。

migration

实际上,和 migration 相关的还有 qemu_mutex_lock_ramlist,从原则上将当持有了 BQL 的时候,就屏蔽了所有的 lock 的。 但是在 ram_init_bitmaps 首先上锁 BQL,然后是 ramlist 的。 具体可以从 b2a8658ef5dc57ea 分析,有待进一步跟进。

main loop

main loop 中上锁位置非常的早,在 pc_init1 => qemu_init_subsystems 中几乎是 BQL 初始化之后就会获取。

创建的 vCPU 例如 mttcg_cpu_thread_fn 因为无法获取 BQL 而无法进一步执行,一切都需要等待 main loop 初始化好。

如果 cpu realize 失败,会调用 x86_cpu_unrealizefn => cpu_remove_sync 来清理资源包括释放 vCPU 的,为了让 vCPU 进一步执行,所以 cpu_remove_sync 中需要短暂的释放 BQL

QEMU 中的线程和事件循环中,我们分析了 main loop 如何实现事件监听。当 vCPU thread 需要模拟设备操作,比如 DMA 的时候,最后会调用 具体设备的 callback 函数,但是 vCPU thread 不会等待下去,而是将其中 callback 函数让 main loop 执行。而 main loop 就是靠事件监听来知道有 vCPU 提交任务给他了。当 main loop 执行完成之后, 只需要向 vCPU 发送一个中断,也即是最后调用到 tcg_handle_interrupt, 向 CPUState::interrupt_request 插入一个中断,而 tcg 执行的时候,每一个 tb 都会检查这个,如果插入了中断,就会退出,最后在 cpu_handle_interrupt 地方处理。

#0  apic_send_msi (msi=0x7fffffffd110) at ../hw/intc/apic.c:726
#1  0x0000555555c6ab4c in apic_mem_write (opaque=<optimized out>, addr=4100, val=48, size=<optimized out>) at ../hw/intc/apic.c:757
#2  0x0000555555cd2711 in memory_region_write_accessor (mr=mr@entry=0x55555698bc90, addr=4100, value=value@entry=0x7fffffffd298, size=size@entry=4, shift=<optimized out
>, mask=mask@entry=4294967295, attrs=...) at ../softmmu/memory.c:492
#3  0x0000555555cceb9e in access_with_adjusted_size (addr=addr@entry=4100, value=value@entry=0x7fffffffd298, size=size@entry=4, access_size_min=<optimized out>, access_
size_max=<optimized out>, access_fn=access_fn@entry=0x555555cd2680 <memory_region_write_accessor>, mr=0x55555698bc90, attrs=...) at ../softmmu/memory.c:554
#4  0x0000555555cd1c47 in memory_region_dispatch_write (mr=mr@entry=0x55555698bc90, addr=4100, data=<optimized out>, data@entry=48, op=op@entry=MO_32, attrs=attrs@entry
=...) at ../softmmu/memory.c:1504
#5  0x0000555555ca12ed in address_space_stl_internal (endian=DEVICE_LITTLE_ENDIAN, result=0x0, attrs=..., val=48, addr=<optimized out>, as=0x0) at /home/maritns3/core/k
vmqemu/include/exec/memory.h:2868
#6  address_space_stl_le (as=as@entry=0x555556606820 <address_space_memory>, addr=<optimized out>, val=48, attrs=attrs@entry=..., result=result@entry=0x0) at /home/mari
tns3/core/kvmqemu/memory_ldst.c.inc:357
#7  0x0000555555cee124 in stl_le_phys (val=<optimized out>, addr=<optimized out>, as=0x555556606820 <address_space_memory>) at /home/maritns3/core/kvmqemu/include/exec/
memory_ldst_phys.h.inc:121
#8  ioapic_service (s=s@entry=0x555556a42360) at ../hw/intc/ioapic.c:138
#9  0x0000555555cee3ff in ioapic_set_irq (opaque=0x555556a42360, vector=<optimized out>, level=1) at ../hw/intc/ioapic.c:186
#10 0x0000555555b92664 in gsi_handler (opaque=0x555556af6ff0, n=0, level=1) at ../hw/i386/x86.c:600
#11 0x0000555555b42a9e in qemu_irq_pulse (irq=0x555556ab3b50) at /home/maritns3/core/kvmqemu/include/hw/irq.h:22
#12 update_irq (timer=<optimized out>, set=<optimized out>) at ../hw/timer/hpet.c:219
#13 0x0000555555e6fe88 in timerlist_run_timers (timer_list=0x555556707120) at ../util/qemu-timer.c:573
#14 timerlist_run_timers (timer_list=0x555556707120) at ../util/qemu-timer.c:498
#15 0x0000555555e70097 in qemu_clock_run_all_timers () at ../util/qemu-timer.c:669
#16 0x0000555555e4ced9 in main_loop_wait (nonblocking=nonblocking@entry=0) at ../util/main-loop.c:542
#17 0x0000555555c58231 in qemu_main_loop () at ../softmmu/runstate.c:726
#18 0x0000555555940c92 in main (argc=<optimized out>, argv=<optimized out>, envp=<optimized out>) at ../softmmu/main.c:50

rcu

call_rcu_thread 中,需要持有 lock 才可以释放资源,这很奇怪。既然都是可以开始来执行 hook 函数了,说明这些资源已经是没有人使用的,那么为什么还需要使用 BQL 保护。 其原因在: https://lists.gnu.org/archive/html/qemu-devel/2015-02/msg03170.html

interrupt_request

因为一个 CPU 利用 ipi 机制给另一个 vCPU 发送中断,所以 interrupt_request 需要被 BQL 保护,其调用位置为:

因为中断的注入可能来自于 main loop 或者是其他的 vCPU thread,所以同样这个需要 BQL 的保护

qemu_mutex_iothread_locked

下面来讨论一下一些持有 BQL 的位置

resources shared between vCPU thread

在这里,重新总结一下被 vCPU 共享的资源,以及建立起来的 lock 因为 vCPU 存在一些共享资源,所以需要也是需要互斥的,下面罗列一些:

tcg vCPU thread

QEMU 只有两种模式,不存在有的 thread 模拟多个,有的模拟一个的鬼畜情况,那只是徒增复杂了。 一个 thread 模拟一个 vCPU 当然是很自然的,但是多线程带来很多挑战,具体可以参考 mttcg

rr 和 mttcg 的执行的相似指出在于,都是调用 tcg_cpus_exec 执行的,其展开之后大致如下:

    cpu_exec_start(cpu);
    cpu_exec_enter(cpu);
    while (!cpu_handle_exception(cpu, &ret)) {
        while (!cpu_handle_interrupt(cpu, &last_tb)) {
          tb_gen_code
          cpu_loop_exec_tb
        }
    }
    cpu_exec_exit(cpu);
    cpu_exec_end(cpu);

其中,cpu_exec_enter 和 cpu_exec_exit 调用 arch 相关的 hook

对比 rr_cpu_thread_fn 和 mttcg_cpu_thread_fn 的执行流程:

while (1) {
  while (cpu && cpu_work_list_empty(cpu) && !cpu->exit_request) {
    qatomic_mb_set(&rr_current_cpu, cpu);
    current_cpu = cpu;

    tcg_cpus_exec()

    cpu = CPU_NEXT(cpu); // 在这里轮转需要使用的 cpu
  }

  rr_wait_io_event();
}
while (!cpu->unplug || cpu_can_run(cpu)){
    if (cpu_can_run(cpu)) {
      tcg_cpus_exec()
    }

    qatomic_mb_set(&cpu->exit_request, 0);
    qemu_wait_io_event(cpu);
}

下面总结一下 rr 和 mttcg 的实现差异:

lifecycle of vCPU thread

exit_request

halt

x86 halt 指令会让 CPU 进入低功耗的状态,当外界有中断到来的时候,CPU 才会继续运行。 显然,当 guest 执行 halt 指令之后,host 对应的 vCPU thread 也是需要进入到 idle 的状态。

locks between vCPU

因为 kvm vCPU thread 的比较简单,就不分析了。下面只是关注 tcg 的 vCPU thread,在没有 explicit 的指出的情况下,vCPU thread 指的是 tcg vCPU thread。

static QemuMutex qemu_cpu_list_lock;   // 这个就是 cpu 的 lock,一旦持有,其他的 cpu 都不可以动弹的,也是用于实现下面的各种 cond
static QemuCond exclusive_cond;        // 用于实现 start_exclusive 中 wait,在 cpu_exec_end 中 notify 的。
static QemuCond exclusive_resume;      // 在 inclusive_idle 的调用
static QemuCond qemu_work_cond;        // 用于实现 do_run_on_cpu 的,在 process_queued_cpu_work 中 qemu_cond_broadcast(&qemu_work_cond);

static QemuMutex qemu_global_mutex;    // 这个居然就是 bql 啊
struct QemuCond * CPUState::halt_cond; // 当整个 cpu 处于 stop 的状态,那么会卡到这里去

static QemuCond qemu_pause_cond;       // pause_all_vcpus 中用于等待所有的 vCPU 进入 stop 的状态
sync between main loop and vCPU

在 x86_cpu_realizefn => qemu_init_vcpu 中,会创建并且执行 vCPU thread 如果是 -thread=single 的 tcg 的 vCPU 执行的位置从 qemu_tcg_rr_cpu_thread_fn 开始

  /* wait for initial kick-off after machine start */
  while (first_cpu->stopped) {
    qemu_cond_wait(first_cpu->halt_cond, &qemu_global_mutex);

    /* process any pending work */
    CPU_FOREACH(cpu) {
      current_cpu = cpu;
      qemu_wait_io_event_common(cpu);
    }
  }

通过上面的流程,终于可以理解为什么 tlb_flush_by_mmuidx 需要增加一个对于 CPUState::created 的判断了。

void tlb_flush_by_mmuidx(CPUState *cpu, uint16_t idxmap)
{
    tlb_debug("mmu_idx: 0x%" PRIx16 "\n", idxmap);

    if (cpu->created && !qemu_cpu_is_self(cpu)) {
        async_run_on_cpu(cpu, tlb_flush_by_mmuidx_async_work,
                         RUN_ON_CPU_HOST_INT(idxmap));
    } else {
        tlb_flush_by_mmuidx_async_work(cpu, RUN_ON_CPU_HOST_INT(idxmap));
    }
}

因为 tlb_flush_by_mmuidx 的调用比 qemu_init_vcpu 早,此时 vCPU 还被挡在 BQL 上了,是不可能有去执行 qemu_wait_io_event 来执行 async_run_on_cpu 挂载上的任务的。

/*
#0  tlb_flush_by_mmuidx (cpu=0x555556b09970, idxmap=7) at ../accel/tcg/cputlb.c:384
#1  0x0000555555c5e3e8 in listener_add_address_space (as=<optimized out>, listener=0x555556a08508) at ../softmmu/memory.c:2839
#2  memory_listener_register (listener=0x555556a08508, as=<optimized out>) at ../softmmu/memory.c:2902
#3  0x0000555555c9362a in cpu_address_space_init (cpu=cpu@entry=0x555556b09970, asidx=asidx@entry=0, prefix=prefix@entry=0x555555f871a7 "cpu-memory", mr=<optimized out>) at ../softmmu/physmem.c:759
#4  0x0000555555b772bd in tcg_cpu_realizefn (cs=0x555556b09970, errp=<optimized out>) at ../target/i386/tcg/sysemu/tcg-cpu.c:76
#5  0x0000555555cf1e6b in cpu_exec_realizefn (cpu=cpu@entry=0x555556b09970, errp=errp@entry=0x7fffffffcd70) at ../cpu.c:137
#6  0x0000555555be220e in x86_cpu_realizefn (dev=0x555556b09970, errp=0x7fffffffcdd0) at ../target/i386/cpu.c:6156

current_cpu

setlongjmp

保存上下文: sigsetjmp

跳转回去的位置: siglongjmp

采用 sigsetjmp 可以快速上下文(regs and stack),通过 siglongjmp 可以快速回到 sigsetjmp 的位置,如果函数调用层次很深,可以避免逐个返回。

queue_work_on_cpu

queue_work_on_cpu 存在三个调用者:

分析一下 async_run_on_cpu 的执行流程:

exclusive context

使用 rr 作为例子,mttcg 差不多类似:

需要 exclusive context 下执行主要是两个点:

考虑一下这个上锁的需求,多个 cpu_exec 可以同时进行,但是一旦一个进入 exclusive 的状态,其他的都不可以使用:

qemu 通过下面的变量组合起来实现的:

其好处在于:

misc

mmap_lock

static pthread_mutex_t mmap_mutex = PTHREAD_MUTEX_INITIALIZER;
static __thread int mmap_lock_count;

void mmap_lock(void)
{
    if (mmap_lock_count++ == 0) {
        pthread_mutex_lock(&mmap_mutex);
    }
}

利用 mmap_lock_count 一个 thread 可以反复上锁,但是可以防止其他 thread 并发访问。

参考两个资料,可以知道 mmap_lock 是只有用户态翻译才需要的:

  1. https://qemu.readthedocs.io/en/latest/devel/multi-thread-tcg.html
  2. tcg_region_init 上面的注释

用户态的线程数量可能很大,所以创建多个 region 是不合适的,所以只创建一个, 而且用户进程的代码大多数都是相同,所以 tb 相关串行也问题不大。

本站所有文章转发 CSDN 将按侵权追究法律责任,其它情况随意。

  1. Live Migrating QEMU-KVM Virtual Machines