arm64 TLB 硬件设计
TLB flush 硬件行为
arm64 与 x86 在 tlb flush 差异挺大,比如 flush 的范围、同步机制。x86 的 tlb flush 一般是(1)只处理当前核,其他核是否需要刷有操作系统内核去判断,这算是同步机制的范畴,依赖软件发起 IPI 到需要的核,(2)而 arm64 的 TLBI 的范围是整个共享域,以不成熟的经验,arm64 一般只会设计一个共享域,是否其他核也需要同步,是由硬件上去判断。
先从最简单的场景,TLBI 只刷一个核开始。
首先,TLBI 在硬件上并不是一个高优的指令,在执行 TLBI 时,首先会将其存到具有 4 个条目的后台任务队列 STQ 中,等 MMU 主流水线有空闲周期时,开始处理 STQ 队列中的任务。下面是其中一个条目,命名分别是 stq0-stq3:
always_ff @(posedge clk) begin: u_mm_stq0_type_q_1_0_grp if (mm_stq0_en == 1'b1) begin mm_stq0_type_q[1:0] <= `PERSEUS_DFF_DELAY l2_cpu_snp_type[1:0]; mm_stq0_va_q[`PERSEUS_MMU_VA] <= `PERSEUS_DFF_DELAY l2_cpu_snp_addr[`PERSEUS_MMU_SNP_VA_RANGE]; mm_stq0_va_valid_q <= `PERSEUS_DFF_DELAY l2_cpu_snp_va_valid_ql; mm_stq0_vmid_valid_q <= `PERSEUS_DFF_DELAY l2_cpu_snp_vmid_valid; mm_stq0_vmid_q[`PERSEUS_MMU_VMID_RANGE] <= `PERSEUS_DFF_DELAY l2_cpu_snp_vmid[`PERSEUS_MMU_VMID_RANGE]; mm_stq0_asid_valid_q <= `PERSEUS_DFF_DELAY l2_cpu_snp_asid_valid; mm_stq0_asid_q[`PERSEUS_MMU_ASID_RANGE] <= `PERSEUS_DFF_DELAY l2_cpu_snp_asid[`PERSEUS_MMU_ASID_RANGE]; mm_stq0_sec_q[1:0] <= `PERSEUS_DFF_DELAY l2_cpu_snp_sec[1:0]; mm_stq0_el_q[2:0] <= `PERSEUS_DFF_DELAY mm_stq_el_nxt[2:0]; mm_stq0_leaf_q <= `PERSEUS_DFF_DELAY l2_cpu_snp_leaf; mm_stq0_stage_q[1:0] <= `PERSEUS_DFF_DELAY l2_cpu_snp_stage[1:0]; mm_stq0_ttl_q[1:0] <= `PERSEUS_DFF_DELAY l2_cpu_snp_ttl[1:0]; mm_stq0_tg_q[1:0] <= `PERSEUS_DFF_DELAY l2_cpu_snp_tg[1:0]; mm_stq0_num_q[4:0] <= `PERSEUS_DFF_DELAY l2_cpu_snp_num[4:0]; mm_stq0_scale_q[1:0] <= `PERSEUS_DFF_DELAY l2_cpu_snp_scale[1:0]; mm_stq0_range_q <= `PERSEUS_DFF_DELAY l2_cpu_snp_range_ql; end
当开始处理 STQ 中的任务时,会从条目中选择一个最老的指令,并判断其类型,如果满足:mm_stq_type[1:0] == 2'b00,说明是一个 TLBI 任务,mm_stq_tmo 信号会拉高,同时mm_tc_non_crit_snp_tmo_nxt 也会拉高。
assign mm_stq_tmo = (mm_stq_type[1:0] == 2'b00); assign mm_tc_non_crit_snp_tmo_nxt = mm_stq_req_vld & mm_stq_tmo;
mm_tc_non_crit_snp_tmo_nxt 信号会被发送到 TLB Array。
接下里,会在 TLB Array 进行寻找匹配的条目。
发送到 TLB Array 的信号中可以提取到 asid、va 等信息,如果匹配到条目,mm_tc_hit_for_rams_t3 信号会被拉高。
同时,匹配信号和 tmo 探测信号会合成mm_tc_tmo_write_for_rams_t3, 并通过条件向量匹配,生成一个命中 Way 的写使能信号mm_tc_chip_select_t1。
assign mm_tc_tmo_write_for_rams_t3 = mm_tc_hit_for_rams_t3 & mm_tc_snp_tmo_t3_q & mm_tc_vld_t3_q & ~mm_tc_slow_tmo_inv_t3_q; always_comb begin: u_mm_tc_chip_select_t1_4_0 casez({mm_tc_non_crit_req_t1, mm_tc_parity_err_t3, mm_tc_parity_err_t4, mm_tc_alloc_write_ql_t3, mm_tc_tmo_write_for_rams_t3, mm_tc_slow_tmo_write_t4}) 6'b000000: mm_tc_chip_select_t1[`PERSEUS_MMU_TC_WAY_MSB:0] = {`PERSEUS_MMU_TC_WAYS{(mm_tc_req_vld_t1&(~(mm_tc_req_mmu_dis)))}}; 6'b?????1: mm_tc_chip_select_t1[`PERSEUS_MMU_TC_WAY_MSB:0] = (mm_tc_way_hit_for_snp_t4_q[`PERSEUS_MMU_TC_WAY_MSB:0]|({`PERSEUS_MMU_TC_WAYS{mm_tc_parity_err_t4}}&mm_tc_way_parity_err_for_rams_t4_q[`PERSEUS_MMU_TC_WAY_MSB:0])); 6'b????10: mm_tc_chip_select_t1[`PERSEUS_MMU_TC_WAY_MSB:0] = (mm_tc_way_hit_raw_for_rams_t3_q[`PERSEUS_MMU_TC_WAY_MSB:0]|({`PERSEUS_MMU_TC_WAYS{mm_tc_parity_err_t3}}&mm_tc_way_parity_err_for_rams_t3_q[`PERSEUS_MMU_TC_WAY_MSB:0])); 6'b???100: mm_tc_chip_select_t1[`PERSEUS_MMU_TC_WAY_MSB:0] = mm_tc_rrip_chip_select_t3[`PERSEUS_MMU_TC_WAY_MSB:0]; 6'b??1000: mm_tc_chip_select_t1[`PERSEUS_MMU_TC_WAY_MSB:0] = mm_tc_way_parity_err_for_rams_t4_q[`PERSEUS_MMU_TC_WAY_MSB:0]; 6'b?10000: mm_tc_chip_select_t1[`PERSEUS_MMU_TC_WAY_MSB:0] = mm_tc_way_parity_err_for_rams_t3_q[`PERSEUS_MMU_TC_WAY_MSB:0]; 6'b100000: mm_tc_chip_select_t1[`PERSEUS_MMU_TC_WAY_MSB:0] = ((mm_tc_non_crit_chip_select_q[`PERSEUS_MMU_TC_WAY_MSB:0]&{`PERSEUS_MMU_TC_WAYS{((~(mm_mbist_en_q))|mm_mbist_tc_acc_t1)}})|{`PERSEUS_MMU_TC_WAYS{(mm_mbist_en_q&mm_mbist_all_mode_t1)}}); default: mm_tc_chip_select_t1[`PERSEUS_MMU_TC_WAY_MSB:0] = {5{1'bx}}; endcase `ifdef PERSEUS_XPROP_CASE if((^({mm_tc_non_crit_req_t1, mm_tc_parity_err_t3, mm_tc_parity_err_t4, mm_tc_alloc_write_ql_t3, mm_tc_tmo_write_for_rams_t3, mm_tc_slow_tmo_write_t4})) === 1'bx) mm_tc_chip_select_t1[`PERSEUS_MMU_TC_WAY_MSB:0] = {5{1'bx}}; `endif end
mm_tc_slow_tmo_write_t4 也是一种 TMO 信号,但与 TLBI 不同,slow tmo 的源头是内部硬件页表漫游状态机。在 MMU 缺页遍历过程中,可能会发现引起 TLB 别名冲突的情况(比如 contiguous 分裂页)。这种情况,硬件会触发一个 slow tmo 请求,强制将可能重叠的老旧 tlb entry 失效。
tmo_write 在条件向量里边在倒数第二个,属于“6'b????10”,写信号只会落在命中的哪一路(Way)。
有了“写使能信号”和“被选中的那一条 Way”之后,最后一个问题是:写入的数据是什么内容?
mm_tc_data_in_t1 是送到 TLB Array 的原始数据。在下面的多路选择器中,TMO 失效写属于后者,原始数据将会被全部设置为 0({`PERSEUS_MMU_TC_WIDTH{1'b0}})。到此,TLBI 失效指令的擦除任务就完成了。
assign mm_tc_data_in_t1[`PERSEUS_MMU_TC_DATA_RANGE] = (mm_tc_alloc_write_ql_t3 | mm_mbist_en_q) ? mm_tc_non_crit_wr_data_q[`PERSEUS_MMU_TC_DATA_RANGE] : {`PERSEUS_MMU_TC_WIDTH{1'b0}};
后续补上对于多核,tlbi snoop 的大致流程。
2M/4K TLB entries 处理
arm 与 x86 在 TLB 设计上有一个明显的差异,x86 的 tlb entries 是区分 4K、2M 和 1G 的,一个 tlb entry 不能既能缓存 4k 页表,还能缓存 2M 页表,而 arm 的 tlb entry 是不区分页面大小。arm 在设计上花了更多的逻辑处理页面大小,比如在 tlb entry 上划分有 PGSZ 字段保存当前页面大小,同时也支持 tlb 合并功能,连续的 4k、16k 等可以合并成一个支持更大页面的 tlb entry,用于节省 tlb 资源。
从mm_tsm_s1_s2_pgsz 信号开始:
`define PERSEUS_MMU_TC_DATA_COLT 6 `define PERSEUS_MMU_TC_DATA_RSLVD_PGSZ `PERSEUS_MMU_TC_DATA_COLT+13:`PERSEUS_MMU_TC_DATA_COLT+11 `define PERSEUS_MMU_TC_DATA_RSLVD_PGSZ_M1 `PERSEUS_MMU_TC_DATA_COLT+12:`PERSEUS_MMU_TC_DATA_COLT+11 assign mm_tsm_s1_s2_pgsz[6:0] = mm_tsm_s1_final_pgsz_dec[6:0] | mm_tsm_s2_desc_pgsz_dec[6:0];
mm_tsm_s1_s2_pgsz[6:0] 的每一位对应一级基础翻译块的大小,如下:
[0]: 表示这是一个基础粒度为 4KB 的 Page。[1]: 表示这是一个基础粒度为 16KB 的 Page。[2]: 表示这是一个基础粒度为 64KB 的 Page。[3]: 表示这是一个基础粒度为 2MB 的 Block(通常来自 4KB Granule 的 Level 2 Block 或 16KB 连续块相关状态)。[4]: 表示这是一个基础粒度为 32MB 的 Block(通常来自 16KB Granule 的 Level 2 Block)。[5]: 表示这是一个基础粒度为 512MB 的 Block(通常来自 64KB Granule 的 Level 2 Block)。[6]: 表示这是一个基础粒度为 1GB 的 Block(通常来自 4KB Granule 的 Level 1 Block)。
下面是一段场景的优先级编码器,主要找出mm_tsm_s1_s2_pgsz[6:0] 中第一个出现 1 的位置:
assign mm_tsm_s1_s2_rslvd_pgsz[2] = (mm_tsm_s1_s2_pgsz[6]&!mm_tsm_s1_s2_pgsz[5] &!mm_tsm_s1_s2_pgsz[4]&!mm_tsm_s1_s2_pgsz[3]&!mm_tsm_s1_s2_pgsz[2]&!mm_tsm_s1_s2_pgsz[1]&!mm_tsm_s1_s2_pgsz[0]) | (mm_tsm_s1_s2_pgsz[5]&!mm_tsm_s1_s2_pgsz[4]&!mm_tsm_s1_s2_pgsz[3]&!mm_tsm_s1_s2_pgsz[2]&!mm_tsm_s1_s2_pgsz[1]&!mm_tsm_s1_s2_pgsz[0]) | (mm_tsm_s1_s2_pgsz[4]&!mm_tsm_s1_s2_pgsz[3]&!mm_tsm_s1_s2_pgsz[2]&!mm_tsm_s1_s2_pgsz[1]&!mm_tsm_s1_s2_pgsz[0]) | (mm_tsm_s1_s2_pgsz[3]&!mm_tsm_s1_s2_pgsz[2]&!mm_tsm_s1_s2_pgsz[1]&!mm_tsm_s1_s2_pgsz[0]); assign mm_tsm_s1_s2_rslvd_pgsz[1] = (mm_tsm_s1_s2_pgsz[6]&!mm_tsm_s1_s2_pgsz[5]&!mm_tsm_s1_s2_pgsz[4]&!mm_tsm_s1_s2_pgsz[3]&!mm_tsm_s1_s2_pgsz[2]&!mm_tsm_s1_s2_pgsz[1]&!mm_tsm_s1_s2_pgsz[0]) | (mm_tsm_s1_s2_pgsz[5]&!mm_tsm_s1_s2_pgsz[4]&!mm_tsm_s1_s2_pgsz[3]&!mm_tsm_s1_s2_pgsz[2]&!mm_tsm_s1_s2_pgsz[1]&!mm_tsm_s1_s2_pgsz[0]) | (mm_tsm_s1_s2_rslvd_colt&mm_tsm_s1_s2_pgsz[1]&!mm_tsm_s1_s2_pgsz[0]) | (mm_tsm_s1_s2_pgsz[2]&!mm_tsm_s1_s2_pgsz[1]&!mm_tsm_s1_s2_pgsz[0]); assign mm_tsm_s1_s2_rslvd_pgsz[0] = (mm_tsm_s1_s2_pgsz[6]&!mm_tsm_s1_s2_pgsz[5]&!mm_tsm_s1_s2_pgsz[4]&!mm_tsm_s1_s2_pgsz[3]&!mm_tsm_s1_s2_pgsz[2]&!mm_tsm_s1_s2_pgsz[1]&!mm_tsm_s1_s2_pgsz[0]) | (mm_tsm_s1_s2_pgsz[4]&!mm_tsm_s1_s2_pgsz[3]&!mm_tsm_s1_s2_pgsz[2]&!mm_tsm_s1_s2_pgsz[1]&!mm_tsm_s1_s2_pgsz[0]) | (mm_tsm_s1_s2_rslvd_colt&mm_tsm_s1_s2_pgsz[2]&!mm_tsm_s1_s2_pgsz[1]) | (!mm_tsm_s1_s2_rslvd_colt&mm_tsm_s1_s2_pgsz[1]&!mm_tsm_s1_s2_pgsz[0]) | (mm_tsm_s1_s2_rslvd_colt&mm_tsm_s1_s2_pgsz[0]);
以mm_tsm_s1_s2_rslvd_pgsz[2] 为例,其为 1 有四种情况:
- mm_tsm_s1_s2_pgsz[6] 为 1,且其余 LSB 0-5 都为 0;
- mm_tsm_s1_s2_pgsz[5] 为 1,且其余 LSB 0-4 都为 0;
- mm_tsm_s1_s2_pgsz[4] 同上类推;
- mm_tsm_s1_s2_pgsz[3] 同上类推;
mm_tsm_s1_s2_rslvd_pgsz[0] 和mm_tsm_s1_s2_rslvd_pgsz[1] 信号逻辑类似,多出了一个mm_tsm_s1_s2_rslvd_colt 合并信号也能合成对应的 PGSZ。关于 TLB 合并的细节就不在此处介绍。
| 对应的有效命中页大小 | 典型应用场景 / 来源溯源 |
(0) | 4KB | 纯粹的 4KB 小页,无 Colt(硬件聚合)。 |
(1) | 16KB |
|
(2) | 64KB |
|
(3) | 256KB | 四个 64KB 依靠 Colt 底层连片聚合而成的(ARM 架构中本身没有独立的 256K 页面结构)。 |
(4) | 2MB | 4KB 粒度对应的 2MB Block 大页。 |
(5) | 32MB | 16KB 粒度对应的 32MB Block 大页。 |
(6) | 512MB | 64KB 粒度对应的 512MB Block 大页。 |
(7) | 1GB (或以上) | 4KB 粒度对应的超级大页 1GB(部分连带逻辑可能复用这档处理最大的比如 64GB 映射)。 |
最终,mm_tsm_s1_s2_rslvd_pgsz[2:0] 会被写入到 tlb:
assign mm_tsm_s1_s2_rslvd_attr[`PERSEUS_MMU_TC_DATA_RSLVD_PGSZ] = mm_tsm_s2_in[5] ? mm_tsm_s2_err_info[17:15] : mm_tsm_s1_s2_rslvd_pgsz[2:0]; assign mm_tsm_s1_s2_rslvd_attr[`PERSEUS_MMU_TC_DATA_S1_PGSZ] = mm_tsm_tc_entry_s1_pgsz[2:0]; assign mm_tsm_s1_s2_rslvd_attr[`PERSEUS_MMU_TC_DATA_S1_PXN] = mm_tsm_tc_entry_s1_pxn & mm_tsm_s1_mmu_en; assign mm_tsm_s1_s2_rslvd_attr[`PERSEUS_MMU_TC_DATA_S1_XN] = mm_tsm_tc_entry_s1_xn & mm_tsm_s1_mmu_en; assign mm_tsm_s1_s2_rslvd_attr[`PERSEUS_MMU_TC_DATA_S2_PXN] = mm_tsm_s2_in[5] ? 1'b0 : mm_tsm_s2_desc_pxn; assign mm_tsm_s1_s2_rslvd_attr[`PERSEUS_MMU_TC_DATA_S2_XN] = mm_tsm_s2_in[5] ? 1'b0 : mm_tsm_s2_desc_uxn;
可以发现,这里有两个 PGSZ:PERSEUS_MMU_TC_DATA_RSLVD_PGSZ、PERSEUS_MMU_TC_DATA_S1_PGSZ,其中(1)RSLVD_PGSZ 表示实际硬件上存储的真实有效的页面大小,例如虚拟化场景,需要考虑 S1 和 S2,取最小识别的尺寸,同时也会存在TLB Coalescing 合并的情况;(2)S1_PGSZ 主要是指 S1 页表识别的大小,比如 Guest 在 S1 阶段解析的页面大小。
综上所述,其实就是 TLB 页表项中有 PGSZ 字段用于存储当前页表项所表示的页面大小。
页表 AF 更新逻辑
根据 ARMv9 的设计,在 TLB miss 后读取目标的页表,获取内存中的页表数据后,如果发现 AF 为 0,则会在总线上触发一个 CAS 事务操作修改内存中 AF 位。同时,也会将写入 TLB 项的 AF 置为 1。这是操作系统判断页面冷热的基础机制。
在下面的逻辑信号中,~mm_tsm_desc_af 表示如果 AF 为 0,则会拉高mm_hw_af_upd_s1_pq。
assign mm_hw_af_upd_s1_pq = ~mm_tsm_desc_af & // 发现当前描述符AF=0 mm_tsm_s1_desc_done_q & // 描述符读取完成 ~mm_tsm_s1_wlk_pending & mm_tcr_elx_ha_q; // TCR_EL1.HA 位已开启(硬件管理使能) assign mm_hw_ad_upd_s1 = (mm_hw_af_upd_s1 | mm_hw_dbm_upd_s1) & ~mm_tsm_rd_type_q & ~mm_tsm_desc_sbe_q;
mm_hw_af_upd_s1_pq 属于 s1 硬件 AF 更新使能信号,后续,会将 AF 和 DBM(dirty 位更新)合并为mm_hw_ad_upd_s1 信号,该信号会在总线上触发一个 CAS 事务修改内存页表。
同时,MMU 模块也会根据以上信号判断在填充到 TLB 前是否将 AF 根据为 1:
assign mm_tsm_desc_af_nxt = mm_tsm_s1_desc_fth_req ? (mm_tsm_desc_data_ee[10] | mm_hw_af_upd_s1_pq) : (mm_tsm_desc_data_ee[10] | mm_hw_af_upd_s2_pq) ;
内存上页表 AF 更新和填充到 TLB 上的 AF 更新没有前后约束,理论上 TLB 上的 AF 更新快一步。
当前内存上的 AF 被重新设置为 0 时,硬件上会有什么反应?
若操作系统清除某个页表项的内存中的 AF 位,且没有执行 TLB flush,硬件 TLB 是不会感知,即硬件上该项页表的 AF 还是为 1,只有等该页表项失效后,例如被替换或主动 flush,才会重新读取内存中页表数据。因此,操作系统在更新内存中的 AF 位后,总是会执行一次 tlb flush,目的就是失效对应的 TLB entry,下次重新检查 AF 才能表示这段时间是否被访问。