> For the complete documentation index, see [llms.txt](https://richardweiyang-2.gitbook.io/kernel-exploring/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://richardweiyang-2.gitbook.io/kernel-exploring/nei-cun-guan-li/00-index/03-page_table_fault.md).

# 页表和缺页中断

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

## 页表的层次

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

```
            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)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://richardweiyang-2.gitbook.io/kernel-exploring/nei-cun-guan-li/00-index/03-page_table_fault.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
