# 页表和缺页中断

虚拟内存空间的物理根本可以说就是页表了，没有页表虚拟地址空间是无法幻化出让人眼花缭乱的变化。

## 页表的层次

当然，页表本身已经够让人眼花缭乱了。那就先让我们来看一下页表的样子先。

```
            47               39 38              30 29              21 20              12 11                  0
            +------------------+------------------+------------------+------------------+---------------------+
            |PML4              |Page Directory Ptr|Page Directory    |Page Table        |Offset               |
            +------------------+------------------+------------------+------------------+---------------------+
                   |                    |                      |                     |
                   |                    |                      |                     |
                   |                    |                      |                     |
                   |                    |                      |                     |
  pgd_index(addr)  |    pud_index(addr) |      pmd_index(addr) |     pte_index(addr) |      +----------+
                   |                    |                      |                     |      |          |
                   |                    |                      |                     |      |          |
                   |                    |                      |                     |      +----------+
                   |                    |                      |    pte_offset_map() +----> |pte       |
                   |                    |                      |                            +----------+
                   |                    |                      |     +----------+           |          |
                   |                    |                      |     |          |           |          |
                   |                    |                      |     |          |           |          |
                   |                    |                      |pmdp +----------+           |          |
                   |                    |         pmd_offset() +---->| *pmdp    |---------->+----------+
                   |                    |                            +----------+ pmd_page_vaddr(*pmdp)
                   |                    |                            |          |
                   |                    |                            |          |
                   |                    |                            |          |
                   |                    |     +----------+           |          |
                   |                    |     |          |           |          |
                   |                    |pudp +----------+           |          |
                   |       pud_offset() +---->| *pudp    |---------->+----------+
                   |                          +----------+ pud_pgtable(*pudp)
                   |     +----------+         |          |
                   |     |          |         |          |
                   |pgdp +----------+         |          |
pgd_offset(mm,addr)+---->| *pgdp    |-------->+----------+
                         +----------+
                         |          |
                         |          |
                         |          |
           mm->pgd --->  +----------+

```

其中最上面一行中出现的名词，如PML4，是在intel手册上的。而下方的xx\_index/xx\_offset是在内核代码中对应使用的名字。

当然上面这个图例已经有点过时了，这个是四层页表的情况，现在已经有五层也表了。

