Fork me on GitHub
0%

页中断机制详解

基础

概念

“页中断”(Page Fault)是指在虚拟内存系统中,当程序试图访问当前未分配物理内存的虚拟内存页面时所引发的一种中断。这种中断是由硬件或操作系统内核生成的,它们使得操作系统能够进行必要的操作以满足页面的访问需求。

与普通中断一样,当发生页中断的时候也会进入到相应的中断处理流程中,进而实现某些操作系统的赋予用户的功能。我们知道对于虚拟空间的内存,其共会经历三个阶段:未映射未分配、已映射未分配、已映射已分配。而页中断的中断处理过程将实现虚拟内存状态从已映射未分配转向已映射已分配。

作用

  • 虚拟内存支持

    页中断机制是支持虚拟内存的关键机制。当程序访问虚拟内存中的某一页,而该页当前未加载到物理内存中时,会触发页中断。操作系统通过这个机制,能够将虚拟内存页面从磁盘加载到物理内存,从而满足程序的内存需求。

  • 内存管理

    有效的管理内存的换入换出,提高内存的使用效率

  • 内存保护

    如果程序试图访问没有权限的虚拟页则会触发缺页中断,操作系统可以检测并处理这种情况,防止程序访问无权限的内存区域。

  • 内存分配

    对于用户申请内存空间以及相关功能(mmap、fork、execve)底层都是通过页中断机制实现的

类型

  • 缺页中断(Page Fault on Demand)

    当程序访问一个虚拟内存页面,但该页面当前未加载到物理内存中时,会触发缺页中断。操作系统会根据需要将缺失的页面从磁盘加载到物理内存,以满足程序的访问需求。

  • 写保护中断(Page Fault for Write)

    当程序试图写入一个只读的虚拟内存页面时,会触发写保护中断。操作系统可以通过将页面标记为可写,或者进行写时复制(Copy-On-Write)操作来处理这种情况。

  • 非法访问中断(Page Fault for Non-Existent Memory)

    当程序试图访问虚拟地址空间中未分配的部分时,会引发非法访问中断。操作系统通常会响应这种中断并处理访问错误。

  • 页面大小中断(Page Size Fault)

    在一些体系结构中,不同的虚拟内存页面可以有不同的大小。如果程序试图访问不匹配页面大小的地址,会触发页面大小中断。

在操作系统中最重要且最常用的中断类型就是缺页中断和写保护中断,这两个中断支撑起了需要系统的调用的核心原理

结构体说明

mm_struct

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
398 struct mm_struct {
399 struct vm_area_struct * mmap; // 虚拟地址空间结构体,双向链表包含红黑树节点访问到不能访问的区域。
400 struct rb_root mm_rb; // 红黑树的根节点
401 struct vm_area_struct * mmap_cache; // mmap的高速缓冲器,指的是mmap最后指向的一个虚拟地址区间
402 #ifdef CONFIG_MMU
403 unsigned long (*get_unmapped_area) (struct file *filp,
404 unsigned long addr, unsigned long len,
405 unsigned long pgoff, unsigned long flags);
406 void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
407 #endif
408 unsigned long mmap_base; // mmap区域的基地址
409 unsigned long mmap_legacy_base; // 自底向上的配置
410 unsigned long task_size; // 进程的虚拟地址空间大小
411 unsigned long cached_hole_size; // 缓冲器的最大的大小
412 unsigned long free_area_cache; // 不受约束的空间大小
413 unsigned long highest_vm_end; // 虚拟地址空间最大结尾地址
414 pgd_t * pgd; // 页表的全局目录
415 atomic_t mm_users; // 用户数量
416 atomic_t mm_count; // 有多少用户引用mm_struct
417 atomic_long_t nr_ptes; // 页表
418 int map_count; // 虚拟地址空间的个数
419
420 spinlock_t page_table_lock; // 保护页表和用户
421 struct rw_semaphore mmap_sem; // 读写信号
422
423 struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung
424 * together off init_mm.mmlist, and are protected
425 * by mmlist_lock
426 */
428
429 unsigned long hiwater_rss; /* High-watermark of RSS usage */
430 unsigned long hiwater_vm; /* High-water virtual memory usage */
431
432 unsigned long total_vm; // 映射的总页数
433 unsigned long locked_vm; // 已设置PG_mlocked的页面
434 unsigned long pinned_vm; // 重新计数永久增加
435 unsigned long shared_vm; // 共享页面
436 unsigned long exec_vm; // 可执行可写页面
437 unsigned long stack_vm;
438 unsigned long def_flags;
439 unsigned long start_code, end_code, start_data, end_data; //开始代码段,结束代码。开始数据,结束数据
440 unsigned long start_brk, brk, start_stack; //堆的开始和结束。
441 unsigned long arg_start, arg_end, env_start, env_end; //参数的起始和结束,环境变量的起始和终点
};

