取出当前(假设0) level 的 index 1.vaddr 高 16 位抹 0 = vaddr_rel,用 vaddr - kernal_base,kernel_base 是 kernel 的基地址,因为整个内存空间是一张页表,所以 VA 的高 16 位必然相同 VA = - L0 index - L1 index - LN index - 页内偏移 2.然后 index = vaddr_rel >> index_shift VA = - L0 index - 最后剩下 L0 index
算出页内偏移 1.vaddr 高 16 位抹 0,用 vaddr - kernal_base,kernel_base 是 kernel 的基地址,因为整个内存空间是一张页表,所以 VA 的高 16 位必然相同 VA = - L0 index - L1 index - LN index - 页内偏移 2.当前等级页表所能管理的内存块大小 block_size = 1 << index_shift; 3.vaddr 当前 index_shift - 64 位抹 0 = vaddr_rem,通过 vaddr & (block_size - 1) VA = - - L1 index - LN index - 页内偏移 4.block_size - vaddr_rem = 页内偏移,还需要和剩下需要映射的内存大小 size 比一下大小 MIN(size, block_size - vaddr_rem); VA = - - 页内偏移
判断是否是最后一级页表,是则根据传入的页表属性填充最后一级页表项目,否则递归执行,下一级页表 index 偏移 = index_shift - (page_size_shift - 3)
代码:
这个方法等于是模拟了 MMU 的工作: 逻辑与上面类似,就不重复了
简单来说也是去每一级页表 index -> 由 index 一级一级查找下一个页表,最后找到目标内存页面 PA = 目标页基地址 + 页内偏移
从本文开始可以知道,MMU 会将近期查到的页地址存储在 Cache 中,叫做 TLB 的东西。下一次查询就可以先查 TLB 以加快寻址速度,不然但的话一次寻址 MMU 将多次访问内存。 当 CPU 运行的代码从用户进程 A 切换到用户进程 B 时,由于 A 和 B 的进程地址空间一般是不一样的,所以当前的 TLB 对于 B 进程完全没有意义,而且可能导致 B 错误的访问到 A 的进程空间。 所以,我们就需要将用户空间的 TLB 完全清空,而这样一来,当 CPU 再次切换到进程 A 时,MMU 就需要重新从内存中查页表,内存压力将越来越大。。。
幸好 ARM64 想到了这一点,还记得 VA 的高 16 位么?其中就承载了一个叫 ASID 的信息,每条 VA 都会携带上对应进程的 ASID,这样的话 MMU 就有能力区别 TLB 中的 VA 是哪个进程的。 这样的话,进程切换将完全没有必要 Flush TLB,而是让 Cache 自然淘汰。 当然,当进程死亡的时候,还是可以主动 Flush TLB 的,而且因为 ASID,CPU 也有能力单独抹掉某个进程空间的 TLB。