0%

qemu tlb实现分析

TLB相关数据结构

每个vCPU都有一个TLB相关的数据结构,riscv上这个结构在RISCVCPU neg域段的CPUTLB tlb结构里。

1
2
3
4
5
typedef struct CPUTLB {
CPUTLBCommon c;
CPUTLBDesc d[NB_MMU_MODES];
CPUTLBDescFast f[NB_MMU_MODES];
} CPUTLB;

如上是CPUTLB的结构,CPUTLBCommon存放TLB的公有信息,目前是dirty标记、锁和一些统计
变量,CPUTLBDescFast和CPUTLBDesc存放的都是TLB的内容,两者组成一个两级TLB,其中
CPUTLBDescFast是第一级,CPUTLBDesc是第二级,搜索的时候会先查第一级然后查第二级。

NB_MMU_MODES表示TLB的种类,目前riscv上的定义是这样的:

1
2
3
4
5
6
U mode 0b000                                                              
S mode 0b001
M mode 0b011
U mode HLV/HLVX/HSV 0b100
S mode HLV/HLVX/HSV 0b101
M mode HLV/HLVX/HSV 0b111

每个MMU mode下的CPUTLBDesc和CPUTLBDescFast都有若干个TLB entry组成的TLB表, 相关
的TLB表的大小是可以动态调整的。其中一个TLB entry的定义是:

1
2
3
4
5
6
7
8
9
10
11
typedef struct CPUTLBEntry {
union {
struct {
target_ulong addr_read;
target_ulong addr_write;
target_ulong addr_code;
uintptr_t addend;
};
uint8_t dummy[1 << CPU_TLB_ENTRY_BITS];
};
} CPUTLBEntry;

TLB entry对读、写以及代码是分开做缓存的。

第二级TLB的数据结构,相关的数据可以大概分成三部分:1. 和大页相关;2. 和TLB table
动态调整大小有关系;3. TLB entry内容相关。

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct CPUTLBDesc {                                                     
target_ulong large_page_addr;
target_ulong large_page_mask;

int64_t window_begin_ns;
size_t window_max_entries;
size_t n_used_entries;
size_t vindex;

CPUTLBEntry vtable[CPU_VTLB_SIZE];
CPUTLBEntryFull vfulltlb[CPU_VTLB_SIZE];
CPUTLBEntryFull *fulltlb;
} CPUTLBDesc;

qemu里的TLB模拟并不是对真实硬件的模拟,而是针对所有构架做的一个通用的TLB实现,
它的目的是加速地址翻译。

基本逻辑

TLB的作用是加速地址访问时的地址翻译,地址访问一般分为显示地址访问和隐式地址访问,
显示访问就是通过显示的load/store指令完成地址访问,隐式的访问是CPU在运行时不通过
访存指令做的内存访问,比如访问页表以及取指令。不考虑虚拟化时,页表放在物理地址上,
所以,我们这里先只考虑load/store以及取指令中涉及的TLB逻辑。

TLB无效化是TLB相关的重要操作,一般也是软件和TLB打交道的唯一接口,有专门的TLB无效
化指令触发相关的逻辑。当虚拟地址到物理地址的映射改变时,就需要做TLB的无效化操作,
相关指令可以有不同的参数,定义TLB无效化的范围。

qemu取指令的基本逻辑可以参考这里。qemu load/store的基本逻辑可以参考这里

代码分析

TLB创建的相关代码分析:

1
2
3
4
5
6
7
8
9
10
11
/* 对于取指令和load/store操作都是在page walk成功后创建对应的TLB */
riscv_cpu_tlb_fill
+-> tlb_set_page
[...]
+-> tlb_set_page_full
/*
* 如果页属性是可写,会在TLB上打一个还没有写过的标记。因为代码页面
* 的权限在创建的时候一般不会有可写,所以,这里TLB_MOTDIRTY这个标记
* 针对的是数据相关的可写页面。
*/
+-> write_address |= TLB_MOTDIRTY;

(todo: 补充大页和iommu的逻辑)

创建tb时也会配置代码所在page对应TLB的TLB_MOTDIRTY标记,这里TLB_MOTDIRTY是专门针对
指令页面的。

1
2
3
4
5
6
7
8
9
cpu_exec
+-> tb_gen_code
+-> tb_link_page
+-> tb_page_add
+-> tlb_protect_code
[...]
+-> tlb_reset_dirty
/* 这里会把两级TLB里的TLB_MOTDIRTY都配置上 */
+-> tlb_reset_dirty_range_locked

