基础
概念
“页中断”(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 | 398 struct mm_struct { |
vm_area_struct
1 | struct vm_area_struct { |
源码分析
本节相关的源码取自Linux-4.4(MIPS)只保留了关键段落代码
整体调用流程
在 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);
}内核态用户态上下文切换
- 对于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;页中断的下层封装__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
}核心函数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 | static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma, |
文件映射(do_fault)
- 调用条件:PTE不存在,该vma里的页是文件页或者共享匿名页
- 调用流程:
- 把文件页从存储设备上的文件系统读到文件的缓存 (Page Cache)
- 设置进程的页表项PTE,把虚拟页映射到文件的页缓存物理页(Page Cache)
1 | static int do_fault(struct mm_struct *mm, struct vm_area_struct *vma, |
do_read_fault
1 | static int do_read_fault(struct mm_struct *mm, struct vm_area_struct *vma, |
do_cow_fault
1 | static int do_cow_fault(struct mm_struct *mm, struct vm_area_struct *vma, |
do_share_fault
1 | static int do_shared_fault(struct mm_struct *mm, struct vm_area_struct *vma, |
公共函数(_do_fault)
主要完成缺页对应文件页缓存(file cache)的获取
对于回调函数filemap_fault() ,会判断内存中有没有以前访问后留下的文件页缓存(file cache),如果还有,则直接把留存的 file cache 赋给 vmf—>page。如果没有,则需要先新申请页作为 file cache,再从磁盘中读取文件的数据到 file cache。file cache 的存在使得第二次映射同一个文件时,读取会比第一次快很多
1 | static int __do_fault(struct vm_area_struct *vma, unsigned long address, |
写时复制(do_wp_page)
调用条件:PTE页表项存在且PRESENT已置位;此次操作为写操作且PTE没有写权限
处理逻辑
对线性地址对应的物理页面进行检验,首先判断是否存在共享,如果没有,直接设置pte为可读可写,然后刷新页缓冲之后返回;如果有共享,则通过get_free_page函数取得一页新的物理页面,然后取消共享,将新的页表项设置为这个新页的地址,刷新缓冲,然后将旧页面的内容复制到新页面。
1 | static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma, |
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信号 |