vm_area_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct vm_area_struct {
struct mm_struct *vm_mm; // 指向进程的 mm_struct 结构
unsigned long vm_start; // 虚拟内存区域的起始地址
unsigned long vm_end; // 虚拟内存区域的结束地址
struct vm_area_struct *vm_next; // 指向下一个 vm_area_struct 结构
struct vm_area_struct *vm_prev; // 指向前一个 vm_area_struct 结构

pgprot_t vm_page_prot; // 页面保护属性
unsigned long vm_flags; // 虚拟内存区域的标志位

// 以下是一些在内核中进行内部管理的字段,用于将 vm_area_struct 插入红黑树等
struct rb_node vm_rb;
struct rb_node vm_rb_subtree_last;
};

源码分析

本节相关的源码取自Linux-4.4(MIPS)只保留了关键段落代码

整体调用流程

  1. 在 Linux 系统上,页中断服务程序的名称是 do_page_fault

    1
    2
    3
    4
    5
    6
    7
    8
    // fault.c
    asmlinkage void __kprobes do_page_fault(struct pt_regs *regs, unsigned long write, unsigned long address)
    {
    enum ctx_state prev_state;
    prev_state = exception_enter();
    __do_page_fault(regs, write, address);
    exception_exit(prev_state);
    }
  2. 内核态用户态上下文切换

    • 对于exception_enter函数,其流程为:检查上下文跟踪是否启用;如果启用,读取先前上下文状态;如果先前状态是内核态直接返回,如果不是内核态则调用context_tracking_exit通知上下文跟踪子系统处理器正在退出用户模式并进入内核模式。
    • 对于exception_exit函数,基本流程与上述一致。只是最后context_tracking_enter函数将通知上下文跟踪子系统处理器将从内核模式退出并进入非内核模式即先前的上下文模式(用户模式)
    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
    // context_tracking.h
    static inline enum ctx_state exception_enter(void)
    {
    enum ctx_state prev_ctx;

    if (!context_tracking_is_enabled()) // 判断上下文跟踪是否启用
    return 0;

    prev_ctx = this_cpu_read(context_tracking.state); // 读取先前上下文状态,内核态/用户态?
    if (prev_ctx != CONTEXT_KERNEL) // 不是内核态直接退出上一状态
    context_tracking_exit(prev_ctx);

    return prev_ctx;
    }
    static inline void exception_exit(enum ctx_state prev_ctx)
    {
    if (context_tracking_is_enabled()) {
    if (prev_ctx != CONTEXT_KERNEL)
    context_tracking_enter(prev_ctx);
    }
    }

    // context_tracking_state.h
    enum ctx_state {
    CONTEXT_DISABLED = -1, /* returned by ct_state() if unknown */
    CONTEXT_KERNEL = 0,
    CONTEXT_USER,
    CONTEXT_GUEST,
    } state;
  3. 页中断的下层封装__do_page_fault

    • find_vma通过失效地址addr来查找vma,如果没有找到vma,说明addr地址还没有在进程地址空间中分配任何一个VMA的线性区,这将是一种严重的错误,返回VM_FAULT_BADMAP错误,内核将会杀掉该进程
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // https://elixir.bootlin.com/linux/latest/source/arch/mips/mm/fault.c
    static void __do_page_fault(struct pt_regs *regs, unsigned long write, unsigned long address)
    {
    // 为了获得有关内核恐慌、内核oops、不可屏蔽中断或其他事件的通知,调用者需要将其自身插入到链中
    if (notify_die(DIE_PAGE_FAULT, "page fault", regs, -1, current->thread.trap_nr, SIGSEGV) == NOTIFY_STOP) return;
    // 查找VMA
    vma = find_vma(mm, address);
    // 判断VMA是否有写权限
    if (!(vma->vm_flags & VM_WRITE))
    xxx;
    // 页中断的核心处理函数
    fault = handle_mm_fault(mm, vma, address, flags);
    // 相关goto处理函数
    xxx
    }
  4. 核心函数handle_pte_fault

    对于虚拟地址的映射过程,首先检查表项中P位(PRESENT),该项表示物理页面是否存在在内存中。如果P=0那么其他各个字段都毫无意义,并且pte_none检查页表项也为空说明页表项未建立,如果pte_none非空说明映射已经建立,只是物理页面不在内存中。

    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
    // mm/memory.c
    static int handle_pte_fault(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long address, pte_t *pte, pmd_t *pmd, unsigned int flags)
    {
    // 内存屏障
    barrier();
    //pte页表项PRESENT未置位,说明PTE还没有映射物理页面(没有页面或页面被交换出去)
    if (!pte_present(entry)) {
    // 判断页表项是否为空(映射)
    if (pte_none(entry)) {
    // 判断VMA是否是匿名映射
    if (vma_is_anonymous(vma))
    // vma是匿名映射线性区,匿名映射
    return do_anonymous_page(mm, vma, address, pte, pmd, flags);
    else
    // vma是文件映射线性区,文件映射
    return do_fault(mm, vma, address, pte, pmd, flags, entry);
    }
    //PTE非空,从swap分区读回页面
    return do_swap_page(mm, vma, address, pte, pmd, flags, entry);
    }

    if (pte_protnone(entry))
    //非一致性内存访问(NUMA)下的页表映射操作
    return do_numa_page(mm, vma, address, entry, pte, pmd);

    if (flags & FAULT_FLAG_WRITE) {
    if (!pte_write(entry)) // PTE只读
    return do_wp_page(mm, vma, address, pte, pmd, ptl, entry); //写时复制,fork父子进程
    entry = pte_mkdirty(entry); // pte可写时,标记为脏,
    }

    // /标记pte项刚刚被访问过,以免页面被换出
    entry = pte_mkyoung(entry);
    if (ptep_set_access_flags(vma, address, pte, entry, flags & FAULT_FLAG_WRITE)) {
    update_mmu_cache(vma, address, pte);
    } else {
    if (flags & FAULT_FLAG_WRITE)
    flush_tlb_fix_spurious_fault(vma, address); // flush tlb cache
    }
    }

