准备

内核版本: 4.20.1

在Linux中,所有进程共享内核虚拟地址空间,每个进程都有独立的虚拟地址空间,即逻辑地址,数据的寻址是将逻辑地址经过MMU硬件设备转换为物理地址物理地址即为物理内存的实际地址。在逻辑地址物理地址之间还存在一个线性地址,这是体系结构中的分段机制设计,为了简化,Linux中的逻辑地址线性地址地址值相同的。

Linux的分页机制用来实现以页(Page)为单位的虚拟内存系统,而具体的寻址方法则是逻辑地址经过分页机制的处理转换为物理地址

硬件基础

Linux的分页机制离不开硬件的支持,而其中最重要的部分就是下面CPU中的几个控制寄存器:

  • CR0:11个标志位,每个bit为代表不同的意义,具体详情:wiki
  • CR1:Intel预留的控制器,暂无任何作用。
  • CR2:页故障线性地址寄存器,保存最后一次出现页故障的地址。
  • CR3:页目录基址寄存器,保存页目录表的物理地址。

分页机制

CR0的第31位假如为1,即开启分页机制:

If 1, enable paging and use the CR3 register, else disable paging.

四级页表结构:

我们以最常用的x86体系结构为例,它采取的分级策略: arch/x86/Kconfig

1
2
3
4
5
6
config PGTABLE_LEVELS
int
default 5 if X86_5LEVEL
default 4 if X86_64
default 3 if X86_PAE
default 2

分别有2,3,4,5级结构,而最常用的x86_64是4级页表结构,我们后面的介绍全部以x86_64的4级页表结构为例。

四种类型的页表:

  • 页全局目录(Page Global Directory)
  • 页上层目录(Page Upper Directory)
  • 页中间目录(Page Middle Derectory)
  • 页表(Page Table)

下图是x86_64的四级页表模型

linux_four_level

其中Page Global Directory包含Page Upper Directory的地址,而Page Middle Derectory又包括Page Middle Derectory的地址,Page Middle Derectory包含Page Table的地址,其中每个Page Table对应一个Page Frame即物理页. 因此一个线性地址被分为5个部分。

四种类型的页表数据结构

pgd_tpud_tpmd_tpte_t分别是四种页面的数据结构,定义如下:

1
2
3
4
typedef struct { pgdval_t pgd; } pgd_t;
typedef struct { pudval_t pud; } pud_t;
typedef struct { pmdval_t pmd; } pmd_t;
typedef struct { pteval_t pte; } pte_t;

其中pgdval_tpudval_tpmdval_tpteval_t的类型全部为unsigned long.

分页机制寻址过程

查询页表,把虚拟地址转换成物理地址的过程如下:

每个进程都有独立的页表, 进程的mm_struct的成员pgd指向页全局目录.

  • pgd指向的页全局目录和页全局目录索引得到页全局目录项.
  • 由页上层目录索引得到页上册目录项.
  • 由页中间目录索引得到页中间目录项.
  • 由页表索引得到页表项.
  • 由页内偏移得到具体的物理页.

通过逻辑地址查找页表Page Table

下面是基于x86体系结构,通过逻辑地址address查找Page Table指针的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static int __follow_pte_pmd(struct mm_struct *mm, unsigned long address,
unsigned long *start, unsigned long *end,
pte_t **ptepp, pmd_t **pmdpp, spinlock_t **ptlp)
{
pgd_t *pgd;
p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
pte_t *ptep;

/* 进程的Page Global Directory基指针存放在mm->pgd */
pgd = pgd_offset(mm, address); /* 返回address指向该进程的Page Global Directory指针 */
if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
goto out;

p4d = p4d_offset(pgd, address); /* 在4级页面机制中,不做任何操作,直接返回pgd */
if (p4d_none(*p4d) || unlikely(p4d_bad(*p4d)))
goto out;

pud = pud_offset(p4d, address); /* 返回address指向该进程的Page Upper Directory指针 */
if (pud_none(*pud) || unlikely(pud_bad(*pud)))
goto out;

pmd = pmd_offset(pud, address); /* 返回address指向该进程的Page Middle Derectory指针 */
/* ... */
if (pmd_none(*pmd) || unlikely(pmd_bad(*pmd)))
goto out;

if (start && end) {
*start = address & PAGE_MASK;
*end = *start + PAGE_SIZE;
mmu_notifier_invalidate_range_start(mm, *start, *end);
}
ptep = pte_offset_map_lock(mm, pmd, address, ptlp); /* 返回address指向该进程的Page Table指针 */
/* 判断Page Table是否保留在物理内存之中 */
if (!pte_present(*ptep))
goto unlock;
*ptepp = ptep;
*ptepp = ptep;
return 0;
unlock:
pte_unmap_unlock(ptep, *ptlp);
if (start && end)
mmu_notifier_invalidate_range_end(mm, *start, *end);
out:
return -EINVAL;
}
  • 假如返回的目录项不存在,pgd_none()pud_nonepmd_none 返回1.
  • pte_present 宏的值为 1 或 0,表示 _PAGE_PRESENT标志位。如果页表项不为 0,但标志位pte_present()的值为 0,则表示映射已经建立,但所映射的物理页面不在内存。

Page Table转为物理地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int follow_phys(struct vm_area_struct *vma,
unsigned long address, unsigned int flags,
unsigned long *prot, resource_size_t *phys)
{
/* ... */
if (follow_pte(vma->vm_mm, address, &ptep, &ptl))
goto out;
pte = *ptep;
/* ... */
*phys = (resource_size_t)pte_pfn(pte) << PAGE_SHIFT; /* Page Table转化为物理地址 */

ret = 0;
unlock:
pte_unmap_unlock(ptep, ptl);
out:
return ret;
}

页表是一个元素为页表条目(Page Table Entry, PTE)的集合,每个虚拟页在页表中一个固定偏移量的位置上都有一个PTE. 所以两个虚拟地址是存在可能映射到同一个物理地址的.

更新CR3寄存器

当进程切换时,Linux内核会更新CR3寄存器的值:

1
2
/* Force ASID 0 and force a TLB flush. */
write_cr3(build_cr3(mm->pgd, 0));

总结

我们通过内核代码与硬件机制结合的方式介绍了Linux内核的分页机制,根据分页机制的寻址过程,我们可以更直观的了解如何通过4级的分页映射从逻辑地址得到物理地址