MIPS Linux 内核页表隐性 Bug 分析与修复

来自Jack's Lab
跳转到: 导航, 搜索

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 动态调整了。









个人工具
名字空间

变换
操作
导航
工具箱