匿名映射(do_anonymous_page)

  • 调用时机:PTE不存在且Flags是匿名映射

  • 处理逻辑:

    • 读操作

      使用 零页(BSS段) 进行映射,并且设置为只读。因为这里已经是缺页异常的请求调页的处理,所以肯定是本进程第一次访问这个页,分配全零即可。

    • 写操作

      申请一块新的物理内存页,然后根据物理内存页的地址生成映射关系,再对页表项进行映射,增加进程匿名页面的计数,把匿名页面添加到RMAP系统和LRU对应链表里,以用于后续进行内存回收。

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
unsigned int flags)
{
// //如果page_table之前用来建立了临时内核映射,则释放该映射
pte_unmap(page_table);

// 判断映射虚拟内存vma是否在不同进程间共享
if (vma->vm_flags & VM_SHARED)
return VM_FAULT_SIGBUS;

/* Use the zero-page for reads, VMA只读情况 */
if (!(flags & FAULT_FLAG_WRITE) && !mm_forbids_zeropage(mm)) {
// 创建页表项(使用全0页,并设置属性标记)
entry = pte_mkspecial(pfn_pte(my_zero_pfn(address), vma->vm_page_prot));
// 在给定的进程地址空间中,找到虚拟地址 address 对应的页表项,并返回一个指向该页表项的指针
page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
if (!pte_none(*page_table))
goto unlock;
/* Deliver the page fault to userland, check inside PT lock,传递page fault到用户空间 */
if (userfaultfd_missing(vma)) {
pte_unmap_unlock(page_table, ptl);
return handle_userfault(vma, address, flags, VM_UFFD_MISSING);
}
goto setpte;
}

/* 以下是VMA可写的情况 */
// 为进程地址空间中的VMA准备struct anon_vma结构
if (unlikely(anon_vma_prepare(vma)))
goto oom;
// 分配可移动的匿名页面,底层通过alloc_page
page = alloc_zeroed_user_highpage_movable(vma, address);
if (!page)
goto oom;
// 请求的内存分配进行资源计数,以检查是否有足够的内存资源可供使用
if (mem_cgroup_try_charge(page, mm, GFP_KERNEL, &memcg))
goto oom_free_page;
// 内核宏,用于设置页表项中的 "uptodate" 标志位,表示页面已经更新并且包含了有效数据。在页缓存和文件系统操作中,这个标志用于跟踪页面的有效性状态。
__SetPageUptodate(page);
// 创建页表项 PTE
entry = mk_pte(page, vma->vm_page_prot);
if (vma->vm_flags & VM_WRITE)
// 设置页表项可写
entry = pte_mkwrite(pte_mkdirty(entry))
// 获取 PTE 地址
page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
if (!pte_none(*page_table))
goto release;

/* Deliver the page fault to userland, check inside PT lock,传递page fault到用户空间*/
if (userfaultfd_missing(vma)) {
pte_unmap_unlock(page_table, ptl);
mem_cgroup_cancel_charge(page, memcg);
page_cache_release(page);
return handle_userfault(vma, address, flags,
VM_UFFD_MISSING);
}
// 增加mm的rss成员加一,用于记录分配给本进程的物理页总数
inc_mm_counter_fast(mm, MM_ANONPAGES);
// 建立线性区和匿名页的反向映射
page_add_new_anon_rmap(page, vma, address);
mem_cgroup_commit_charge(page, memcg, false);
// 将一个页面添加到 LRU(Least Recently Used)缓存中的不活跃链表或不可驱逐链表
lru_cache_add_active_or_unevictable(page, vma);
setpte:
// /将pte页表项值entry设置到硬件page_table页表项
set_pte_at(mm, address, page_table, entry);
/* No need to invalidate - it was non-present before,更新MMU Cache */
update_mmu_cache(vma, address, page_table);
unlock:
pte_unmap_unlock(page_table, ptl);
return 0;
// 异常处理
release:
mem_cgroup_cancel_charge(page, memcg);
page_cache_release(page);
goto unlock;
oom_free_page:
page_cache_release(page);
oom:
return VM_FAULT_OOM;

文件映射(do_fault)

  • 调用条件:PTE不存在,该vma里的页是文件页或者共享匿名页
  • 调用流程:
    • 把文件页从存储设备上的文件系统读到文件的缓存 (Page Cache)
    • 设置进程的页表项PTE,把虚拟页映射到文件的页缓存物理页(Page Cache)
1
2
3
4
5
6
7
8
9
10
11
12
13
static int do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
unsigned int flags, pte_t orig_pte)
{
// 缺页由读内存导致
if (!(flags & FAULT_FLAG_WRITE))
return do_read_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);
// 缺页由写内存导致,且是非共享映射
if (!(vma->vm_flags & VM_SHARED))
return do_cow_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);
// 缺页由写内存导致,且是共享映射
return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);
}

