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);
|