编程高手必学的内存知识05:深入理解页中断

目录

1 页中断基础设施

1.1 硬件基础设施

1.1.1 页中断异常向量

1.1.2 触发条件

1.1.3 中断现场信息

1.2 软件基础设施

1.2.1 vm_area_struct结构

1.2.2 2层权限模式

1.2.3 页中断处理函数调用流程

2 页中断类型

3 页中断功能情景分析

3.1 fork系统调用的写时复制

3.2 execve系统调用的按需加载

3.3 mmap系统调用的各种映射

3.3.1 私有匿名映射

3.3.2 私有文件映射

3.3.3 共享文件映射

3.3.4 共享匿名映射


1 页中断基础设施

1.1 硬件基础设施

说明:本节以80386为例说明页中断相关的硬件基础设施

1.1.1 页中断异常向量

1. 80386处理器在保护模式下对异常向量的分配如下表所示,其中页中断为向量14

编程高手必学的内存知识05:深入理解页中断_第1张图片

编程高手必学的内存知识05:深入理解页中断_第2张图片

2. 在构建保护模式下的异常向量表时,需要将页中断处理程序的入口地址设置到IDT的14号中断描述符中

以80386 + Linux 2.4为例,在arch/i386/kernel/entry.S文件中,将page_fault设置为页中断处理函数

编程高手必学的内存知识05:深入理解页中断_第3张图片

1.1.2 触发条件

同时满足如下2个条件时,会触发页中断,

1. 分页机制被启用,即CR0寄存器中的PG位为1

2. 指令在进行内存访问时,出现如下情况,

被访问的线性地址所在的页未驻留在内存中(体现为相应的页目录项或页表项中P位为0)

以不适当的权限访问内存页

说明:在80386体系结构的页目录项 / 页表项格式中,与访问权限相关的位如下,

编程高手必学的内存知识05:深入理解页中断_第4张图片

① U/S(User/Supervisor)用户/系统位

  • 当U/S = 1,表示该页表项所映射的页可以由任特权等级下执行的程序访问
  • 当U/S = 0,表示该页表项所映射的页只能由特权模式(即0 / 1 / 2特权级)下执行的程序访问,如果是执行在特权级3的程序访问,则会触发页中断

② R/W(Read/Write)读写位

  • 当R/W = 1,表示对该页表项所映射的页可以进行读 / 写 / 执行
  • 当R/W = 0,表示对该页表项映射的页可读可执行,但是不能进行写操作。如果对这些页进行写操作,则会触发页中断

1.1.3 中断现场信息

1. 处理器将引起页中断的线性地址保存在CR2,并提供一个出错码,指示引起页中断的存储器访问类型,页中断处理程序可以根据该出错码进行不同的处理

2. 页中断提供的出错码格式如下,

编程高手必学的内存知识05:深入理解页中断_第5张图片

① P位表示页中断是否由页不存在导致

  • P = 0,表示页中断由于访问不存在的页导致
  • P = 1,表示页中断由于访问类型违反保护权限导致

② W位表示引起页中断的访问类型

  • W = 0,表示进行读访问
  • W = 1,表示进行写访问

③ U位表示引起页中断的访问特权级

  • U = 0,表示访问来自特权级0、1、2(系统级)执行的程序
  • U = 1,表示访问来自特权级3(用户级)指向的程序

1.2 软件基础设施

说明:本节以80386 + Linux 2.4为例说明页中断相关的软件基础设施

1.2.1 vm_area_struct结构