do_read_fault

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int do_read_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd,
pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
// do_fault_around函数 建立映射关系,而不会创建页面高速缓存
// 提前建立进程地址空间与页面高速缓存的映射关系有利于减小因为发生缺页的次数,提高了效率
if (vma->vm_ops->map_pages && fault_around_bytes >> PAGE_SHIFT > 1) {
do_fault_around(vma, address, pte, pgoff, flags);
if (!pte_same(*pte, orig_pte))
goto unlock_out;
pte_unmap_unlock(pte, ptl);
}
// do_fault是真正为异常地址分配物理页面
ret = __do_fault(vma, address, pgoff, flags, NULL, &fault_page);
//为物理页面和缺页异常发生的虚拟地址建立映射关系,即使用这个物理页面来创建一个PTE,然后设置PTE,然后释放页锁,并且唤醒等待这个页锁的进程
do_set_pte(vma, address, fault_page, pte, false, false);
}

do_cow_fault

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int do_cow_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd,
pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
// 准备 anon_vma 结构体
if (unlikely(anon_vma_prepare(vma)))
return VM_FAULT_OOM;
// 申请页
new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);
// 将内容读取到页上
ret = __do_fault(vma, address, pgoff, flags, new_page, &fault_page);
// 如果fault页面存在,将其拷贝到new页面上
if (fault_page)
copy_user_highpage(new_page, fault_page, address, vma);
// 设置页表项中的 "uptodate" 标志位,表示页面已经更新并且包含了有效数据
__SetPageUptodate(new_page);
// 设置PTE
do_set_pte(vma, address, new_page, pte, true, true);
}

