文章目录
- [1. 前言](#1. 前言)
- [2. 页表管理](#2. 页表管理)
-
- [2.1 两级分页](#2.1 两级分页)
- [2.2 三级分页](#2.2 三级分页)
- [3. 参考资料](#3. 参考资料)
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 页表管理
本文以 Linux 4.14.x 在 ARMv7 架构下,分别对两级和三级分页进行讨论。
在讨论之前,先假定 4G 虚拟地址空间按 1G:3G 划分,中断向量位于 4G 虚拟地址最高地址位置(Vector High),一如下图:

2.1 两级分页
在没有启用 LPAE(Large Physical Address Extension) 功能的情形下,内核最多使用两级分页进行寻址。注意,这里的最多表示:有些情形下可以一级分页,两级分页是上限。
先上图,Linux 下两级分页寻址过程如下:


仅用到 TTBR0 来存储第一级页表物理基址,用来进行 page table walk,其值初始在 __enable_mmu 中设置,后续会随着进程切换变化;而 TTBR1 仅用来备份内核页表 swapper_pg_dir 的物理地址,其值在 proc-v7-2level.S:v7_ttb_setup 中设置,在切换页表期间临时拷贝覆盖 TTBR0,避免非法的地址访问。
来简单看一下上图形成的过程。先看 TTBR0,TTBR1 初始化过程:
c
__v7_ca7mp_setup:
...
__errata_finish:
mov r10, #0
...
#ifdef CONFIG_MMU
...
/*
* r10 = 0
* r8 = 内核页表 swapper_pg_dir 物理基址
*/
v7_ttb_setup r10, r4, r5, r8, r3 @ TTBCR, TTBRx setup
...
#endif
c
.macro v7_ttb_setup, zero, ttbr0l, ttbr0h, ttbr1, tmp
// TTBCR = 0
mcr p15, 0, \zero, c2, c0, 2 @ TTB control register
ALT_SMP(orr \ttbr0l, \ttbr0l, #TTB_FLAGS_SMP) // \ttbr0l |= TTB_FLAGS_SMP (r4 |= TTB_FLAGS_SMP)
...
ALT_SMP(orr \ttbr1, \ttbr1, #TTB_FLAGS_SMP) // \ttbr1 |= TTB_FLAGS_SMP (r8 |= TTB_FLAGS_SMP)
...
// TTBR1 = swapper_pg_dir 页表物理地址
mcr p15, 0, \ttbr1, c2, c0, 1 @ load TTB1
.endm
BOOT CPU 上 TTBCR,TTBR0,TTBR1 从初始化如上分析。注意,MMU 是每 CPU 的,那自然 TTBCR,TTBR0,TTBR1 也是每 CPU 的。在非 BOOT CPU 上,除了 TTBR0 初始为 idmap_pgd 的物理地址外,TTBR1 仍然初始化为 swapper_pg_dir 页表物理地址,这里就不做展开了。
再简单看下内核 lowmem 的映射建立(包括内核镜像区间):
c
start_kernel()
setup_arch()
paging_init()
map_lowmem()
map_lowmem() 建立了 lowmem 的页表映射,内核区间用 1MB section 大小进行映射,只使用了一级映射(正如前面提到的)。
接下来看下进程第一级页表的创建:
c
copy_process()
copy_mm()
dup_mm()
mm_init()
mm_alloc_pgd()
pgd_alloc()
/*
* need to get a 16k page for level 1
*/
pgd_t *pgd_alloc(struct mm_struct *mm)
{
pgd_t *new_pgd, *init_pgd;
pud_t *new_pud, *init_pud;
pmd_t *new_pmd, *init_pmd;
pte_t *new_pte, *init_pte;
/* 分配页目录表(第一级页表)空间(表项为未配置状态) */
new_pgd = __pgd_alloc();
if (!new_pgd)
goto no_pgd;
/* 页目录表(第一级页表)用户空间部分的所有表项清 0 */
memset(new_pgd, 0, USER_PTRS_PER_PGD * sizeof(pgd_t));
/*
* Copy over the kernel and IO PGD entries
*/
/* 拷贝 内核空间页目录表映射表项 到新分配页目录表的内核空间映射部分 */
init_pgd = pgd_offset_k(0);
memcpy(new_pgd + USER_PTRS_PER_PGD, init_pgd + USER_PTRS_PER_PGD,
(PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t));
/* 清除新页目录表空间的 cache */
clean_dcache_area(new_pgd, PTRS_PER_PGD * sizeof(pgd_t));
return new_pgd;
}
最后看一下进程切换时的页表切换:
c
__schedule()
context_switch()
switch_mm_irqs_off()
switch_mm()
check_and_switch_context()
void check_and_switch_context(struct mm_struct *mm, struct task_struct *tsk)
{
unsigned long flags;
unsigned int cpu = smp_processor_id();
u64 asid;
...
/* TTBR0 = TTBR1 = swapper_pg_dir 物理地址 */
cpu_set_reserved_ttbr0();
...
switch_mm_fastpath:
/* 切换到目标进程 @tsk 的页表: TTBR0 = 标进程 @tsk 的页表 */
cpu_switch_mm(mm->pgd, mm);
}
#define cpu_switch_mm(pgd,mm) cpu_do_switch_mm(virt_to_phys(pgd),mm) // cpu_v7_switch_mm
2.2 三级分页
在启用了 LPAE(Large Physical Address Extension) 功能的情形下,内核最多使用三级分页进行寻址。
照样先上图,Linux 下三级分页寻址过程如下:


Linux 下三级分页寻址过程同时使用 TTBR0 和 TTBR1 进行 page table walk:TTBR1 指向内核空间 1GB 页表物理基址,且其值始终不会变化;TTBR0 用来存储每进程第一级页表物理基址。
来简单看一下上图形成的过程。先看 TTBR0,TTBR1 初始化过程,和二级分页基本一致,TTBCR,TTBR1 设置是通过 proc-v7-3level.S:v7_ttb_setup 设置,不过 TTBR1 并不是指向内核页表 swapper_pg_dir 的物理首地址,而是指向了第二级 PMD 页表的最后一个 PMD 页表的物理首地址,为啥这样?来看看。三级分页使用 Long-descriptor 格式,我们讨论的场景是 1G:3G 划分,所以 swapper_pg_dir 包含的最后一个 PMD 页表刚好映射内核的 1G 空间。那为什么不让 TTBR1 指向 swapper_pg_dir 的物理首地址呢?这和硬件的实现有关,看一下 ARMv7 架构手册 DDI0406C_d_armv7ar_arm.pdf 中的相关原文:

简单来说,在启用了 LPAE(Large Physical Address Extension) 功能的情形下,寻址内核 [0xC0000000, 0xFFFFFFFF] 空间时,从第二级 PMD 页表开始 page table walk 的过程,这就是 TTBR1 指向最后一个第二级 PMD 页表的原因。寻址时 VA(虚拟地址) 的 PGD Index 部分可以不用关心了,因为确定寻址的内核空间。
Linux 下三级分页的进程第一级页表的创建和页表切换过程和二级分页基本相同,这里就不再赘述了。
3. 参考资料
1\] DDI0406C_d_armv7ar_arm.pdf