Linux深入理解内存管理32

Linux深入理解内存管理32(基于Linux6.6)---__do_page_fault处理介绍

一、前情回顾

内核对于异常处理的总体的流程: 

在 Linux 内核中,异常处理(Exception Handling)是操作系统核心的一个重要部分,它确保在硬件或软件出现错误时,内核能够及时、准确地做出反应,避免系统崩溃或不稳定。内核的异常处理包括硬件异常(如 CPU 异常、内存错误)、软件异常(如用户进程访问无效内存)以及系统调用的处理等。

1. 异常的分类

异常可以大致分为以下几类:

  • 硬件异常(Hardware Exception)

    • CPU 异常:如除零错误、非法指令、段错误等。
    • 内存访问异常:如缺页错误(Page Fault)、总线错误等。
  • 软件异常(Software Exception)

    • 系统调用:用户空间通过软中断发起系统调用。
    • 信号处理:进程收到信号(如 SIGSEGV)时需要进行处理。
  • 中断(Interrupts):虽然严格来说,中断不是异常的一部分,但它们也会触发内核的处理机制,处理外部事件(如硬件中断)。

  • 虚拟化相关的异常:在虚拟化环境中,异常可能涉及到虚拟机监控程序(Hypervisor)处理的虚拟化特定错误。

2. 异常处理的总体流程

Linux 内核中的异常处理机制是通过中断和异常处理程序来实现的。当系统中出现异常时,CPU 会通过一组硬件中断向内核报告错误,内核会根据异常的类型进行适当的处理。

(1)异常的触发

异常可以由硬件、软件或外部事件触发,具体触发的过程如下:

  • 硬件异常:例如除零、访问无效内存等,CPU 会自动将异常信息存储在异常寄存器中,并转向一个预定义的异常处理程序。

  • 中断:由硬件设备(如定时器、I/O 设备)发起的中断请求,通过中断向量表进行处理。

  • 系统调用:用户进程通过软中断(如 int 0x80syscall 指令)发起系统调用,内核通过 syscall 中断进入系统调用处理流程。

(2)中断向量表(Interrupt Vector Table)

无论是硬件中断、异常还是系统调用,Linux 都使用中断向量表来管理所有的异常处理函数。每个异常和中断都有一个唯一的向量值(即编号),并在中断向量表中注册相应的处理函数。

  • 硬件中断和异常:这些异常和中断会触发与之对应的中断处理函数(IRQ Handler 或 Exception Handler)。
  • 系统调用:系统调用会触发 syscall 中断,跳转到特定的系统调用处理函数。

(3)异常处理的核心步骤

当异常发生时,内核的处理流程大致如下:

  1. CPU 保存现场:当发生异常或中断时,CPU 会自动保存当前的上下文(如寄存器状态、程序计数器等),并跳转到中断处理程序。

  2. 异常类型识别:内核通过中断号或异常号识别异常的类型。不同类型的异常会被分配不同的中断向量,并调用对应的处理函数。

  3. 处理异常

    • 硬件异常:内核首先根据异常类型(如除零、段错误、缺页错误等)来选择对应的处理方式。
    • 软件异常:对于软件引发的异常(如系统调用或进程信号处理),内核会根据具体的操作来进行处理。
  4. 错误报告和恢复

    • 错误报告:如果异常导致进程崩溃(如访问无效内存),内核会生成日志,报告错误并处理相应的信号(如 SIGSEGV)。
    • 错误恢复:一些异常(如缺页错误)可以通过恢复执行或重新加载页面来解决;而对于一些不可恢复的错误(如非法指令),内核可能会终止进程或系统。
  5. 恢复执行:异常处理完毕后,内核会恢复进程的执行,或在必要时终止进程。如果异常处理是由硬件中断触发的,中断处理程序会执行完毕后返回到原先的执行流。

(4)内核异常处理的具体流程

在具体的 Linux 内核实现中,异常处理的具体步骤包括:

  • 异常发生时,CPU 进入内核模式:CPU 切换到内核模式,开始执行内核的异常处理程序。
  • 查找中断向量表中的处理函数:根据异常号或中断号,内核会从中断向量表中查找到对应的异常处理函数。
  • 保存进程上下文:内核会保存当前进程的上下文信息,包括寄存器状态、堆栈等。
  • 调用对应的异常处理程序:根据异常类型,调用对应的处理程序进行处理。常见的异常类型有:
    • 缺页错误(Page Fault):通过 do_page_fault 函数处理。
    • 除零错误(Divide by Zero):通过 do_divide_error 处理。
    • 段错误(Segmentation Fault):通过 do_segmentation_fault 处理。
  • 恢复执行或终止进程:根据异常的处理结果,决定是否恢复进程的执行或终止进程。

