使用host原子指令模拟
用一个原子加指令为例,说明下原子指令模拟的逻辑,如下是amoadd指令模拟的基本逻辑。
1 | /* target/riscv/insn_trans/trans_rva.c.inc */ |
如上最后一个函数的定义在:tcg/tcg-op.c
1 | #define GEN_ATOMIC_HELPER(NAME, OP, NEW) \ |
do_atomic_op_i64里会调用gen_helper_atomic_add_xxx,这个函数的定义在:
accel/tcg/atomic_template.h
1 | #define GEN_ATOMIC_HELPER(X) \ |
可以看到,里面还是使用的host平台上的基本的原子语义函数做的。
vCPU互斥模拟原子指令
如果需要模拟多条指令拼起来的原子指令,我们就考虑用锁保护。要保护的对象是内存的状态。
之所以需要保护,是多CPU可能会去改相同的内存位置。qemu使用一个线程模拟一个CPU,
所以一个CPU对本CPU的寄存器的更新总是顺序的,所以CPU的寄存器状态是不需要做互斥的。
对于无法映射到host上原子指令的情况,其实qemu里已经做了处理,我们也可以直接使用
qemu中的方式处理。我们可以参考qemu对i386 cmpxchg16b指令的处理:qemu/target/i386/tcg/mem_helper.c
1 | helper_cmpxchg16b |
如上,把CPU的状态设置为atomic异常,回退当前guest PC,这个使得下次再进来的时候可以
使指令再次执行。最后用长跳转跳出整个tb翻译执行的大循环。可以从
accel/tcg/tcg-accel-ops-mttcg.c中的CPU线程代码看相关调用:
1 | mttcg_cpu_thread_fn |
可以看到cpu_exec_step_atomic里有tb的翻译执行的小循环。这里需要注意的地方有,tb
翻译执行是在一个互斥区里,执行tb翻译执行之前把这个tb配置成了只容许有一条guest指令,
这样做是为了使临界区尽量小。相应的cmpxchg16b翻译执行跑两遍,第一遍触发发原子异常,
第二遍跑到同样的位置会进入一个无锁的实现里执行一遍:target/i386/tcg/translate.c:
gen_helper_cmpxchg16b_unlocked,控制进哪个分支的逻辑是tb cflags的CF_PARALLEL,
在进入cpu_exec_step_atomic的时候会把这个标记为去掉,翻译的时候就会进入相应的代码,
产生相应的tb,如果是直接lookup执行这个tb,注意tb lookup的参数里也包含了
tb的cflags。
vCPU互斥区代码细节分析
cpu_exec_step_atomic里使用start_exclusive/end_exclusive创建一个互斥区,在这个区间
里,系统里只有当前的vCPU在运行,其它的vCPU线程都处于挂起状态。
start_exclusive/end_exclusive的细节逻辑分析如下:
1 | void start_exclusive(void) |
end_exclusive里把pending_cpus清0,表示不需要其它vCPU挂起了,然后唤醒exclusive_resume
条件变量上等待的其它vCPU线程,相关逻辑同样被qemu_cpu_list_lock保护。
可以看到start_exclusive/end_exclusive还需要和cpu_exec_start/cpu_exec_end的逻辑一起
才能构造出vCPU互斥区。