本文将分析 QEMU TCG 模式下的访存模型,也就是 softmmu 的设计,基于的版本为 QEMU 6.2,架构则以 RISC-V 为例。

基本调用链

1
2
3
4
5
6
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

1
2
3
4
5
typedef struct CPUTLB {
CPUTLBCommon c; // 存储 TLB 的一系列元数据。
CPUTLBDesc d[NB_MMU_MODES]; // 慢速(二级) TLB,主要用于存储从一级 TLB 中被驱逐(evict)出的条目。
CPUTLBDescFast f[NB_MMU_MODES]; // 快速(一级) TLB,用于快速完成地址转换。
} CPUTLB;

CPUTLBDesc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

1
2
3
4
typedef struct CPUTLBDescFast {
uintptr_t mask; // 用于完成 (address, mmu_idx) -> TLB_index 的映射
CPUTLBEntry *table; // 一级 TLB 表
} CPUTLBDescFast QEMU_ALIGNED(2 * sizeof(void *));

CPUTLBEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
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_readaddr_write 都会被赋上 addr 的值,而 addr_code 则为(无符号)-1。这样在后续进行 TLB 命中判定时,本次是什么访问方式就与哪个字段进行比对,那么自然,如果本次针对 addr 的访问是取址访问(执行),自然就会发生 TLB miss。

这样的设计可以使得 TLB 命中判定仅由一条 cmp 指令来完成,而如果使用类似页表条目的设计方法,引入一些权限位来标识页面是否可读可写可执行,空间占用自然更少,但同时比对效率也更低。

内存访问

load_helper

load_helper/store_helper 是 QEMU softmmu 访存的核心函数,作用是根据 addr 和访问类型来对指定的模拟器内存进行对应的读/写操作。本文只分析 load_helperstore_helper 的实现与其类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/*
* 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/* 
* 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 进行大页的记录,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 条目全部刷新。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
}
}

参考资料