(5)异常处理的关键函数

  • do_irq:用于处理中断请求,通常涉及硬件设备的处理中断。
  • do_page_fault:用于处理缺页错误(Page Fault),即进程访问到未映射或已被交换的内存页面。
  • do_divide_error:用于处理除零错误。
  • do_segv:用于处理段错误(Segmentation Fault),通常由非法内存访问引起。
  • syscall:用于系统调用的处理,内核通过此函数接收并处理用户进程请求的系统调用。

3. 错误处理和信号机制

异常处理不仅仅是处理硬件或软件错误,还包括进程与内核之间的信号传递。例如,进程尝试访问非法内存时,内核会触发 SIGSEGV 信号,通知进程发生段错误,进程可以选择处理该信号,或者直接终止。

4. 异常的恢复与终止

对于某些异常(如缺页错误),内核可以通过恢复内存或修改页表来继续执行。然而对于其他不可恢复的错误(如非法指令),内核会终止进程并产生错误日志,进程会收到一个致命信号。

二、概述

在 Linux 内核中,__do_page_fault 是处理 页面错误(Page Fault)的核心函数之一。页面错误是指在进程访问某个虚拟内存页面时,操作系统发现该页面不在物理内存中,可能是页面被交换出去了,或者该页面尚未映射到物理内存。页面错误通常发生在进程试图读取或写入一个无效的内存地址时。

页面错误的处理过程非常关键,Linux 内核通过高效的机制来处理这种错误,确保进程可以继续执行或根据需要进行适当的错误处理。

1. 页面错误发生的条件

页面错误通常发生在以下情况下:

  • 缺页错误(Page Not Present):当进程访问的虚拟页没有映射到物理内存时,会发生缺页错误。操作系统需要将该虚拟页加载到物理内存中。
  • 写保护错误(Write Protection Fault):进程试图修改一个只读页面(如共享库或映射文件的只读部分)时,会发生写保护错误。
  • 访问控制错误:操作系统检测到进程试图以不允许的方式访问内存(如执行非可执行区域的代码)时,也会触发页面错误。

2. 页面错误处理的基本流程

__do_page_fault 主要负责处理这些错误,它的处理流程可以概述为以下步骤:

(1)捕获错误并进入异常处理

当进程发生页面错误时,CPU 会触发一个异常,内核会调用异常处理函数。对于页面错误,内核会进入 __do_page_fault

  • 上下文切换:当页面错误发生时,CPU 会保存当前执行的上下文,并触发内核异常处理。__do_page_fault 会使用异常传递的信息(如错误码、缺失的虚拟地址等)来处理错误。

(2)解析错误类型

__do_page_fault 会首先解析触发页面错误的类型,这通常依赖于 CPU 提供的错误码。错误码能够告诉内核具体是因为哪种原因导致的页面错误。

  • 缺页错误:若虚拟页没有映射到物理内存,内核需要将它加载进来。内核会根据页面的类型(如匿名页、文件映射页、共享内存等)执行不同的处理逻辑。
  • 写保护错误:如果进程试图写入只读页面,内核会根据页面的保护属性来决定是允许写入,还是产生进程的信号(如 SIGSEGV)。

(3)查找页表项(Page Table Entry, PTE)

内核需要检查进程的页表,看是否已经为该虚拟页分配了物理内存。页表项包含了虚拟页和物理页之间的映射关系。

  • 页表不存在:如果页表项不存在,表示该页面需要从磁盘中加载。
  • 页表项存在但标记为无效:如果页表项存在,但标记为不可用(如无效的保护标志),则会根据具体的错误类型进行不同的处理。

(4)缺页处理:加载页面

如果页面是缺页错误(即页表项不存在),内核会尝试加载该页面。常见的加载策略包括:

  • 文件映射页面:如果页面是通过 mmap 映射的文件,内核会从磁盘中读取该文件的相应部分。
  • 匿名映射页面:如果是匿名映射页面(如堆栈、堆区等),内核会为其分配一个新的物理页面。
  • 交换页面:如果页面已经被交换到磁盘(即页面置换),内核会从交换空间加载该页面回物理内存。

(5)更新页表并恢复执行

一旦页面被加载或创建,内核会更新进程的页表,将虚拟地址与物理地址之间的映射关系设置好。此时,内核会恢复进程的执行,返回到发生错误的指令。

如果这是由于写保护错误或其他访问控制错误导致的,内核会根据访问权限进行处理。如果需要的话,还会发送信号(如 SIGSEGV)给进程,通知进程发生了访问错误。

(6)结束处理

  • 如果页面加载成功,进程可以继续执行,内核退出页面错误处理函数。
  • 如果处理失败(例如,分配内存失败,或页面无法从交换空间加载),进程可能会收到致命信号(如 SIGSEGV),导致进程终止。