1. Linux操作系统将进程的用户地址空间划分为若干区间进行管理,这些区间称为虚拟内存区(virtual memory area,简称vma

2. 进程的用户地址空间主要由mm_struct和vm_area_struct结构来描述,其中,

① mm_struct:描述进程整个用户地址空间

② vm_area_struct:描述用户地址空间中的各个虚拟内存区

二者的关系如下图所示,可见每个虚拟内存区都是一段具有相同属性的内存区域,内核将其作为一个单独的内存对象进行管理

编程高手必学的内存知识05:深入理解页中断_第6张图片

3. vm_area_struct结构中比较重要的属性如下,

编程高手必学的内存知识05:深入理解页中断_第7张图片

1.2.2 2层权限模式

1. vm_area_struct结构中的vm_flags字段设置了虚拟内存区的读 / 写 / 执行权限,该权限在mmap系统调用中设置

编程高手必学的内存知识05:深入理解页中断_第8张图片

2. 如上文所属,在页目录项 / 页表项中也有读 / 写属性权限位

编程高手必学的内存知识05:深入理解页中断_第9张图片

3. 页目录项 / 页表项中的权限位是硬件触发页中断的标准,vm_area_struct结构中的权限位可辅助构建丰富的页中断功能场景

1.2.3 页中断处理函数调用流程

Linux 2.4内核页中断处理函数调用流程如下,

page_fault
--> do_page_fault
  --> handle_mm_fault
    --> handle_pte_fault
      --> do_no_page   // 处理缺页异常
      --> do_swap_page // 处理页面换入换出
      --> do_wp_page   // 处理写保护异常

1. page_fault(arch/i386/kernel/entry.S)

页中断入口函数

② 跳转到error_code标签后,会进行一系列保存现场的操作,之后调用do_page_fault函数

编程高手必学的内存知识05:深入理解页中断_第10张图片

2. do_page_fault(arch/i386/mm/fault.c)

① 判断触发页中断的线性地址的合法性,查找对应的vm_area_struct结构

② 判断触发页中断的操作是否为写操作

3. handle_mm_fault(mm/memory.c)

① 页中断处理过程进入体系无关阶段

② 获取PTE地址并调用handle_pte_fault函数

4. handle_pte_fault(mm/memory.c)

根据不同的页中断类型,调用相应的中断处理函数

编程高手必学的内存知识05:深入理解页中断_第11张图片

2 页中断类型

结合页中断的软硬件基础设施,以及vma和页表项的2层权限模式,就可以构造如下的页中断类型

编程高手必学的内存知识05:深入理解页中断_第12张图片

需要时刻注意的是,硬件触发页中断的原因要么是页表项P位为0,要么是页表项存在但是访问权限有误。那么上述异常类型对应的中断现场信息如下,

1. 页面未映射[只需分配内存]

① 页表项P位为0

② 页表项其余字段也全为0(这其实是操作系统软件的处理约定)

③ 此时会经由do_no_page函数调用do_anonymous_page函数,在其中分配内存并建立映射

编程高手必学的内存知识05:深入理解页中断_第13张图片

2. 页面内容在磁盘中[尚未从磁盘中加载过]

① 页表项P位为0

② 页表项其余字段也全为0

③ 此时会经由do_no_page函数调用vma区域操作函数集中的nopage方法,在其中分配内存,从磁盘中读取数据并建立映射

编程高手必学的内存知识05:深入理解页中断_第14张图片

3. 页面内容在磁盘中[页面内容被交换到磁盘中]

① 页表项P位为0

② 页表项其余字段记录了相应的磁盘信息(这其实是操作系统软件的处理约定)

③ 此时会调用do_swap_page函数进行内存换入操作

编程高手必学的内存知识05:深入理解页中断_第15张图片

4. 写只读页面[触发写时复制]

① 页表项P位为1

② 页表项没有写权限(因此才会在硬件上触发异常)

③ 相应vma有写权限

④ 此时会调用do_wp_page函数进行写时复制操作

编程高手必学的内存知识05:深入理解页中断_第16张图片

5. 写只读页面[无权写入]

① 页表项P位为1

② 页表项没有写权限(因此才会在硬件上触发异常)

③ 相应vma也没有写权限

④ 此时在do_page_fault函数中就会进入bad_area标号进行错误处理

编程高手必学的内存知识05:深入理解页中断_第17张图片

6. 没有访问权限

① 没有访问权限的场景对应do_page_fault函数中各种进入bad_area标签的情况

② 可能是触发页中断的线性地址非法(e.g. 找不到对应的vma),也可能是访问权限非法(e.g. 写一个没有写权限的页面,读一个P位为0的页面)

3 页中断功能情景分析

3.1 fork系统调用的写时复制

1. fork系统调用会为子进程拷贝父进程的线性地址空间,其中就包括vma的拷贝和页表的拷贝。在拷贝页表的过程中,会进行如下操作,

① 取消父子进程页表项的写权限,这样只要父子进程中有一方进行写操作就会触发页中断,但是此时vma原先的写权限是不变的,因此触发页中断后会进入do_wp_page函数进行写时复制处理

② 将已映射的物理页面的引用计数加1

进行上述操作后,父子进程的页表情况如下图所示,

编程高手必学的内存知识05:深入理解页中断_第18张图片

说明:在Linux 2.4内核中,上述操作在copy_page_range函数中完成,

编程高手必学的内存知识05:深入理解页中断_第19张图片

2. 当fork系统调用创建子进程完成后,只要父子进程中有一方进行写操作,就会触发页中断

假设子进程先触发页中断,在do_wp_page函数中会判断触发中断的线性地址所对应的物理页面引用计数,如果引用计数大于1,就说明目前有多个进程共享该物理页面。此时操作系会进如操作,

① 分配一个物理页面

② 将老的物理页面内容拷贝进这个新的物理页面,并且减少老的物理页面的引用计数

③ 将发生中断的线性地址映射到新的物理页,并且恢复页表项中的写权限

这就完成了一次写时复制(Copy On Write,COW),操作后父子进程的页表情况如下图所示,

编程高手必学的内存知识05:深入理解页中断_第20张图片

3. 由于父进程中页表项依然没有写权限,因此当父进程进行写操作时还是会触发页中断,并且也会进入do_wp_page函数。由于此时物理页面的引用计数为1,只需要将父进程页表项的权限恢复为可读写即可

3.2 execve系统调用的按需加载

1. execve系统调用封装函数的原型如下,

#include 

/*
* filename: 可执行程序文件名
* argv: 命令行参数指针数组
* envp: 环境变量参数指针数组
*/
int execve(const char *filename, const char *argv[], const char *envp[]);

2. execve系统调用的执行步骤如下,

清空进程页表,这样整个进程中的页都变成不存在,进程一旦访问这些页,就会触发缺页异常

② 打开待加载的文件,在内核中创建代表这个文件的file结构

③ 加载和解析文件头部信息,头部信息中会描述该文件的Section信息

④ 创建相应的vm_area_struct结构来描述代码段和数据段等,并且将文件的各个Section与这些vm_area_struct结构建立映射关系

⑤ 如果当前加载的文件还依赖其他动态库文件,则找到这个动态库文件,并跳转到step 2继续处理这个动态库文件

⑥ 最后跳转到可执行程序的入口处执行

说明:execve系统调用并不会将文件内容加载到物理内存中,他只是建立了文件Section与vm_area_struct结构的映射关系,此时会将vm_area_struct结构体中的vm_file字段设置为打开的文件

3. 当execve系统调用从内核态返回,跳转到可执行程序的入口地址开始执行时,由于页表均被清空,因此会触发缺页异常

在处理缺页异常的do_no_page函数中,内核会检查出当前触发缺页异常的线性地址所在的vma区域存在文件映射(vm_file字段不为空),就可以将文件内容从磁盘加载进物理内存

4. 可执行程序的加载不是一次性完成的,而是由缺页异常根据需要以页为单位加载进内存的,一次只会加载一页

3.3 mmap系统调用的各种映射

3.3.1 私有匿名映射

1. 私有匿名映射常用于分配堆内存

2. 在mmap系统调用中,只需要在文件映射区分配一块虚拟内存并创建这块内存所对应的vm_area_struct结构即可。在创建vm_area_struct结构的过程中,

① 私有体现在vma区域vm_flags字段不具有VM_SHARED属性

② 匿名体现在vma区域不存在文件映射,即vm_file字段为空

3. 当访问到这块虚拟内存时,由于页表项无效,会触发缺页异常。由于vm_file字段为空,此时会经由do_no_page函数调用do_anonymous_page函数分配一页物理内存,并将该页清空,然后在页表中建立线性地址和物理地址的映射关系

3.3.2 私有文件映射

1. 私有文件映射常用于加载动态库

2. 在私有文件映射创建的vm_area_struct结构中,

① 私有体现在vma区域vm_flags字段不具有VM_SHARED属性

② 文件体现在vma区域存在文件映射,即vm_file字段不为空

因此当访问到这块虚拟内存时,会触发缺页异常,并进行物理内存分配与磁盘文件加载操作

3. 私有文件映射的只读页是多进程共享的

① 在Linux操作系统中使用inode结构描述一个磁盘上的文件,inode结构与磁盘上的文件是一一对应的。也就是说,对于同一个文件,整个内核只会有一个inode结构

② 在Linux操作系统中使用file结构描述一个打开的文件,file结构与进程相关,假设进程A和进程B都打开了同一文件,则两个进程都会持有一个file结构

③ file结构中有一个指针指向inode结构,从而将进程A和进程B各自持有的file结构与inode结构联系起来

inode结构中,有一个哈希表,以文件的页号为key,以物理内存页面为value。假设进程A打开了文件并读取了该文件的第4页,那么内核就会将4和对应的物理页面关联起来,并加入哈希表

⑤ 假设进程B也打开同样的文件,并且也要读取第4页。因为该文件的第4页已经被加载到物理内存中,所以不用再加载一次,只需要将进程B的线性地址与这个物理页建立映射即可,从而实现了只读页在多进程间的共享。场景如下图所示,

编程高手必学的内存知识05:深入理解页中断_第21张图片

说明:哈希表在现代的Linux内核中已经被优化为Radix tree和最小堆的数据结构,比哈希表有更好的时间性能

4. 私有文件映射的可写页,是每个进程都有一个独立的副本,创建副本的时机是写时复制

当进程对可写页进行写入操作时,因为映射的内存区域属性是私有的,所以内核会做一次写时复制,为写文件的进程单独地创建一份副本。因此一个进程在写文件时,并不会影响到其他进程的读操作

3.3.3 共享文件映射

1. 共享文件映射常用于多进程间通信

2. 在共享文件映射创建的vm_area_struct结构中,

① 共享体现在vma区域vm_flags字段具有VM_SHARED属性

② 文件体现在vma区域存在文件映射,即vm_file字段不为空

3. 与私有文件映射相比,共享文件映射对于可写的页面在写入时不进行写时复制即可

这样的话,无论何时,也无论是读还是写,多个进程在访问同一个文件的同一页时,访问的都是相同的物理页面

3.3.4 共享匿名映射

1. 共享匿名映射常用于父子进程间通信

2. 在共享匿名映射创建的vm_area_struct结构中,

① 共享体现在vma区域vm_flags字段具有VM_SHARED属性

② 文件体现在vma区域不存在文件映射,即vm_file字段为空

3. 共享匿名映射要解决的问题

① 导致问题的核心是mmap系统调用只是分配了一段虚拟内存,并没有分配物理内存

② 假设父进程还没有访问过这块虚拟内存时就发生了fork进程拷贝,此时在父子进程中,这段共享内存区域都处于未映射状态

假设父进程先访问到这块虚拟内存,内核通过缺页异常处理为其分配物理内存并建立映射关系。当子进程也访问到相同的线性地址时,由于子进程的页表也为映射,因此也会触发缺页异常。但是异常现场信息只包括当前进程和触发异常的线性地址,内核无法得知父进程已经将共享的物理内存准备好了

4. 基于虚拟文件解决问题

① 虚拟文件并不是真实在磁盘上存在的文件,而是由内核模拟出来的。但是虚拟文件也有自己的inode结构,这样一来,内核就能在创建共享匿名映射时创建一个虚拟文件,并将这个文件与映射区域的vma结构关联起来,这样后续的处理就和共享文件映射相同

② 在内核使用虚拟文件解决这个问题之前,早期的Linux内核并不支持共享匿名映射

你可能感兴趣的:(计算机体系结构,计算机体系结构)