本节主要分析 Cavium Octeon 上 TLB 异常相关的处理函数
1.2 TLB Refill 例外入口
非启动模式 (BEV=0) 下,MIPS64 R1上,64 位地址空间的 TLB Refill 入口在物理地址 0x80 处,其空间可以放一个 32 条指令的处理函数; 32 位地址空间的 TLB Refill 的 入口在物理地址 0x0 处,大小是 0x80,也可以放一个 32 条指令的处理函数;MIPS64 R2 默认与 R1 的入口一样,但其多了一个 cp0_ebase 寄存器,可以设置异常的基地址。
至于什么时候是 64 地址空间的访问什么时候又是 32 位地址空间访问,MIPS64 使用 cp0_status 的 KX, SX, UX 位来区分,以下情形都是 64 位地址模式,否则就是 32 位地址模式:
KX = 1 时,访问 Kernel Address Space
UX = 1 时,访问 Supervisor Address Space
UX = 1 时,访问 User Address Space
Loongson 2E 没有实现这个 KX/SX/UX 对地址空间的区分,因此其入口始终为 0xFFFF FFFF 8000 0000
Cavium Octeon 实现标准的 MIPS64 R2,但其通过 bootloader 设置 cp0_ebase 使其所有异常的基址变为 0xFFFF FFFF 8000 1000,从而 TLB Refill 用 0xFFFF FFFF 8000 1080 的入口
当访问一个需要经过 TLB 的虚拟地址 (mapped address) 时,如果在 TLB 中没找到相应的映射 (TLB Miss),同时原始 cp0_status[EXL] 为 0,才会进入这个 TLB Refill 入口;如果原来 cp0_status[EXL] 为 1,则表示 CPU 已经处于例外过程中,此时再发生找不到 TLB 项 (TLB Miss) 的情形,应该进入另外一个入口,以免 TLB Refill 嵌套。对于其它 EXL 为 1 的情形,不管在什么例外环境下出现 TLB Miss 的情形,都不会进入 TLB Refill 入口
比如访问 mapped address VA1,原来 EXL 为 0,无对应 TLB 项 (TLB Miss),进入这个入口,执行 TLB Refill 处理函数,注意,在 TLB Refill 处理函数中是不会去清除 EXL 位的,则此时如果再发生 mapped address 无对应 TLB 项 (TLB Miss)的情形,说明 TLB Refill 处理函数已经不能处理 VA1 对应 TLB 项的重填,得由另外功能更全面的处理函数善待之。更精确地,因为此时 EXL 已经为 1,EPC 不会被更新,还是原来访问 VA1 引起异常的地方,于是换个入口,进其他例外入口,最终由 TLB Invalid 异常的处理函数 handle_tlbl/handle_tlbs 来处理。
注意一下,TLB Invalid 异常的只在访问 VA1 时,对应 TLB 项的有效位 V 为 0 时才会触发;而 TLB Refill 是在 VA1 在 TLB 里没有对应项时触发,因此尽管 EXL 为 1 时,发生的 TLB Refill 异常同样进入 TLB Invalid 异常的处理函数,但还是能区分到底是 TLB Refill 还是 TLB Invalid,只要在处理函数里检查一下 TLB 里有没有 VA1 对应的项即可区分。
位于这些入口处 TLB Refill 处理函数,是由位于 arch/mips/mm/tlbex.c 中的 build_r4000_tlb_refill_handler() 在 kernel 初始化阶段动态生成,然后写到这些入口处的。至于为何要采取这种方式,主要是 MIPS 各种平台太多,如果根据用户的静态配置生成相应的 TLB Refill Handler,要照顾的情形太多,使用通常的条件编译方式已经不能满足要求了。关于这个动态生成过程后面的章节详细讨论。为分析这些动态生成的 Handler,我们可以在内核里把他们直接 dump 出来,具体操作过程,可以参考拙文:
1.2.1 Cavium Octeon CN38xx
CN38xx实现 49 bit 虚拟地址,其 segbit 为 49
1.2.2 EXL=0 时 TLB Refill 的处理
这个的处理函数,在 Cavium Octeon 上位于 0xFFFF FFFF 8000 1080 处,但往往这个处理函数都会超过 0x80 字节,所以动态生成过程就使用原来 32 位的入口空间再放一部分代码,在 64 位下,这个 32 位的 TLB Refill 入口空间是不用的:
0xFFFF FFFF 8000 1000:
c: 07410003 bgez k0,1c <0x1c> # badvaddr >= 0,即 badvaddr 低 40 位以上有货,最高位不为 1,即在 0x0 ~ 0x7FFF FFFF FFFF FFFF (xuseg or xsseg) 中时则跳转。一句话就是去处理位于 xuseg or xsseg 的异常地址
10: 3c1b817c lui k1,0x817c # 位于延迟槽,总是被执行。0xFFFF FFFF 817c 0000
#badvaddr低 40 位以上不为 0 且不位于 xuseg 或 xsseg 中,又走 TLB,则其应为位于 xkseg,范围为 0xC000 0000 0000 0000 ~ 0xFFFF FFFF FFFF FFFF。一句话就是去处理位于 xkseg 中的地址
14: 10000024 b a8 <0xa8> ----->@@@ k1 = 0xFFFF FFFF 817B E000, it's swapper_pg_dir, 他是内核使用的除模块以外的 PGD 基地址,内核模块使用
18: 277be000 addiu k1,k1,-8192 # 位于延迟槽,总是被执行。0xFFFF FFFF 817c 0000 - 8192 = 0xFFFF FFFF 817B E000
#badvaddr 低 40 位以上不为 0 且位于 0x0 ~ 0x7FFF FFFF FFFF FFFF ( xuseg or xsseg)
1c: 3c1b8110 lui k1,0x8110
20: 277b4e00 addiu k1,k1,19968 # k1 = 0xFFFF FFFF 8110 4E00, tlb_do_page_fault_0
24: 03600008 jr k1 # call tlb_do_page_fault_0
28: 00000000 nop
...
0xFFFF FFFF 8000 1080:
8c: 403a4000 dmfc0 k0,c0_badvaddr # 取转换失败的虚拟地址 VA,TLB Refill 异常下,cp0_badvaddr (MIPS64 下,64 位长)存放翻译失败的虚拟地址
90: 001ada3e dsrl32 k1,k0,0x8 # VA 逻辑右移 (32+8)位,置入 k1
94: 1760ffdd bnez k1,c <0xc> # k1 不为 0 则跳转到 0xc 处。即 badvaddr[63:41] 不为 0,即高于 40 位不为 0,就跳转。
对于这种高于 40 位还有有效位的虚拟地址,要不他位于 xkseg,要不就是 xuseg/xsseg 的地址。对于 xkseg 的地址,有有效的地址这是确定的(MIPS32 兼容地址空间内核是用的,高于 40 位的部分都不为 0)。
MIPS64 R2 下,segbit 默认为 40,但具体实现时,MIPS Core 的 segbit 可以超过 40,即实现的真正有效的虚拟地址可以超过 40 位
对只实现 40 位 segbit,出现位于 xuseg/xsseg 的地址且高于 40 位不为 0 的话,这个地址肯定就是用户瞎访问,其就不是一个有效地址,进 do_page_fault() 然后 segment fault 即可
但对实现超过 40 位的 segbit,像 Cavium Octeon CN38xx 实现 49 位,则需要特别对待,因为 4KB 的页大小下,Linux/MIPS 设计时规定整个虚拟地址的有效位是低 40 位,高于 40 的虚拟地址空间,内核没有为之建立页表,即没有映射!虽然你实现了 49 位虚拟地址,但我 Linux 还是用不了,最终也要进入 do_page_fault() 进行处理
# badvaddr 高于 40 位为 0
98: 403b2000 dmfc0 k1,c0_context # 取 cp0_context 值入 k1
9c: 7c1bb007 dins k1,zero,0x0,0x17 # zero[22:0] 替换 k1[22:0],k1 低 23 位置 0;
# 留意:cp0_context[22:4] 是硬件拷贝自 cp0_badvaddr[31:13]
# cp0_context[63:23] 是 PGD 的物理地址入口
a0: 377b0540 ori k1,k1,0x540 # 0x540 置入 k1 的低位
a4: 003bdafa dror k1,k1,0xb # drotr k1, k1, 11,即 k1 的低 11 位,和 k1 的高 53 位交换;k1 = k1[10:0] | k1[63:11]
上两条指令实际上是让 0b101 0100 0000 (0x540)或上 k1 的高 11 位,相当于 0xA800 0000 0000 0000 + Phy_PGD_ADDR,其是 PGD 的基地址
@@@ k1 中是 PGD 的基地址
a8: 001ad6fa dsrl k0,k0,0x1b # k0 右移 27 位
ac: 335a1ff8 andi k0,k0,0x1ff8 # 再保留 k0[12:3],实际保留的是 cp0_badvaddr[39:30] 正好是 PGD 的索引值,此时 k0 = cp0_badvaddr[39:30] << 3,64 位下 PGD 的项大小为 2^3 = 8 字节,因此其恰是索引 PGD 的指针
b0: 037ad82d daddu k1,k1,k0 # 获取 VA 对应的 PGD 项,即 VA 对应 PMD 基址的指针
b4: 403a4000 dmfc0 k0,c0_badvaddr # 取转换失败的虚拟地址 VA
b8: df7b0000 ld k1,0(k1) # 取对应 PMD 基址
bc: 001ad4ba dsrl k0,k0,0x12 # 右移 18 位
c0: 335a0ff8 andi k0,k0,0xff8 # 保留 k0[12:3],实际是 cp0_badvaddr[29:21] << 3,正好是 PMD 的索引指针,PMD 的项大小也是 2^3 = 8 字节
c4: 037ad82d daddu k1,k1,k0 # 索引 PMD,指向 VA 对应之 PT 基址
c8: 403aa000 dmfc0 k0,c0_xcontext # 取 cp0_xcontext 值,Cavium Octeon 上,BadVPN2 是 xcontext[39:4],置 badvaddr[48:13]
cc: df7b0000 ld k1,0(k1) # 取 PT 基址
d0: 335a0ff0 andi k0,k0,0xff0 # 保留 xcontext 的位 11:4,实际为 cp0_badvaddr[20:13] << (3 + 1),这实际为 PT 的索引值,因为 TLB Refill 总是一起填相邻的两项,因此这索引,实际是 VA 对应的偶数项索引
d4: 037ad82d daddu k1,k1,k0 # 索引 PT,指向 VA 对应之偶数项 PTE
d8: df7a0000 ld k0,0(k1) # 取 VA 对应偶数项的 PTE
dc: df7b0008 ld k1,8(k1) # 取 VA 对应奇数项的 PTE
e0: 001ad13a dsrl k0,k0,0x4 # 忽略 PTE 的低 4 位,那是 OS 用的,TLB 的项里没有这些位
e4: 001bd93a dsrl k1,k1,0x4
e8: 003ad0ba dror k0,k0,0x2 # drotr k0, k0, 0x2,k0 低两位和高 62 位交换,这个操作和上面的右移 4 位应该是合起来的忽略 PTE 的低 6 位,因为在 MIPS64 R2 上,写入数据到 entrylo0/1[63:62] 是始终为 0 的。
ec: 40ba1000 dmtc0 k0,c0_entrylo0 # 偶数项 PTE 入 entrylo0
f0: 003bd8ba dror k1,k1,0x2 # 同上面偶数项的分析
f4: 40bb1800 dmtc0 k1,c0_entrylo1 # 奇数项PTE 入 entrylo1
f8: 42000006 tlbwr # 把 entrylo0, entrylo1 随机写入 TLB 的一项
fc: dc1a8078 ld k0,-32648(zero)
100: 42000018 eret
...
1.2.3 EXL=1 时 TLB Refill 的处理
EXL=1 时,TLB Refill 进的是其他例外入口下 TLB Invalid 异常处理函数,TLB Invalid 分两类异常,一个是读数据引发的 (ExcCode=2, 处理函数 handle_tlbl),一个是写数据引发的 (ExcCode=3, 处理函数是 handle_tlbs)。
位于其他例外入口处的处理函数 except_vec3_generic 只是根据 ExcCode 跳转到具体例外的处理函数,TLB Load Invalid 进入 handle_tlbl;TLB Write Invalid 进入 handle_tlbs。这两个函数也是动态生成。
1.2.3.1 Cavium Octeon handle_tlbl 分析
handle_tlbl (位于代码段,不受向量空间限制。从其它例外入口进入):
c: 403a4000 dmfc0 k0,c0_badvaddr
10: 001ada3e dsrl32 k1,k0,0x8 # 失败地址右移 40 位
14: 17600032 bnez k1,e0 <0xe0> # 40 位外不为 0 则跳转。不管CPU 具体实现了多少位的虚拟地址,对内核来讲 40 位外不为 0 都是异常地址,需要小心处理
18: 403b2000 dmfc0 k1,c0_context
1c: 7c1bb007 dins k1,zero,0x0,0x17
20: 377b0540 ori k1,k1,0x540
24: 003bdafa dror k1,k1,0xb # 以上四条指令是为:k1 = 0xA800 0000 0000 0000 + PGD_Phy,即使用一个固定映射的虚址访问 PGD
28: 001ad6fa dsrl k0,k0,0x1b # k0 里放的是 c0_badvaddr
2c: 335a1ff8 andi k0,k0,0x1ff8 # PGD offset
30: 037ad82d daddu k1,k1,k0 # PMD base addr
34: 403a4000 dmfc0 k0,c0_badvaddr
38: df7b0000 ld k1,0(k1)
3c: 001ad4ba dsrl k0,k0,0x12
40: 335a0ff8 andi k0,k0,0xff8 # PMD offset
44: 037ad82d daddu k1,k1,k0 # get PT base addr
48: 403a4000 dmfc0 k0,c0_badvaddr
4c: df7b0000 ld k1,0(k1)
50: 001ad27a dsrl k0,k0,0x9
54: 335a0ff8 andi k0,k0,0xff8 # PT offset
58: 037ad82d daddu k1,k1,k0 # index PT
5c: d37a0000 lld k0,0(k1) # get PTE
60: 42000008 tlbp # 查找失效地址 VA1 对应之 TLB 项,有则将 index 写入 cp0_index,无则将 cp0_index[31] = 1;注意所有 TLB 相关的异常 (Refill, Invalid, Modified) 出现时,CPU 都会把失效地址的高位置入 EntryHi[VPN2]
64: 335a0001 andi k0,k0,0x1
68: 13400020 beqz k0,ec <0xec> # 判断 PTE 的最低位 _PAGE_PRESENT,为 0 表示该页未分配或未在内存,则跳转到 do_page_fault() 处理。此应是 EXL=1 TLB Miss 的在此的主要路径,进入这里的 TLB Miss,多数情形是页面尚未分配,在 TLB Miss 的处理中,索引 PT 失败。经此一路,在 do_page_fault() 中,系统会为其分配页面,填充页表项,往下就不会是页面尚未分配的情形
注意,tlbp 失败只能是失效地址对应页表没有分配或不在内存(交换出去);往下 tlbp 不可能是其失败的情形,如果失效地址有页表项(有效无效皆可),其也不会在 TLB Miss 中再次 TLB Miss
另外 tlbp 失败会将 cp0_index[31] = 1,这个值远远大于现代 MIPS 实现的 TLB 项数目,随后来一条 tlbr 的话,其结果未定义!
6c: d37a0000 lld k0,0(k1) # 该页已分配,get PTE again
70: 335a0080 andi k0,k0,0x80
74: 13400008 beqz k0,98 <0x98> # 判断 PTE 的 _PAGE_VALID 位,为 0 表示该页无效,跳转。失效地址对应的页表项无效,其就不是 EXL=1 TLB Miss 引起的了,应为 TLB Invalid 异常
78: 00000000 nop # 页表项有效
7c: 42000001 tlbr # 读 TLB,cp0_Index 对应 TLB 项存入 EntryHi, EntryLo0/1, PageMask
80: 337a0008 andi k0,k1,0x8 # k1 为页表项指针,此为判断奇偶页操作
84: 13400002 beqz k0,90 <0x90> # 奇数项则跳转
88: 403a1000 dmfc0 k0,c0_entrylo0 # 延迟槽,总是被执行。取奇数页在 TLB 中的值
8c: 403a1800 dmfc0 k0,c0_entrylo1 # 偶数项
90: 335a0002 andi k0,k0,0x2 # TLB 项之 V 位
94: 17400015 bnez k0,ec <0xec> # 不为 0(有效)则跳转到 do_page_fault() 处理。这是一个奇怪的现象,能找到失效地址对应的TLB项(非 EXL=1 TLB Refill),页表项目有效(非空 PTE),且对应的 TLB 项亦有效(非 TLB Invalid),应由 do_page_fault() 进一步甄别
# 失效地址对应之页表项无效 (V=0);或者对应页表项有效,却 TLB 项无效 (V=0)
98: d37a0000 lld k0,0(k1) # 再取 PTE
9c: 375a0084 ori k0,k0,0x84 # 置 _PAGE_VALID 和 _PAGE_WRITE
a0: f37a0000 scd k0,0(k1) # 更新页表项
a4: 1340ffed beqz k0,5c <0x5c> # k0=0 则事务操作失败,重做
a8: 00000000 nop
ac: 377b0008 ori k1,k1,0x8
b0: 3b7b0008 xori k1,k1,0x8 # 重置页表项指针,使其重新指向失效地址对应之奇数项
b4: df7a0000 ld k0,0(k1) # 以下操作取失效地址的页表项,填充 TLB,分析同 TLB Refill
b8: df7b0008 ld k1,8(k1)
bc: 001ad13a dsrl k0,k0,0x4
c0: 001bd93a dsrl k1,k1,0x4
c4: 003ad0ba dror k0,k0,0x2
c8: 40ba1000 dmtc0 k0,c0_entrylo0
cc: 003bd8ba dror k1,k1,0x2
d0: 40bb1800 dmtc0 k1,c0_entrylo1
d4: 42000002 tlbwi # 写入 Index 指定的 TLB 入口中
d8: dc1a8078 ld k0,-32648(zero)
dc: 42000018 eret
# 40位外有货,地址异常 ,需要小心对待
e0: 3c1b817c lui k1,0x817c
e4: 1000ffd0 b 28 <0x28> # k1 = 0xFFFF FFFF 817B E000, it's swapper_pg_dir
e8: 277be000 addiu k1,k1,-8192
ec: 08441380 j 1104e00 <0x1104e00> # jump to tlb_do_page_fault_0, call do_page_fault(not write)
f0: 00000000 nop
...
1.2.3.2Cavium Octeon handle_tlbs 分析
handle_tlbs (位于代码段,不受向量空间限制):
c: 403a4000 dmfc0 k0,c0_badvaddr
10: 001ada3e dsrl32 k1,k0,0x8
14: 17600028 bnez k1,b8 <0xb8>
18: 403b2000 dmfc0 k1,c0_context
1c: 7c1bb007 dins k1,zero,0x0,0x17
20: 377b0540 ori k1,k1,0x540
24: 003bdafa dror k1,k1,0xb
28: 001ad6fa dsrl k0,k0,0x1b
2c: 335a1ff8 andi k0,k0,0x1ff8
30: 037ad82d daddu k1,k1,k0
34: 403a4000 dmfc0 k0,c0_badvaddr
38: df7b0000 ld k1,0(k1)
3c: 001ad4ba dsrl k0,k0,0x12
40: 335a0ff8 andi k0,k0,0xff8
44: 037ad82d daddu k1,k1,k0
48: 403a4000 dmfc0 k0,c0_badvaddr
4c: df7b0000 ld k1,0(k1)
50: 001ad27a dsrl k0,k0,0x9
54: 335a0ff8 andi k0,k0,0xff8
58: 037ad82d daddu k1,k1,k0
5c: d37a0000 lld k0,0(k1)
60: 42000008 tlbp
64: 335a0003 andi k0,k0,0x3
68: 3b5a0003 xori k0,k0,0x3
6c: 17400015 bnez k0,c4 <0xc4>
# 失效地址对应之页表项无效 (V=0);或者对应页表项有效,却 TLB 项无效 (V=0)
70: d37a0000 lld k0,0(k1)
74: 375a018c ori k0,k0,0x18c # 置 _PAGE_VALID, _PAGE_DIRTY, _PAGE_WRITE, _PAGE_ACCESSED
78: f37a0000 scd k0,0(k1)
7c: 1340fff7 beqz k0,5c <0x5c> # 事务失败,重做
80: 00000000 nop
84: 377b0008 ori k1,k1,0x8
88: 3b7b0008 xori k1,k1,0x8
8c: df7a0000 ld k0,0(k1)
90: df7b0008 ld k1,8(k1)
94: 001ad13a dsrl k0,k0,0x4
98: 001bd93a dsrl k1,k1,0x4
9c: 003ad0ba dror k0,k0,0x2
a0: 40ba1000 dmtc0 k0,c0_entrylo0
a4: 003bd8ba dror k1,k1,0x2
a8: 40bb1800 dmtc0 k1,c0_entrylo1
ac: 42000002 tlbwi
b0: dc1a8078 ld k0,-32648(zero)
b4: 42000018 eret
b8: 3c1b817c lui k1,0x817c
bc: 1000ffda b 28 <0x28> # k1 = 0xFFFF FFFF 817B E000, it's swapper_pg_dir
c0: 277be000 addiu k1,k1,-8192
c4: 084413c8 j 1104f20 <0x1104f20> # jump to tlb_do_page_fault_1, call do_page_fault(write)
c8: 00000000 nop
cc: 00000000 nop
除标出的外,其它与 handle_tlbl 同