数据的load/store访问,总是要进过TLB的,相关的逻辑可以参考这里。load/store
以及取指令中的TLB搜索逻辑基本一致,我们在如下分析中统一说明。当TLB的flag区域里有
标记时会强制进入load/store的慢速路径,在慢速路径里处理各种TLB flag,慢速路径里有
专门对TLB_MOTDIRTY的处理,所以,对于代码页面,当程序把页面改成可写,然后改动代码,
继续执行改动过的代码,就会出问题,因为guest代码可能已经被翻译到tb里,guest代码被
改动后,曾经翻译得到tb就应该被删掉,如果这个tb在chain tb的链条里,同时应该从tb链
条里把这个tb删除。相关的代码分析如下:

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
load_helper
+-> index = tlb_index(env, mmu_idx, addr);
/*
* 得到addr在第一级TLB也就是CPUTLBDescFast f中的entry,CPUTLBDescFast中的
* mask保存CPUTLBDescFast里每种MMU mode下TLB table的size,第一级TLB的是按照
* 虚拟地址页号在TLB table中依次存放。
*/
+-> entry = tlb_entry(env, mmu_idx, addr);
+-> tlb_addr = code_read ? entry->addr_code : entry->addr_read;

/* 判断第一级TLB是否命中 */
+-> if (!tlb_hit(tlb_addr, addr))
/*
* 第一级TLB没有命中,继续找第二级TLB,第二级TLB命中后直接把TLB的值和
* 第一级TLB交换。
*/
if (!victim_tlb_hit(env, mmu_idx, index, tlb_off,
addr & TARGET_PAGE_MASK)) {
/* 第二级TLB没有命中,于是去做page table walk */
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 entry中物理地址的低位保存一些属性bit */
+-> if (unlikely(tlb_addr & ~TARGET_PAGE_MASK)) {
/*
* 处理的内容有:非对齐情况,TLB_WATCHPOINT,TLB_MMIO, TLB_BSWAP。
* 在store_helper里,处理的内容还包括:TLB_DISCARD_WRITE,TLB_NOTDIRTY?
*/
[...]

/* guest PA加一个偏移addend得到host VA */
haddr = (void *)((uintptr_t)addr + entry->addend);
return load_memop(haddr, op);
}

/* 也是处理非对齐的情况?*/
+-> if (size > 1 && unlikely((addr & ~TARGET_PAGE_MASK) + size - 1 >= TARGET_PAGE_SIZE)) {
[...]
}

/* guest PA加一个偏移addend得到host VA,这里是主路径上 */
+-> haddr = (void *)((uintptr_t)addr + entry->addend);
+-> return load_memop(haddr, op);

但是,指令的访问不一定每次都要经过TLB,可以说大部分不经过TLB,因为翻译过成的TB
块是可以chain在一起的,这样整个执行的过程可能全部在TB链条里跳来跳去。因为qemu约束
chain tb只能在一个page内,所以tb在一个page内跳来跳去是安全的。当guest的执行逻辑
进入一个新page时,取指令的时候,必然要做TLB相关的操作。

qemu提供了tlb无效化的公共函数,相关的实现在accel/tcg/cputlb.c。对于riscv或者x86
这种借助IPI做remote tlb无效化的构架,tlb无效化在qemu(机器)层面就是无效化本CPU上
的TLB, 对于ARM这种支持TLB硬件广播的构架,qemu实现就需要无效化本CPU以及其它CPU上
的TLB。下面分析TLB硬件广播的实现逻辑:

1
2
3
4
5
6
7
8
9
10
tlb_flush_by_mmuidx_all_cpus(CPUState *src_cpu, uint16_t idxmap)           
+-> const run_on_cpu_func fn = tlb_flush_by_mmuidx_async_work;
+-> flush_all_helper(src_cpu, fn, RUN_ON_CPU_HOST_INT(idxmap));
/* 这里是用什么同步的?*/
+-> fn(src_cpu, RUN_ON_CPU_HOST_INT(idxmap));
+-> tlb_flush_one_mmuidx_locked
/* 动态调整TLB table的大小就在这里 */
+-> tlb_mmu_resize_locked(desc, fast, now);
/* TLB无效化在这里实施 */
+-> tlb_mmu_flush_locked(desc, fast);