QEMU softmmu模型
本文将分析 QEMU TCG 模式下的访存模型,也就是 softmmu 的设计,基于的版本为 QEMU 6.2,架构则以 RISC-V 为例。
基本调用链
1. target/riscv/translate.c 访存指令翻译。
2. accel/tcg/cputlb.c 调用 helper 加载函数(如 helper_le_ldq_mmu)。
3. 调用 load_helper 函数
1. 查 TLB,若未命中,则 tlb_fill 进行填充。
2. 处理各种特殊情况(MMIO、不对界访问等)。
3. 计算得到对应的宿主机虚拟地址 haddr = addr + entry->addend,并根据字长进行访问。
TLB 数据结构
QEMU 的 softmmu 模型的核心数据结构为其 TLB 的设计,结构如下:
CPUTLB
typedef struct CPUTLB {
CPUTLBCommon c; // 存储 TLB 的一系列元数据。
CPUTLBDesc d[NB_MMU_MODES]; // 慢速(二级) TLB,主要用于存储从一级 TLB 中被驱逐(evict)出的条目。
CPUTLBDescFast f[NB_MMU_MODES]; // 快速(一级) TLB,用于快速完成地址转换。
} CPUTLB;
CPUTLBDesc
typedef struct CPUTLBDesc {
/* 大页处理相关 */
target_ulong large_page_addr;
target_ulong large_page_mask;
/* 与 TLB 动态调整相关 */
int64_t window_begin_ns;
size_t window_max_entries;
size_t n_used_entries;
/* vTLB 中表中使用的下一个索引 */
size_t vindex;
/* 二级 TLB(vTLB) 和二级 IOTLB(vIOTLB) */
CPUTLBEntry vtable[CPU_VTLB_SIZE];
CPUIOTLBEntry viotlb[CPU_VTLB_SIZE];
/* 一级 IOTLB,与 IOMMU 相关(不太了解) */
CPUIOTLBEntry *iotlb;
} CPUTLBDesc;
CPUTLBDescFast
typedef struct CPUTLBDescFast {
uintptr_t mask; // 用于完成 (address, mmu_idx) -> TLB_index 的映射
CPUTLBEntry *table; // 一级 TLB 表
} CPUTLBDescFast QEMU_ALIGNED(2 * sizeof(void *));
CPUTLBEntry
typedef struct CPUTLBEntry {
union {
struct {
/* 用于与 address 对比判断是否命中 */
target_ulong addr_read;
target_ulong addr_write;
target_ulong addr_code;
/* 宿主机虚拟地址 haddr 与模拟器虚拟地址 address 的偏移量,用于地址转换 */
uintptr_t addend;
};
uint8_t dummy[1 << CPU_TLB_ENTRY_BITS];
};
} CPUTLBEntry;
这里为读、写、执行都分别设置一个地址字段,其实是一种空间换时间的策略。比如说一个页面(如地址为 addr
)具有 可读可写但不可执行 的权限,那么在进行 TLB 填充时,字段 addr_read
和 addr_write
都会被赋上 addr
的值,而 addr_code
则为(无符号)-1。这样在后续进行 TLB 命中判定时,本次是什么访问方式就与哪个字段进行比对,那么自然,如果本次针对 addr
的访问是取址访问(执行),自然就会发生 TLB miss。
这样的设计可以使得 TLB 命中判定仅由一条 cmp
指令来完成,而如果使用类似页表条目的设计方法,引入一些权限位来标识页面是否可读可写可执行,空间占用自然更少,但同时比对效率也更低。
内存访问
load_helper
load_helper/store_helper
是 QEMU softmmu 访存的核心函数,作用是根据 addr
和访问类型来对指定的模拟器内存进行对应的读/写操作。本文只分析 load_helper
,store_helper
的实现与其类似。
/*
* env: CPU 架构相关的状态寄存器集合
* addr: 要读取的模拟器目标虚拟地址
* oi: 内存操作索引(包含 Memop 和 mmu_idx)
* retaddr: 调用者返回地址(用于异常处理)
* op: 内存操作类型(大小、端序)
* code_read: 标志(是取指还是数据访问)
* full_load: 用于递归处理不对界/跨页
*/
static inline uint64_t QEMU_ALWAYS_INLINE
load_helper(CPUArchState *env, target_ulong addr, MemOpIdx oi,
uintptr_t retaddr, MemOp op, bool code_read,
FullLoadHelper *full_load)
{
uintptr_t mmu_idx = get_mmuidx(oi);
uintptr_t index = tlb_index(env, mmu_idx, addr);
CPUTLBEntry *entry = tlb_entry(env, mmu_idx, addr);
target_ulong tlb_addr = code_read ? entry->addr_code : entry->addr_read;
const size_t tlb_off = code_read ?
offsetof(CPUTLBEntry, addr_code) : offsetof(CPUTLBEntry, addr_read);
const MMUAccessType access_type =
code_read ? MMU_INST_FETCH : MMU_DATA_LOAD;
unsigned a_bits = get_alignment_bits(get_memop(oi));
void *haddr;
uint64_t res;
size_t size = memop_size(op);
/* 处理架构相关的访存不对界异常 */
if (addr & ((1 << a_bits) - 1)) {
cpu_unaligned_access(env_cpu(env), addr, access_type,
mmu_idx, retaddr);
}
/* 判断一级 TLB 是否命中 */
if (!tlb_hit(tlb_addr, addr)) {
/*
* 判断二级 TLB 是否命中
* 若二级 TLB 命中,则将二级 TLB 中的条目与 addr 对应的一级 TLB
* 中的条目进行交换,此后 entry 将为正确的条目。
*/
if (!victim_tlb_hit(env, mmu_idx, index, tlb_off,
addr & TARGET_PAGE_MASK)) {
/* 若二级 TLB 未命中,则需要进行填充。 */
tlb_fill(env_cpu(env), addr, size,
access_type, mmu_idx, retaddr);
index = tlb_index(env, mmu_idx, addr);
entry = tlb_entry(env, mmu_idx, addr);
}
tlb_addr = code_read ? entry->addr_code : entry->addr_read;
tlb_addr &= ~TLB_INVALID_MASK;
}
/* 处理一些特殊情况(TLB 的 tlb_addr 的低位存储着一些属性位) */
if (unlikely(tlb_addr & ~TARGET_PAGE_MASK)) {
CPUIOTLBEntry *iotlbentry;
bool need_swap;
if ((addr & (size - 1)) != 0) {
goto do_unaligned_access;
}
iotlbentry = &env_tlb(env)->d[mmu_idx].iotlb[index];
/* 处理观测点访问 */
if (unlikely(tlb_addr & TLB_WATCHPOINT)) {
cpu_check_watchpoint(env_cpu(env), addr, size,
iotlbentry->attrs, BP_MEM_READ, retaddr);
}
/* 判断是否需要端序交换 */
need_swap = size > 1 && (tlb_addr & TLB_BSWAP);
/* 处理 I/O 访问 */
if (likely(tlb_addr & TLB_MMIO)) {
return io_readx(env, iotlbentry, mmu_idx, addr, retaddr,
access_type, op ^ (need_swap * MO_BSWAP));
}
haddr = (void *)((uintptr_t)addr + entry->addend);
/* 两个 load_memop 分开写便于编译器优化(不太懂) */
if (unlikely(need_swap)) {
return load_memop(haddr, op ^ MO_BSWAP);
}
return load_memop(haddr, op);
}
/* 处理慢速的不对界访问 (横跨多个页面或者 I/O). */
if (size > 1
&& unlikely((addr & ~TARGET_PAGE_MASK) + size - 1
>= TARGET_PAGE_SIZE)) {
target_ulong addr1, addr2;
uint64_t r1, r2;
unsigned shift;
do_unaligned_access:
addr1 = addr & ~((target_ulong)size - 1);
addr2 = addr1 + size;
r1 = full_load(env, addr1, oi, retaddr);
r2 = full_load(env, addr2, oi, retaddr);
shift = (addr & (size - 1)) * 8;
if (memop_big_endian(op)) {
res = (r1 << shift) | (r2 >> ((size * 8) - shift));
} else {
res = (r1 >> shift) | (r2 << ((size * 8) - shift));
}
return res & MAKE_64BIT_MASK(0, size * 8);
}
/* 加上 TLB 条目的 addend 偏移量得到宿主机的虚拟地址 */
haddr = (void *)((uintptr_t)addr + entry->addend);
return load_memop(haddr, op);
}
TLB 填充
tlb_set_page
/*
* cpu: CPU 数据结构
* vaddr: 虚拟地址
* paddr: 虚拟地址对应的物理地址
* attrs: 内存事务属性,通常为 UNSPECIFIED
* prot: 访问权限(读/写/执行)
* mmu_idx: 地址空间标识符
* size: 映射大小(支持大页)
*/
void tlb_set_page_with_attrs(CPUState *cpu, target_ulong vaddr,
hwaddr paddr, MemTxAttrs attrs, int prot,
int mmu_idx, target_ulong size)
{
CPUArchState *env = cpu->env_ptr;
CPUTLB *tlb = env_tlb(env);
CPUTLBDesc *desc = &tlb->d[mmu_idx];
MemoryRegionSection *section;
unsigned int index;
target_ulong address;
target_ulong write_address;
uintptr_t addend;
CPUTLBEntry *te, tn;
hwaddr iotlb, xlat, sz, paddr_page;
target_ulong vaddr_page;
int asidx = cpu_asidx_from_attrs(cpu, attrs);
int wp_flags;
bool is_ram, is_romd;
assert_cpu_is_self(cpu);
if (size <= TARGET_PAGE_SIZE) {
sz = TARGET_PAGE_SIZE;
} else {
/* 记录大页信息 */
tlb_add_large_page(env, mmu_idx, vaddr, size);
sz = size;
}
vaddr_page = vaddr & TARGET_PAGE_MASK;
paddr_page = paddr & TARGET_PAGE_MASK;
/*
* 将物理内存区域转换为对应的内存区域 MemoryRegionSection
* 并获取内存区域的偏移量 xlat,实际可用大小 sz 和访问权限 prot
*/
section = address_space_translate_for_iotlb(cpu, asidx, paddr_page,
&xlat, &sz, attrs, &prot);
assert(sz >= TARGET_PAGE_SIZE);
tlb_debug("vaddr=" TARGET_FMT_lx " paddr=0x" TARGET_FMT_plx
" prot=%x idx=%d\n",
vaddr, paddr, prot, mmu_idx);
address = vaddr_page;
/* 映射小于页大小(奇怪的情况?) */
if (size < TARGET_PAGE_SIZE) {
/* 使得 TLB 条目无效化 */
address |= TLB_INVALID_MASK;
}
if (attrs.byte_swap) {
address |= TLB_BSWAP;
}
is_ram = memory_region_is_ram(section->mr);
is_romd = memory_region_is_romd(section->mr);
if (is_ram || is_romd) {
addend = (uintptr_t)memory_region_get_ram_ptr(section->mr) + xlat;
} else {
addend = 0;
}
write_address = address;
if (is_ram) {
iotlb = memory_region_get_ram_addr(section->mr) + xlat;
if (prot & PAGE_WRITE) {
if (section->readonly) {
write_address |= TLB_DISCARD_WRITE;
} else if (cpu_physical_memory_is_clean(iotlb)) {
write_address |= TLB_NOTDIRTY;
}
}
} else {
iotlb = memory_region_section_get_iotlb(cpu, section) + xlat;
write_address |= TLB_MMIO;
if (!is_romd) {
address = write_address;
}
}
/* 检测当前页面是否设置了监视点 */
wp_flags = cpu_watchpoint_address_matches(cpu, vaddr_page,
TARGET_PAGE_SIZE);
index = tlb_index(env, mmu_idx, vaddr_page);
te = tlb_entry(env, mmu_idx, vaddr_page);
qemu_spin_lock(&tlb->c.lock);
/* 标记 TLB 为脏 */
tlb->c.dirty |= 1 << mmu_idx;
/* 确保 vTLB 中没有 vaddr 的缓存 */
tlb_flush_vtlb_page_locked(env, mmu_idx, vaddr_page);
/*
* 如果对应 TLB 条目位置现已存在其他 vaddr 的条目,
* 则将其驱逐至 vTLB 中
*/
if (!tlb_hit_page_anyprot(te, vaddr_page) && !tlb_entry_is_empty(te)) {
unsigned vidx = desc->vindex++ % CPU_VTLB_SIZE;
CPUTLBEntry *tv = &desc->vtable[vidx];
copy_tlb_helper_locked(tv, te);
desc->viotlb[vidx] = desc->iotlb[index];
tlb_n_used_entries_dec(env, mmu_idx);
}
desc->iotlb[index].addr = iotlb - vaddr_page;
desc->iotlb[index].attrs = attrs;
/* 设置 addend 字段,使得 vaddr_page + addend = haddr */
tn.addend = addend - vaddr_page;
/* 设置可读的条目 */
if (prot & PAGE_READ) {
tn.addr_read = address;
if (wp_flags & BP_MEM_READ) {
tn.addr_read |= TLB_WATCHPOINT;
}
} else {
tn.addr_read = -1;
}
/* 设置可执行的条目 */
if (prot & PAGE_EXEC) {
tn.addr_code = address;
} else {
tn.addr_code = -1;
}
/* 设置可写的条目 */
tn.addr_write = -1;
if (prot & PAGE_WRITE) {
tn.addr_write = write_address;
if (prot & PAGE_WRITE_INV) {
tn.addr_write |= TLB_INVALID_MASK;
}
if (wp_flags & BP_MEM_WRITE) {
tn.addr_write |= TLB_WATCHPOINT;
}
}
/* 更新 TLB 条目 */
copy_tlb_helper_locked(te, &tn);
tlb_n_used_entries_inc(env, mmu_idx);
qemu_spin_unlock(&tlb->c.lock);
}
void tlb_set_page(CPUState *cpu, target_ulong vaddr,
hwaddr paddr, int prot,
int mmu_idx, target_ulong size)
{
tlb_set_page_with_attrs(cpu, vaddr, paddr, MEMTXATTRS_UNSPECIFIED,
prot, mmu_idx, size);
}
大页处理
一个值得一提的内容是 QEMU TLB 对大页的处理,可能也是为了性能的权衡,QEMU 对此的策略就是不支持。
当向 TLB 中填充页大小大于 TARGET_PAGE_SIZE
的条目时,QEMU 会调用 tlb_add_large_page
进行大页的记录,代码如下:
static void tlb_add_large_page(CPUArchState *env, int mmu_idx,
target_ulong vaddr, target_ulong size)
{
target_ulong lp_addr = env_tlb(env)->d[mmu_idx].large_page_addr;
target_ulong lp_mask = ~(size - 1);
if (lp_addr == (target_ulong)-1) {
/* 此前未记录大页 */
lp_addr = vaddr;
} else {
/* 扩展已存在的大页来将新的区域包含进去 */
lp_mask &= env_tlb(env)->d[mmu_idx].large_page_mask;
while (((lp_addr ^ vaddr) & lp_mask) != 0) {
lp_mask <<= 1; // 扩大掩码直到覆盖新地址
}
}
env_tlb(env)->d[mmu_idx].large_page_addr = lp_addr & lp_mask;
env_tlb(env)->d[mmu_idx].large_page_mask = lp_mask;
}
它的基本逻辑就是将本次访存的地址和大小记录下来,如果先前已经记录过大页,那么则将其记录的掩码进行扩大,以覆盖本次记录的大页的范围。
具体来说,对于一个 2MB 大页,它在进行 TLB 填充时,每次只会填一个 4KB 小页。但是在 Guest 系统层,它认为存在这么一个 2MB 的大页,因此在它想要无效化大页条目时,我们需要将单独进行填充的若干个小页条目全部无效化,为此 QEMU 采取了一种保守做法:直接将该 mmu_idx
下的所有的 TLB 条目全部刷新。代码如下:
static void tlb_flush_page_locked(CPUArchState *env, int midx,
target_ulong page)
{
target_ulong lp_addr = env_tlb(env)->d[midx].large_page_addr;
target_ulong lp_mask = env_tlb(env)->d[midx].large_page_mask;
/* Check if we need to flush due to large pages. */
if ((page & lp_mask) == lp_addr) {
tlb_debug("forcing full flush midx %d ("
TARGET_FMT_lx "/" TARGET_FMT_lx ")\n",
midx, lp_addr, lp_mask);
tlb_flush_one_mmuidx_locked(env, midx, get_clock_realtime());
} else {
if (tlb_flush_entry_locked(tlb_entry(env, midx, page), page)) {
tlb_n_used_entries_dec(env, midx);
}
tlb_flush_vtlb_page_locked(env, midx, page);
}
}