MIPS Linux 内核页表隐性 Bug 分析与修复
32 bit kernel, 16KB 页大小,龙芯2E平台
tlb_refill_handler为:
lui k1, %hi(pgd_current) mfc0 k0, C0_BADVADDR lw k1, %lo(pgd_current)(k1) srl k0, k0, PGDIR_SHIFT sll k0, k0, PGD_T_LOG2 addu k1, k1, k0 mfc0 k0, C0_CONTEXT lw k1, 0(k1) srl k0, k0, 3 andi k0, k0, (PTRS_PER_PTE/2-1) << (PTE_T_LOG2 + 1) addu k1, k1, k0 lw k0, 0(k1) lw k1, sizeof(pte_t)(k1) srl k0, k0, 6 mtc0 k0, C0_ENTRYLO0 srl k1, k1, 6 mtc0 k1, C0_ENTRYLO1 tlbwr eret
相应的,直接从正在运行的 kernel 里获取的 tlb_refill_handler 为:
0: 52801b3c lui k1,0x8052 4: 00401a40 mfc0 k0,$8 /* 从 BadVaddr 获取转换失败的 VA */ 8: 18007b8f lw k1,24(k1) /* 读取 PGD 所在基地址 */ c: 82d51a00 srl k0,k0,0x16 /* VA 右移22位 */ 10: 80d01a00 sll k0,k0,0x2 14: 21d87a03 addu k1,k1,k0 18: 00201a40 mfc0 k0,$4 /* 读取 Context 值 */ 1c: 00007b8f lw k1,0(k1) 20: c2d01a00 srl k0,k0,0x3 24: f83f5a33 andi k0,k0,0x3ff8 28: 21d87a03 addu k1,k1,k0 2c: 00007a8f lw k0,0(k1) 30: 04007b8f lw k1,4(k1) 34: 82d11a00 srl k0,k0,0x6 38: 00109a40 mtc0 k0,$2 3c: 82d91b00 srl k1,k1,0x6 40: 00189b40 mtc0 k1,$3 44: 06000042 tlbwr 48: 18000042 eret
Context 之格式为:
63 23 22 4 3 0 ---------------------------------------------------- | | BadVPN2 | 0 | ----------------------------------------------------
22~4 共 19 位对应导致 TLB miss 之虚地址的31~13(VA[31:13]),因此如下指令
20: c2d01a00 srl k0,k0,0x3 24: f83f5a33 andi k0,k0,0x3ff8 /* 获取 VA[25:15] 共11位,低3位为0 */
其意图旨在获取 VA[25:15],并以 2^3=8 字节为单位索引PageTable,实际的索引范围为 2^12 = 4K ,这个与每页容纳的项数相一致(16K/4=4k)。实质上使用 VA[25:14]索引 PageTable,因每次取一对,故而VA[14] 不用。
VA[13:0] 页内位移,没有问题。
但使用 VA[31:22] 索引 PGD 就让人困惑了。32bit kernel 中定义 PGDIR_SHIFT 始终为 22 ,不管页大小为何,这个在4KB页大小下,是正好的,但在16KB下,由于使用 VA[25:15] 索引PageTable,因此用于索引 PGD 的域与索引 PT 的域重叠了4位(25~22)!
如果有2个虚址,VA1[22] = 1,VA2[23] = 1 ,当 TLB miss 时,当前内核的 tlb_miss_handler 是向2个不同的 PGD 入口找寻 PTE 的,尽管按照地址划分他们应该在同一个 PGD 入口才是。
关于地址的划分,应该从低位往高位切分才是,即:首先切取PAGE_SIZE所需位,然后根据PAGE_SIZE,确定索引 PT 的位数。如4KB,则为4K/4=1K, 10 位,16KB则为12 位,64KB 则为14位。
余下的高位作为索引PGD 的位数才是。因此应该是 6 | 12 | 14 才对。
32 bit 模式下, PGD 索引位与PT 索引位部分重叠并不会导致灾难性的后果,只是会造成核心分配给PT的页过多,浪费空间。
测了一下,改动后的 kernel,启动后 nr_page_table_pages 为 63 左右,而原有kernel 在101左右,多分配了 (101-63)*16KB 作为 PageTable。
另外我写了一个测试程序,使用mmap 映射一个40MB左右的文件在地址 0x2ac94000 附近,然后随机访问该文件中的数据 1亿次,新旧对比,原 kernel 要多分配 13*16KB 作为 PageTable(cat /proc/vmstat)。
后记:该问题已经提交到linux-mips mail list,最新内核的 PGDIR_SHIFT 定义已经修改为: (2 * PAGE_SHIFT + PTE_ORDER - PTE_T_LOG2)(原来写死为 22),现在可以根据 PAGE_ SHIFT 动态调整了。