三、具体流程

内核对于异常处理的总体的流程,从异常向量为入口,最终调用到真正的异常处理的接口__do_page_fault,内存缺页异常的常见场景中如何实现:

arch/arm/mm/fault.c

static vm_fault_t __kprobes
__do_page_fault(struct mm_struct *mm, unsigned long addr, unsigned int flags,
		unsigned long vma_flags, struct pt_regs *regs)
{
	struct vm_area_struct *vma = find_vma(mm, addr);---解析1
	if (unlikely(!vma))
		return VM_FAULT_BADMAP;

	if (unlikely(vma->vm_start > addr)) {---解析2
		if (!(vma->vm_flags & VM_GROWSDOWN))
			return VM_FAULT_BADMAP;
		if (addr < FIRST_USER_ADDRESS)
			return VM_FAULT_BADMAP;
		if (expand_stack(vma, addr))
			return VM_FAULT_BADMAP;
	}

	/*
	 * ok, we have a good vm_area for this memory access, check the
	 * permissions on the VMA allow for the fault which occurred.
	 */
	if (!(vma->vm_flags & vma_flags))---解析3
		return VM_FAULT_BADACCESS;

	return handle_mm_fault(vma, addr & PAGE_MASK, flags, regs);---解析4
}
  • 首先find_vma通过失效地址addr来查找vma,如果没有找到vma,说明addr地址还没有在进程地址空间中分配任何一个VMA的线性区,这将是一种严重的错误,返回VM_FAULT_BADMAP错误,内核将会杀掉该进程
  • 找到了vma,如果发现addr不在vma的映射区,可能是下面两种原因
  • 该区域的VM_GROWSDOWN标志位置位,意味着该区域是栈区,自顶向下增长,接下来调用expand_stack适当地增大栈
  • 找到的区域不是栈,访问无效
  • expand_stack函数主要完成
  • 开展堆栈区间的VMA,重新调整VMA的起始地址以可以容纳addr,这个前提是不会导致进程区间超限或者进程动态分配的页面超限
  • expand_stack只是更改了堆栈区的vm_area_struct结构,没有建立物理内存映射
  • 当返回值为非0 ,就返回对应的错误码,并退出,交由上级程序处理
  • 当地址存在后,调用access_error判断当前vma是否具有可写或可执行权限,如果发生一个写错误的缺页异常,首先判断vma属性是否具有可写,如果没有就返回VM_FAULT_BADACCESS
  • 最后调用handle_mm_fault函数,它是缺页中断的核心处理函数,正常情况下将返回VM_FAULT_MAJOR或VM_FAULT_MINOR,返回错误码fault并加一task的maj_flt或min_flt成员;

确定异常是在允许的地址触发,内核必须确定将所需数据读取到物理内存的适当方法,该任务委托给handle_mm_fault是一个体系结构无关的,用于选择适当的异常恢复方法(按需调页/换入等),并应用选择的方法。其大致的处理流程如下图所示:

handle_mm_fault为引发缺页的进程分配一个物理页框,它先确定与引发缺页的线性地址对应的各级页目录项是否存在,如不存在则分进行分配。具体如何分配这个页框是通过调用handle_pte_fault()完成的,注意最后一个参数flag,它来源于fsr。

  • 创建各级页表目录,该函数支持四级页表,而ARM32只支持2级页表,那么pgd = pua = pmd,该函数主要是分配pmd,然后调用handle_pte_fault

 handle_mm_fault函数确认在各级页目录中,通向对应于异常地址页表项的各个页目录都存在,handle_pte_fault函数分析缺页的原因,并进行处理,其大致的处理流程如下:

 

1、如果页不在物理内存中,那么就分为以下3中情况:

  • 匿名映射: 如果PTE的内容为空,没有找到对应的页表项,对于anonymous page,将通过调用do_anonymous_page()函数来分配和映射新页面,也就是按需分配。用户空间使用malloc()进行内存申请时(对应底层的实现是mmap或者brk),内核并不会立刻为其分配物理内存,而只是为请求的进程的rbtree管理的vma信息中记录(添加或更改)诸如内存范围和标志之类的信息。只有当内存被真正使用,触发page fault,才会真正分配物理页面和对应的页表项,即demand alloction,对应的函数实现是do_anonymous_page()。通过mmap映射建立的heap和stack等内存区域,在初始未使用时,也适用于这样的规则。
  • 文件映射: 如果PTE的内容为空,处理文件页发生异常,将会通过do_fault来分配和映射新页面对于page cache, 在发生内存回收后,部分text(code)段的页面会被discard,部分data段的页面会被writeback,之后再次访问这些页面,也将出现page fault。此时,需要从外部存储介质中,将页面内容调回内存,即demand paging,对应的函数实现是do_fault()。
  • 换入或按需调页: 如果该页标记为不存在,而页表中保存了相关的信息,则意味着页已经被换出,因而必须从系统的某个交换分区换入,则调用do_swap_page来分配和映射新页面。

