Skip to the content.

QEMU 中的锁

没有完全搞清楚这个到底怎么回事的,很烦! 很多是 tcg 相关的,也是需要分离开的。

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

void cpu_exit(CPUState *cpu)
{
    qatomic_set(&cpu->exit_request, 1);
    /* Ensure cpu_exec will see the exit request after TCG has exited.  */
    smp_wmb();
    qatomic_set(&cpu->icount_decr_ptr->u16.high, -1);
}

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 挂载上的任务的。

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 将按侵权追究法律责任,其它情况随意。