多线程 TCG
本文概述多线程 TCG(MTTCG)系统模拟设计。 用户模式一直映射被翻译程序的线程结构, 不过为 MTTCG 所作的部分修改也改善了 linux-user 稳定性。
最初系统模式 TCG 只有单线程,通过轮转调度多个 CPU。 这简化了许多工作,但模拟系统核心数增长、主机单核性能提升趋缓后,限制日益明显。
vCPU 调度
新模式让每个 vCPU 在自己的用户态线程中运行。 对主机内存模型足以满足客户机的前后端组合, 即 TCGCPUOps::guest_default_memory_order & ~TCG_TARGET_DEFAULT_MO 为零, 且客户机已完成安全支持工作、设置 TCGCPUOps::mttcg_supported 时,默认启用。
以下情况回退到原轮转方式:
使用 --accel tcg,thread=single 强制单线程;
启用 --icount。
一般翻译代码执行不应有 vCPU 间依赖,各 vCPU 可全速运行。 只有访问内部共享结构,或目标架构要求机器状态一致时,才需要同步。
内存一致性
客户机与主机可能采用多种一致性模型。 即使在强序主机模拟弱序系统,也必须在客户机要求时阻止存储越过此前加载等重排。
内存屏障
屏障让软件从其他核心等外部观察者的角度,为内存操作强制特定顺序。 可以作用于全部内存操作,也可仅作用于加载或存储。
Linux 内核的 内存屏障说明 详细介绍不同屏障及其保证。
屏障常封装在同步原语周围,提供明确顺序语义; 也可单独实现安全无锁访问,例如确保载荷修改可见后,通知标志才变为可见。
设计要求:增加 tcg_memory_barrier 操作, 强制较强读写顺序,使此前加载和存储在屏障处完成。 单核、非 SMP、强序后端上可以退化为无操作。
除显式屏障,客户机每条内存访问指令也可能隐含顺序语义。 例如 x86 读写有较强顺序一致性保证,Arm 则有隐含 acquire/release 的特殊变体。
弱序主机模拟强序客户机时,性能损失可能很大。
设计要求:高效使用屏障。 主机已有更强隐含保证时可跳过部分屏障;连续屏障合并为最强形式。
当前方案:
tcg_gen_mb() 在并行代码生成上下文中添加屏障操作。 tcg_optimize() 尝试在加载/存储前合并屏障为最强形式。 方案最初为 linux-user 开发和测试,全部后端已支持按需生成屏障。 目前以下前端也已完成相应转换:
target-i386
target-arm
target-aarch64
target-alpha
target-mips
内存控制与维护
这类指令控制系统缓存行为。 虽然 QEMU 不建模缓存,但软件修改代码后常用这些指令确保变化生效。
同步原语
现代指令集主要有原子指令和独占区域两类同步原语。
第一类以单条指令保证测试和条件存储相对于其他核心共享访问确实原子, 典型例子为 x86 cmpxchg。
第二类通过一对加载/存储保证期间相关内存未被其他访问触及。 例如 Arm ldrex/strex,只有 ldrex 后没有其他 CPU 访问该区域, strex 才返回成功存储标志。
传统 TCG 生成的一组操作位于同一 TB,其他 CPU 被调度前已全部完成,因而有效。 多线程模拟多个 CPU 后,必须显式表达这些语义。
设计要求:支持传统原子指令、独占加载/存储或 LL/SC, 并提供足够通用的基础设施以覆盖全部客户机架构。 当前开放问题是:ABA 问题总体上会有多大影响?
当前方案:
TCG 提供 tcg_gen_atomic_* 辅助函数,可直接使用,
或组合模拟 Arm ldrex/strex 等指令。
虽然可能受 ABA 问题影响,常见客户机目前未使用会触发问题的模式,
通常暴露的是假定 cmpxchg 类似语义的加锁 ABI。
无法使用多线程原子操作时,例如客户机原子宽度大于主机, 会通过 EXCP_ATOMIC 退出,在独占锁下模拟指令,使全部模拟串行化。
当前辅助函数似乎够用,但未来可能需要更精确地建模客户机架构语义。