do_share_fault

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
static int do_shared_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd,
pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
// 通过vma->vm_ops->fault回调函数来读取文件内容到vmf->page里
ret = __do_fault(vma, address, pgoff, flags, NULL, &fault_page);
// 通知进程地址空间,页面将变成可写的。如果一个页面变成可写的,那么进程可能需要等待这个页面的内容回写成功
if (vma->vm_ops->page_mkwrite) {
unlock_page(fault_page);
tmp = do_page_mkwrite(vma, fault_page, address);
if (unlikely(!tmp ||
(tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
page_cache_release(fault_page);
return tmp;
}
}
// 将新生成的PTE entry设置到硬件页表中
do_set_pte(vma, address, fault_page, pte, true, false);
// 将page标记为dirty
if (set_page_dirty(fault_page))
dirtied = 1;
// 通过balance_dirty_pages_ratelimited()来平衡并回写一部分脏页
if ((dirtied || vma->vm_ops->page_mkwrite) && mapping)
balance_dirty_pages_ratelimited(mapping);
}

公共函数(_do_fault)

主要完成缺页对应文件页缓存(file cache)的获取

对于回调函数filemap_fault() ,会判断内存中有没有以前访问后留下的文件页缓存(file cache),如果还有,则直接把留存的 file cache 赋给 vmf—>page。如果没有,则需要先新申请页作为 file cache,再从磁盘中读取文件的数据到 file cache。file cache 的存在使得第二次映射同一个文件时,读取会比第一次快很多

1
2
3
4
5
6
7
8
static int __do_fault(struct vm_area_struct *vma, unsigned long address,
pgoff_t pgoff, unsigned int flags,
struct page *cow_page, struct page **page)
{
// 调用文件映射页的虚拟地址区域的 fault 钩子函数
// 一般文件系统对应 filemap_fault() 函数,也就是说 vm_ops 一般定义为 generic_file_vm_ops
ret = vma->vm_ops->fault(vma, &vmf);
}

写时复制(do_wp_page)

  • 调用条件:PTE页表项存在且PRESENT已置位;此次操作为写操作且PTE没有写权限

  • 处理逻辑

    对线性地址对应的物理页面进行检验,首先判断是否存在共享,如果没有,直接设置pte为可读可写,然后刷新页缓冲之后返回;如果有共享,则通过get_free_page函数取得一页新的物理页面,然后取消共享,将新的页表项设置为这个新页的地址,刷新缓冲,然后将旧页面的内容复制到新页面。

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
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
spinlock_t *ptl, pte_t orig_pte)
__releases(ptl)
{
struct page *old_page;

old_page = vm_normal_page(vma, address, orig_pte);
if (!old_page) {
// 处理可写并且共享的特殊映射页面(包括VM_MIXEDMAP或VM_PFNMAP页面),并调用wp_page_reuse函数来复用缺页异常页面
if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) == (VM_WRITE|VM_SHARED))
return wp_pfn_shared(mm, vma, address, page_table, ptl, orig_pte, pmd);
pte_unmap_unlock(page_table, ptl);
return wp_page_copy(mm, vma, address, page_table, pmd, orig_pte, old_page);
}
if (PageAnon(old_page) && !PageKsm(old_page)) {
if (reuse_swap_page(old_page)) {
// wp_page_reuse:处理可以复用的页面
return wp_page_reuse(mm, vma, address, page_table, ptl, orig_pte, old_page, 0, 0);
}
} else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) == (VM_WRITE|VM_SHARED))) {
return wp_page_shared(mm, vma, address, page_table, pmd, ptl, orig_pte, old_page);
}
//处理写时复制,分配新的page,拷贝旧页面到新页面,并根据vm_page_prot生成新的pte,添加到硬件页表
return wp_page_copy(mm, vma, address, page_table, pmd, orig_pte, old_page);
}

do_numa_page

非一致性内存访问(NUMA)下的页表映射操作

do_swap_page

此时pte的内容所对应的页面在swap空间中,那么就需要由pte得到swap entry,再由swap entry得到page,再由pte以及pte entry添加到pte页表

总结

异常来源 相关函数调用 中断动作
页面未映射 do_anonymous_page 分配物理页面并在页表中设置从虚拟地址到物理地址的映射
写只读页面 do_wp_page 如果写时复制就复制一页并标记可写返回进程;如果异常发送SIGSEGV
页面在磁盘中 从磁盘按需加载
没有访问权限 发送SIGSEGV信号

参考