2、如果页面在物理内存中:

  • 写时复制: 如果页在物理内存中,当用户向共享内存发出写请求时,它将现有的共享页面复制到新页面。

四、总结

在 Linux 内核中,__do_page_fault 是处理 页面错误(Page Fault) 的关键函数之一。页面错误发生时,CPU 会触发一个异常,内核必须处理这个异常以确保程序可以继续运行(如果错误是可恢复的)。页面错误通常由进程访问一个不存在或不再有效的内存页面(例如,缺页、权限错误等)引起。

以下是 __do_page_fault 的处理流程总结:


1. 异常触发

页面错误通常由以下原因触发:

  • 进程尝试访问未映射到物理内存的虚拟内存地址。
  • 进程尝试访问没有足够权限的页面(如读写权限错误)。
  • 进程访问被换出的页面(如交换空间管理)。

当发生页面错误时,CPU 会自动触发一个 缺页中断(Page Fault Interrupt),并将相关的异常信息(如错误码、故障地址等)传递给内核。


2. 进入内核模式

当页面错误发生时,CPU 会进入内核模式,调用内核的 中断处理程序。在 Linux 内核中,页面错误的中断号(异常号)通常会映射到 do_page_fault__do_page_fault 函数。


3. 获取并检查页面错误的相关信息

__do_page_fault 会首先从 CPU 中获取与页面错误相关的信息,包括:

  • 错误码(Error Code):该错误码提供了错误的类型和详细信息。例如,错误码的位可以指示访问是由于权限错误(如试图写入只读页)或缺页错误(如访问未映射的内存)引起的。
  • 故障地址(Faulting Address):发生页面错误时,进程访问的虚拟地址。

__do_page_fault 会使用这些信息来决定后续的处理方式。


4. 判断错误类型

__do_page_fault 会检查页面错误的类型,常见的页面错误包括:

  • 缺页错误(Page Not Present):虚拟内存页没有被映射到物理内存,通常由页面调度器(页表管理)来解决。
  • 权限错误(Protection Fault):访问权限错误(如写入只读页面)。

错误码的不同位表示不同的错误类型,内核会根据错误码判断是进行页面加载还是产生错误信号。


5. 查找错误的进程虚拟地址空间

每个进程都有一个虚拟地址空间,它映射到内核提供的物理内存或交换空间。内核需要检查发生错误的虚拟地址是否是有效的,并查看该地址是否有合适的映射。如果该页面确实存在于进程的地址空间中,且错误是可恢复的(例如缺页),内核会进行相应的处理。


6. 处理缺页错误(Page Fault)

如果错误是 缺页错误,内核会尝试从磁盘、交换空间、文件或其他地方加载缺失的页面。处理缺页错误的过程通常包括:

  • 查找映射:内核首先检查页表,确认该虚拟地址是否已经有对应的物理地址映射。如果没有,内核将需要加载或分配一个新的页面。

  • 查找交换空间:如果页面被交换到磁盘,内核会将该页面从交换空间中加载回内存。

  • 分配新的页面:如果页面没有在交换空间中,且进程需要访问该地址,内核会分配一个新的页面,并将其映射到进程的虚拟地址空间。

  • 更新页表:一旦内核成功地从磁盘或交换空间中加载了页面或分配了新的页面,它会更新进程的页表,建立虚拟地址到物理地址的映射。

  • 恢复执行:一旦处理完缺页错误,内核会恢复执行进程的代码。


7. 处理权限错误(Protection Fault)

如果错误是 权限错误(如尝试写入只读页面),内核会检查该页的权限设置。如果进程尝试对无权访问的内存执行操作,内核通常会终止进程并发送一个 信号(如 SIGSEGV)给该进程,表示 段错误(Segmentation Fault)。如果进程具有适当的信号处理程序,内核会调用处理程序。


8. 错误恢复与终止

  • 恢复:如果页面错误是可以恢复的(例如缺页错误且页面被成功加载),内核会恢复进程执行。
  • 终止进程:如果是不可恢复的错误(例如权限错误),进程会被终止,并且会发送相关信号(如 SIGSEGV)。

9. 返回用户空间

__do_page_fault 完成错误处理后,内核会根据处理的结果:

  • 如果页面被加载或恢复,内核会恢复进程的执行。
  • 如果进程被终止(例如因权限错误或内存访问异常),内核会通知进程,结束其执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值