比如在[page table](https://docs.kernel.org/mm/page_tables.html)中可以看到五层是这样的。

```
  +-----+
  | PGD |
  +-----+
     |
     |   +-----+
     +-->| P4D |
         +-----+
            |
            |   +-----+
            +-->| PUD |
                +-----+
                   |
                   |   +-----+
                   +-->| PMD |
                       +-----+
                          |
                          |   +-----+
                          +-->| PTE |
                              +-----+
```

### 页表层级常用helper

首先我们从上面图中看到内核中对页表每一层都起了自己的名字：

* pgd
* p4d
* pud
* pmd
* pte

在操作对应层级时，也有对应的helper帮助我们获取对应的信息。先按照功能我来分个类：

* 遍历型：用于遍历页表
* 访问型：用于访问页表项内容
* 分配型：用于分配页表

接下来就按照这几个大类来看看内核中常用的helper。 其中xxx代表了 pgd/pud/pmd/pte。

遍历型：

* xxx\_index(address): 获取对应层级偏移量，用来计算下级页表地址
* xxx\_offset(xxx\_t \*, addr): 第一个参数指向的页表起始地址 + xxx\_index()，也就是往下一层级页表走一层。比如pmd\_offset()，传入参数是pud\_t \*，得到的是pmd\_t \*。
* pte\_offset\_map(pmd\_t \*, addr): 没有pte\_offset(), 还有一个pte\_offset\_kernel(pmd\_t \*, addr)

其中xxx\_offset()值的注意的是，除了pgd\_offset()，其余变体都是从上一层级的页表项中获取下一层级的页表虚拟地址，然后加上xxx\_index()得到的。

另外，从含义上来说pte\_offset\_kernel()和其他的xxx\_offset是一样的。pte\_offset\_map()是在pte\_offset\_kernel()上又做了一些数据校验。

访问型：

* xxxp\_get(xxx\_t \*): 获得当前页表项(xxx\_t \*)的内容，读出指针指向的地址里的内容。用作下面一类helper的入参。
* xxx\_val(xxx\_t ): 获取xxx\_t对应的值。注意这个和xxxp\_get()的区别。xxx\_val()才会真正去读出xxx\_t这个类型中的值。
* xxx\_flags(xxx\_t ): 在xxx\_val()的基础上，取出页表项相关的属性位
* xxx\_none(xxx\_t ): 判断xxx\_t对应这个entry是否为空，空说明需要分配下级页表了
* xxx\_present(xxx\_t ): 判断xxx\_t对应这个entry是否存在，其实是看下一层页表是否存在
* xxx\_pfn(xxx\_t ): 获取页表项指向的页的pfn，在xxx\_val()基础上去掉不相关的bit，再右移PAGE\_SHIFT
* xxx\_page(xxx\_t ): 获取页表项指向的页的page结构, 将xxx\_pfn()转换为page struct
* pmd\_page\_vaddr(xxx\_t ): 获取页表项指向的页的虚拟地址。PS:这个和获取xxx\_page()的过程很像，前者是拿到pfn后转换为page struct，后者是将pfn转换为虚拟地址。
* xxx\_pgtable(xxx\_t ): 部分有定义，实际就是xxx\_pfn()转换成虚拟地址，再做一个类型转换。

其中只有xxxp\_get()的入参是指针，其余都不是。通常理解xxxp\_get()的返回值，会用作后续的入参。但实际使用中常常见到pgd\_none(\*pgd)这样的情况。

其中pmd\_pgtable()是个特例，在大多数平台下他默认定义为pmd\_page()。平台可以在asm/pgtable.h中覆盖这个定义。难怪单独有一个pmd\_page\_vaddr的定义。

分配型：

* xxx\_alloc(): 如果已经有页表，返回结果同xxx\_offset()；否则分配xxx对应层级的页表
* xxx\_populate(): 安装页表，把下一层新分配的页表地址填到xxx表示的这一层。这是xxx\_alloc()中的一部分
* xxx\_install(): 和xxx\_populate()差不多，多了一个判断，最后调用xxx\_populate()

### 图示

个人觉得，内核中这些helper写得不是很统一，有些含义不是特别清楚，容易混淆。用图的形式可能更容易理解。

```
                            PMD
   pud_pgtable(pudp) / ---->+------------------+<---- pmd_pgtable_page(pmdp)
     pud_val(pud)        ^  |                  |         pmd_pgtable_page()返回的是pmdp
                         |  |                  |              所在的PMD这个页的struct page
                         |  |                  |
        pmd_index(addr)  |  |                  |
                         |  |                  |
                         v  +------------------+                        PTE
   pmdp                 --->|                  | pmd_page_vaddr(pmdp)-->+------------------+<--- pmd_page(pmd)
    = pmd_offset(pudp, addr)+------------------+   pmd_val(pmd)      ^  |                  |        pmd_page()返回的是
                            |                  |                     |  |                  |           pmdp中保存的页表的struct page
                            +------------------+                     |  |                  |
                                                    pte_index(addr)  |  |                  |
   pud_pgtable() / pmd_offset() 返回的是虚拟地址                     |  |                  |
   pud_val() 返回的是物理地址                                        v  +------------------+
                                                 ptep                -->|                  |
                                                  = pte_offset_kernel() +------------------+
                                                                        |                  |
                                                                        +------------------+

                                                 pmd_page_vaddr() / pte_offset_kernel()
                                                     返回的是虚拟地址
                                                 pmd_val()返回的是物理地址

```

## 页表的填写

那这张表怎么填写呢？当然途径不止一条，不过最重要的就是**缺页中断**了。

总的来讲就是按照虚拟地址来遍历整个页表，根据不同PTE的状态做不同的处理。

### 缺页中断

首先是架构相关的中断处理程序代码：

```
exc_page_fault
  handle_page_fault
    do_kern_addr_fault
    do_user_addr_fault
      vma = lock_vma_under_rcu()              <--- 锁住对应vma
      handle_mm_fault(FAULT_FLAG_VMA_LOCK)    <--- 架构无关代码
      vma_end_read()

      ... or

      vma = lock_mm_and_find_vma()            <--- 锁住mmap_lock
      handle_mm_fault()                       <--- 架构无关代码
      mmap_read_unlock()
```

### 架构无关代码

然后就是架构无关的缺页处理代码：

```
handle_mm_fault(vma, address, flags, regs)
    if (is_vm_hugetlb_page(vma))
        hugetlb_fault()                <--- hugetlb处理
    else
        __handle_mm_fault
            // pud/pmd level
            // pte level
            handle_pte_fault           <--- 包括PTE这层页表，和做后的page
                // empty pte
                do_pte_missing
                    do_anonymous_page
                    do_fault
                // other1
                do_swap_page
                do_numa_page
                // other2
                do_wp_page
                pte_mkdirty
                pte_mkyoung
```

### 匿名页填写

页表按照映射对象来分主要是两种：

* 匿名页表
* 文件页表

这里我们先看匿名页表 -- do\_anonymous\_page。

```
do_anonymous_page()
  pte_alloc()                        <--- 这里会分配pte这一层页表，如果没有的话
  // 如果不是写，用zero-page
  entry = pte_mkspecial(pfn_pte(my_zero_pfn(), ..));

  // 准备匿名映射
  ret = vmf_anon_prepare()
    ret = __vmf_anon_prepare(vmf)
      __anon_vma_prepare(vma)        <--- 分配或者查找相邻可用vma->anon_vma
    return ret
  // 准备真正需要映射的内存
  folio = alloc_anon_folio(vmf);
    // THP or not
    folio_prealloc(, vma, vmf->address, true)
      vma_alloc_folio(GFP_HIGHUSER_MOVABLE, )    <-- 指定了可用的zone

  // 最后设置到pte页表中
  set_ptes()
```

## 页表上的锁

页表是一个公共资源，当发生缺页中断时大家可能同时访问页表并进行操作。

最开始的时候，每个进程只有一把大锁，mm->page\_table\_lock。

为了序列化对页表的访问，内核中提供了各种层次的锁来保护。 是的，就是我们在前面看到的页表层级，内核为不同的层级定义了不同的锁。

* pud\_lockptr(mm, pudp)
* pmd\_lockptr(mm, pmdp)
* pte\_lockptr(mm, pmdp)
* ptep\_lockptr(mm, ptep)

目前pud\_lockptr()是个冒牌货，因为还没有发现有扩展性的问题。

其他的锁搜保存在特定的页表页中

* pmd\_lockptr(): pmd\_pgtable\_page(pmd)，这个是PMD level页表
* pte\_lockptr(): pmd\_page(\*pmdp)
* ptep\_lockptr(): virt\_to\_page(ptep)

直观一些，我慢来看看这几个锁在那里。

```
                       +------pmd_lockptr(mm, pmdp) / pmd_pgtable_page(pmdp)
                      /
      PMD            v
      +--------------+
      |              |                        +--pte_lockptr(mm, pmdp) / pmd_page(pmd)
      |              |                       /   ptep_lockptr()
      |              |        PTE           v
      |              | -----> +--------------+
      |              |        |              |
      +--------------+        |              |
                              |              |
                              +--------------+
```

也就是pmd\_lockptr()存放在PMD的页表里，而pte\_lockptr()和ptep\_lockptr()都在PTE页表里。

### pmd\_lockptr(mm, pmdp)

```
static inline struct page *pmd_pgtable_page(pmd_t *pmd)
{
	unsigned long mask = ~(PTRS_PER_PMD * sizeof(pmd_t) - 1);
	return virt_to_page((void *)((unsigned long) pmd & mask));
}

static inline struct ptdesc *pmd_ptdesc(pmd_t *pmd)
{
	return page_ptdesc(pmd_pgtable_page(pmd));
}

static inline spinlock_t *pmd_lockptr(struct mm_struct *mm, pmd_t *pmd)
{
	return ptlock_ptr(pmd_ptdesc(pmd));
}
```

这个代码很有意思，先是拿到pmd的指针，这个指针是PMD层级中的512项(PTRS\_PER\_PMD)其中之一。然后和一个mask与操作。这个mask正好是512项pmd指针对齐。

所以最后拿到的锁，是在PMD这个层级页表上的。

### pte\_lockptr(mm, pmdp)

```
static inline unsigned long pmd_pfn(pmd_t pmd)
{
	phys_addr_t pfn = pmd_val(pmd);
	pfn ^= protnone_mask(pfn);
	return (pfn & pmd_pfn_mask(pmd)) >> PAGE_SHIFT;
}

#define pmd_page(pmd)	pfn_to_page(pmd_pfn(pmd))

static inline spinlock_t *pte_lockptr(struct mm_struct *mm, pmd_t *pmd)
{
	return ptlock_ptr(page_ptdesc(pmd_page(*pmd)));
}
```

这恶展开和上面的pmd\_lockptr()很像，传进去的参数都是一样的。就是差在这里是用pmd\_page(\*pmd)去找的页表。这个页表是pmd中保存的值所指向的页表。

所以最后拿到的锁，是在PTE这个层级页表上的。

### ptep\_lockptr(mm, ptep)

```
#define virt_to_page(kaddr)	pfn_to_page(__pa(kaddr) >> PAGE_SHIFT)

static inline struct ptdesc *virt_to_ptdesc(const void *x)
{
	return page_ptdesc(virt_to_page(x));
}

static inline spinlock_t *ptep_lockptr(struct mm_struct *mm, pte_t *pte)
{
	BUILD_BUG_ON(IS_ENABLED(CONFIG_HIGHPTE));
	BUILD_BUG_ON(MAX_PTRS_PER_PTE * sizeof(pte_t) > PAGE_SIZE);
	return ptlock_ptr(virt_to_ptdesc(pte));
}

```

这里是通过找到pte这个虚拟地址所在page来找到锁的。其实这个和pmd\_lockptr()的方法是一样的。为什么这两个不写成一样呢？

## 参考文档

[内核文档 -- page tables](https://docs.kernel.org/mm/page_tables.html)
