深刻Linux内核架构——进程虚拟内存

  • 逆向映射(reverse mapping)技术有助于从虚拟内存页跟踪到对应的物理内存页;
  • 缺页处理(page fault handling)容许从块设备按需读取数据填充虚拟地址空间。

1、简介

用户虚拟地址空间的管理比内核地址空间的管理复杂:node

  • 每一个应用程序都有自身的地址空间,与全部其余应用程序分隔开;
  • 一般在巨大的线性地址空间中,只有不多的段可用于各个用户空间进程,这些段彼此有必定的距离,内核须要一些数据结构,来有效地管理这些(随机)分布的段;
  • 地址空间只有极小的一部分与物理内存页直接关联,不常用的部分,则仅当必要时与页帧关联;
  • 内核信任自身,但没法信任用户进程,所以,各个操做用户地址空间的操做都伴随有各类检查,以确保程序的权限不会超出应有的限制,进而危及系统的稳定性和安全性;
  • fork-exec模型在UNIX操做系统下用于产生新进程,若是实现得较为粗劣,模型功能不强大,内核则必须借助于一些技巧,来尽量高效地管理用户地址空间。

(如下默认系统有一个内存管理单元MMU,支持使用虚拟内存)程序员

2、进程虚拟地址空间

各个进程的虚拟地址空间起始于地址0,延伸到TASK_SIZE - 1,其上是内核地址空间,用户程序只能访问整个地址空间的下半部分,不能访问内核部分。不管当前哪一个用户进程处于活动状态,虚拟地址空间内核部分的内容老是一样的,虚拟地址空间由许多不一样长度的段组成,用于不一样的目的,必须分别处理。缓存

一、进程地址空间的布局

虚拟地址空间包含了若干区域,其分布方式特定于体系结构,但它们有如下共同成分:安全

  • 当前运行代码的二进制代码(其代码一般称为text,所处的虚拟内存区域称为text段);
  • 程序使用的动态库的代码;
  • 存储全局变量和动态产生的数据的堆;
  • 用于保存局部变量和实现函数/过程调用的栈;
  • 环境变量和命令行参数的段;
  • 将文件内容映射到虚拟地址空间中的内存映射。

系统中,各个进程都具备一个struct mm_struct实例,实例中保存了进程的内存管理信息,能够经过task_struct访问。数据结构

 1 struct mm_struct {
 2 ...
 3     unsigned long (*get_unmapped_area) (struct file *filp,
 4     unsigned long addr, unsigned long len,
 5     unsigned long pgoff, unsigned long flags);
 6 ...
 7     unsigned long mmap_base; /* mmap区域的基地址 */
 8     unsigned long task_size; /* 进程虚拟内存空间的长度 */
 9 ...
10     unsigned long start_code, end_code, start_data, end_data;
11     unsigned long start_brk, brk, start_stack;
12     unsigned long arg_start, arg_end, env_start, env_end;
13 ...
14 }
  • 可执行代码占用的虚拟地址空间区域,开始和结束部分分别经过start_code和end_code标记;start_data和end_data标记了包含已初始化数据的区域。(ELF二进制文件映射到地址空间后,这些区域长度再也不改变)
  • 堆的起始地址保存在start_brk,brk表示堆区域当前结束的地址(堆的起始地址在进程生命周期中不变,但其长度会变化,意味着brk的值会变化)。
  • 参数列表和环境参数分别由arg_start和arg_end、env_start和env_end描述,两个区域都位于栈中最高的区域。
  • mmap_base表示虚拟地址空间中用于内存映射的其实地址。
  • task_size存储了对应进程的地址空间长度(一般为TASK_SIZE)

(各个体系结构能够经过几个配置选项影响虚拟地址空间的布局,好比在不一样mmap区域布局之间选择,建立新内存映射时指定具体地址,寻找新的内存映射低端内存位置的方式等等。)app

进程有一个标志PF_RANDOMIZE,设置标志后,内核不会为栈和内存映射的起点选择固定位置,而是每次新进程启动时随机改变这些值的设置。(引入复杂性防止攻击)框架

1为前述各部分在大多数体系结构里虚拟地址空间中的分布状况。dom

1 进程的线性地址空间的组成编辑器

  • text段映射到虚拟地址空间中的方式由ELF标准肯定,每一个体系结构都指定了一个特定的起始地址,IA-32系统起始于0x8048000,在text段的起始地址与最低可用地址之间有大约128MB间距,用于捕获NULL指针。堆紧接着text段,向上增加。
  • 栈起始于STACK_TOP(大多数体系结构为TASK_SIZE),若是设置了PF_RANDOMIZE则会减小一个随机量,进程的参数列表和环境变量都是栈的初始数据。
  • 用于内存映射的区域起始于mm_struct->mmap_base,一般设置为TASK_UNMAPPED_BASE(几乎全部状况下其值为TASK_SIZE/3)。

1所示的这种经典布局意味着堆最高只能到mmap的起始位置(IA-32中一般大小为1G),所以出现了图2所示的新的布局。新的布局中,使用固定值限制栈的最大长度,内存映射区域能够在栈末端下方开始,自顶向下扩展,堆依然处于虚拟地址空间中较低位置向上增加,所以mmap区域和堆能够相对扩展,直至耗尽虚拟地址空间中的剩余区域。(为确保栈和mmap区域不冲突,二者之间设置了一个安全隙)函数

 

2 mmap区域自顶向下扩展时IA-32系统虚拟地址空间的布局

二、创建布局

在使用load_elf_binary装载一个ELF二进制文件时,将建立进程的地址空间(exec系统调用中实现)。图3为load_elf_binary的代码流程图。

3 load_elf_binary代码流程图

  • 若是全局变量randomize_va_space设置为1,则启用地址空间随机化机制,一般状况下是启用的;
  • 而后由arch_pick_mmap_layout完成选择布局的工做,若是对应体系结构没有提供一个具体的函数,则使用内核的默认例程;
  • 最后setup_arg_pages函数负责在适当的位置建立栈,该函数须要栈顶位置做为参数,栈顶由特定于体系结构的常数STACK_TOP给出,然后调用randomize_stack_top,确保在启用地址空间随机化的状况下,对该地址进行随机偏移。

3、内存映射的原理

因为全部用户进程总的虚拟地址空间比可用的物理内存大得多,因此只有最经常使用的部分才与物理页帧关联。以文本编辑器为例,一般用户只关注文件结尾处,所以尽管整个文件都映射到内存中,实际上只用了几页来存储文件末尾的数据,至于文件开始处的数据,内核须要在地址空间保存相关信息(如数据在磁盘上的位置,以及须要数据时如何读取)。

内核提供一种数据结构创建虚拟地址空间的区域和相关数据所在位置之间的关联。

按需分配和填充页称为按需调页法(demand paging),它基于处理器和内核之间的交互,使用的数据结构如图4所示。

4 按需调页期间各数据结构的交互

  • 进程试图访问用户地址空间中的一个内存地址,但使用页表没法肯定物理地址(物理内存中没有关联页);
  • 处理器接下来触发一个缺页异常,发送到内核;
  • 内核会检查负责缺页区域的进程地址空间数据结构,找到适当的后备存储器,或者确认该访问其实是不正确的;
  • 分配物理内存页,并从后备存储器读取所需数据填充;
  • 借助于页表将物理内存页并入到用户进程的地址空间,应用程序恢复执行。

4、数据结构

与内存布局相关的信息在struct mm_struct中为:

1 struct mm_struct {
2     struct vm_area_struct * mmap; /* 虚拟内存区域列表 */
3     struct rb_root mm_rb;
4     struct vm_area_struct * mmap_cache; /* 上一次find_vma的结果 */
5 ...
6 }

一、树和链表

每一个区域都经过一个vm_area_struct实例描述,进程各区域按两种方法排序:

  • 在一个单链表上(开始于mm_struct->mmap);
  • 在一个红黑树中,根结点位于mm_rb。

用户虚拟地址空间中的每一个区域由开始和结束地址描述。现存的区域按起始地址以递增次序被纳入链表中。扫描链表找到与特定地址关联的区域,在有大量区域时是很是低效的操做(数据密集型的应用程序就是这样)。所以vm_area_struct的各个实例还经过红黑树管理,能够显著加快扫描速度。

增长新区域时,内核首先搜索红黑树,找到恰好在新区域以前的区域。所以,内核能够向树和线性链表添加新的区域,而无需扫描链表。最后,内存中的状况如图5所示(树的表示只是象征性的,没有反映真实布局的复杂性)。

5 vm_area_struct实例与进程的虚拟地址空间关联

二、虚拟内存区域的表示

每一个区域都是一个vm_area_struct实例。其结构体以下所示:

 1 vm_area_struct {
 2     struct mm_struct * vm_mm;  //反向指针,指向该区域所属的mm_struct实例
 3     unsigned long vm_start;  //该区域在用户空间中的起始地址
 4     unsigned long vm_end;  //该区域在用户空间中的结束地址
 5 /* 各进程的虚拟内存区域链表,按地址排序 */
 6     struct vm_area_struct *vm_next;    //进程全部vm_area_struct实例的链表指针
 7     pgprot_t vm_page_prot;  //存储该区域的访问权限
 8     unsigned long vm_flags;  //描述该区域的一组标志,以下列出
 9     struct rb_node vm_rb; //进程全部vm_area_struct实例的红黑树集成
10 /*
11 对于有地址空间和后备存储器的区域来讲,
12 shared链接到address_space->i_mmap优先树,
13 或链接到悬挂在优先树结点以外、相似的一组虚拟内存区域的链表,
14 或链接到address_space->i_mmap_nonlinear链表中的虚拟内存区域。 */
15     union {
16         struct {
17             struct list_head list;
18             void *parent; /* 与prio_tree_node的parent成员在内存中位于同一位置 */
19             struct vm_area_struct *head;
20         } vm_set;
21         struct raw_prio_tree_node prio_tree_node;
22     } shared;
23 /*
24 *在文件的某一页通过写时复制以后,文件的MAP_PRIVATE虚拟内存区域可能同时在i_mmap树和
25 *anon_vma链表中。MAP_SHARED虚拟内存区域只能在i_mmap树中。
26 *匿名的MAP_PRIVATE、栈或brk虚拟内存区域(file指针为NULL)只能处于anon_vma链表中。
27 */
28     struct list_head anon_vma_node;  //链表元素,用于管理源自匿名映射(anonymous mapping)的共享页
29     struct anon_vma *anon_vma;  //用于管理源自匿名映射(anonymous mapping)的共享页
30 /* 用于处理该结构的各个函数指针。 */
31     struct vm_operations_struct * vm_ops; //指向多个方法的集合,用于在区域上执行各类操做
32 /* 后备存储器的有关信息: */
33     unsigned long vm_pgoff;  //用于只映射文件部份内容时指定文件映射偏移量,单位是PAGE_SIZE,不是PAGE_CACHE_SIZE
34     struct file * vm_file;  //映射到的文件(多是NULL),指向file实例
35     void * vm_private_data;  //vm_pte(即共享内存),用于存储私有数据,取决于映射类型
36 };
  • VM_READ、VM_WRITE、VM_EXEC、VM_SHARED分别指定了页的内容是否能够读、写、执行,或者由几个进程共享;
  • VM_MAYREAD、VM_MAYWRITE、VM_MAYEXEC、VM_MAYSHARE用于肯定是否能够设置对应的VM_*标志,这是mprotect系统调用所须要的;
  • VM_GROWSDOWN和VM_GROWSUP表示一个区域是否能够向下或向上扩展(到更低或更高的虚拟地址),因为堆自下而上增加,其区域须要设置VM_GROWSUP,VM_GROWSDOWN对栈设置,该区域自顶向下增加;
  • 若是区域极可能从头至尾顺序读取,则设置VM_SEQ_READ,VM_RAND_READ指定了读取多是随机的,这两个标志用于“提示”内存管理子系统和块设备层,以优化其性能。

若是设置了VM_DONTCOPY,则相关的区域在fork系统调用执行时不复制。

VM_DONTEXPAND禁止区域经过mremap系统调用扩展。

若是区域是基于某些体系结构支持的巨型页,则设置VM_HUGETLB标志。

VM_ACCOUNT指定区域是否被纳入overcommit特性的计算中。这些特性以多种方式限制内存分配。

三、优先查找树

优先查找树(priority search tree)用于创建文件中的一个区域与该区域映射到的全部虚拟地址空间之间的关联。

1)附加的数据结构

每一个打开文件(和每一个块设备,由于这些也能够经过设备文件进行内存映射)都表示为struct file的一个实例,该结构包含了一个指向地址空间对象struct address_space的指针,该对象是优先查找树(prio tree)的基础,而文件区间与其映射到的地址空间之间的关联即经过优先树创建。

此外,每一个文件和块设备都表示为struct inode的一个实例,struct file是经过open系统调用打开的文件的抽象,与此相反,inode表示文件系统自身中的对象。inode是一个特定于文件的数据结构,file是特定于给定进程的,内存中各结构之间的关联如图6所示。

6 借助优先树跟踪文件给定区间所映射到的虚拟地址空间

地址空间是优先树的基本要素,优先树包含了全部相关的vm_area_struct实例,描述了与inode关联的文件区间到一些虚拟地址空间的映射。每一个struct vm_area的实例都包含了一个指向所属进程的mm_struct的指针,所以创建关联。此外,vm_area_struct还能够经过以i_mmap_nonlinear为表头的双链表与一个地址空间关联,这是非线性映射(nonlinear mapping)所需。

2)优先树的表示

优先树用来管理表示给定文件中特定区间的全部vm_area_struct实例,它不只可以处理重叠区间,还处理相同的文件区间。对于重叠区间,区间的边界提供了一个惟一的索引,将各个区间存储在一个惟一的树结点中,若是一个区间彻底包含在另外一个区间只会中,那么前者是后者的子结点;对于相同区间,能够将一个vm_set的链表与一个优先树结点关联起来,如图7所示。

 

7 管理共享的相同映射所涉及各个数据结构的关联

5、对区域的操做

内核提供了各类函数操做进程的虚拟内存区域,还负责在管理这些数据结构时进行优化。

8 对区域的操做

如图8所示:

  • 若是一个新区域紧接着现存区域先后直接添加,内核将涉及的数据结构合并为一个(前提是涉及的全部区域的访问权限相同,并且是从同一后备存储器映射的连续数据);
  • 若是在区域的开始或结束处进行删除,则必须据此截断现存的数据结构;
  • 若是删除两个区域之间的一个区域,那么一方面须要减少现存数据结构的长度,另外一方面须要为造成的新区域建立一个新的数据结构。

一、将虚拟地址关联到区域

经过虚拟地址,find_vma能够查找用户地址空间中结束地址在给定地址以后的第一个区域,即知足addr小于vm_area_struct->vm_end条件的第一个区域。该函数的参数不只包括虚拟地址(addr),还包括一个指向mm_struct实例的指针,后者指定了扫描哪一个进程的地址空间。

二、区域合并

如图8所示,在新区域被加到进程的地址空间时,内核会检查它是否能够与一个或多个现存域合并,经过函数vm_merge实现,该函数的参数包括相关进程的地址空间实例,紧接着新区域以前的区域,该区域在红黑查找树中的父结点,新区域的开始地址、结束地址、标志。若是该区域属于一个文件映射,有一个指向表示该文件的file实例的指针,和指定了映射在文件数据内的偏移量。

三、插入区域

insert_vm_struct是内核用于插入新区域的标准函数。实际工做委托给两个辅助函数,如图9所示。

9 insert_vm_struct代码流程图

首先调用find_vma_prepare,经过新区域的起始地址和涉及的地址空间(mm_struct),获取相关信息;而后使用vma_link将新区域合并到该进程现存的数据结构中。

四、建立区域

在向数据结构插入新的内存区域以前,内核必须确认虚拟地址空间中有足够的空闲空间,可用于给定长度的区域,该工做分配给get_unmapped_area辅助函数完成。

首先检查是否设置了MAP_FIXED标志,该标志表示映射将在固定地址建立。假若如此,内核只会确保该地址知足对齐要求(按页),并且所要求的区间彻底在可用地址空间内。

若是没有指定区域位置,内核将调用arch_get_unmapped_area在进程的虚似内存区中查找适当的可用区域。若是指定了一个特定的优先选用(与固定地址不一样)地址,内核会检查该区域是否与现存区域重叠。若是不重叠,则将该地址做为目标返回。不然,内核必须遍历进程中可用的区域,设法找到一个大小适当的空闲区域。这样作时,内核会检查是否可以使用前一次扫描时缓存的区域。若是搜索持续到用户地址空间的末端(TASK_SIZE),仍然没有找到适当的区域,则内核返回一个-ENOMEM错误。(若是mmap区域自顶向下扩展,那么分配新区域的函数是arch_get_unmapped_area_topdown,其处理逻辑与上文所述相似)

6、地址空间

文件的内存映射能够认为是两个不一样的地址空间之间的映射,一个地址空间是用户进程的虚拟地址空间,另外一个是文件系统所在的地址空间。

在内核建立一个映射时,必须创建两个地址空间之间的关联,以支持两者以读写请求的形式通讯。vm_operations_struct结构即用于完成该工做,它提供了一个操做,来读取已经映射到虚拟地址空间、但其内容还没有进入物理内存的页。该操做不了解映射类型或其性质的相关信息,但address_space结构中包含了有关映射的附加有信息。

vm_operations_struct和address_space之间的联系不是静态的,它们使用内核为vm_operations_struct提供的标准链接,几乎全部文件系统都采用这种方式。

1 struct vm_operations_struct generic_file_vm_ops = {
2     .fault = filemap_fault,
3 };

filemap_fault的实现使用了相关映射的readpage方法,也采用了address_space的概念。

7、内存映射

C标准库中经过mmap函数创建文件到内存的映射,在内核一端,提供mmap和mmap2两个系统调用在用户虚拟地址空间中的pos位置,创建一个长度为len的映射,其访问权限经过prot定义。flags是一组标志集,fd是文件描述符标识。mmap和mmap2之间的差异在于偏移量的语义(off),前者单位是字节,后者单位是页。

asmlinkage unsigned long sys_mmap{2}(unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flags,
unsigned long fd, unsigned long off)

 

 munmap系统调用用于删除映射(此时不须要文件偏移量)。

一、建立映射

mmap和mmap2可设置的标志集以下:

  • MAP_FIXED指定除了给定地址以外,不能将其余地址用于映射。若是没有设置该标志,内核能够在受阻时随意改变目标地址;
  • 若是一个对象(一般是文件)在几个进程之间共享时,则必须使用MAP_SHARED;
  • MAP_PRIVATE建立一个与数据源分离的私有映射,对映射区域的写入操做不影响文件中的数据;
  • MAP_ANONYMOUS建立与任何数据源都不相关的匿名映射,fd和off参数被忽略。此类映射可用于为应用程序分配相似malloc所用的内存。
  • prot可指定PROT_EXEC、PROT_READ、PROT_WRITE、PROT_NONE值的组合,来定义访问权限。

sys_map在大多数体系结构上行为相似,最终都会进入do_mmap_pgoff函数,mmap2系统调用入口是sys_mmap2,它会当即将工做委托给do_map2,内核在此找到所处理文件的全部特征数据,随后工做委托给do_mmap_pgoff。

do_mmap_pgoff与体系结构无关,图10为它的代码流程图。

10 do_mmap_pgoff代码流程图

  • do_mmap_pgoff函数分为两个部分,一个部分完全检查用户应用程序传递的参数,第二个考虑大量特殊状况。
  • 它首先调用get_unmapped_area函数,在虚拟地址空间中找到一个适当的区域用于映射;
  • 而后检查参数,设置所须要的标志;
  • 最后将工做委托给mmap_region,找到映射的起始地址,在这过程当中,会对代码执行路径中的不一样位置进行几回检查,若是某一次失败则结束操做并返回一个操做代码。

内核维护了进程用于映射的页数目统计。因为能够限制进程的资源用量,内核必须始终确保资源使用不超出容许值。对于每一个进程能够建立的映射,还有一个最大数目的限制。

内核必须进行普遍的安全性和合理性检查,以防应用程序设置无效参数或可能影响系统稳定性的参数。例如,映射不能比虚拟地址空间更大,也不能扩展到超出虚拟地址空间的边界。

二、删除映射

从虚拟地址空间删除现存映射,必须使用munmap系统调用,它须要两个参数:解除映射区域的起始地址和长度,sys_munmap是该系统调用的入口,sys_munmap系统调用将工做委托给do_munmap函数,其代码流程图如图11所示。

11 do_munmap代码流程图

  • 内核首先调用find_vma_prev,以找到解除映射区域的vm_area_struct实例,返回指向前一个区域的指针;
  • 若是解除映射区域的起始地址与find_vma_prev找到的区域起始地址不一样,则只解除部分映射,而不是整个映射区域(此时须要经过区域分裂将映射划分为几个部分);
  • 若是解除映射的部分区域的末端与原区域末端并不重合,那么原区域后部仍然有一部分未解除映射,所以须要对这部分也进行分裂;
  • 接下来调用detach_vmas_to_be_unmapped,列出全部须要解除映射的区域;
  • 最后调用unmap_region从页表删除与映射相关的全部项,完成后将相关项从TLB中移除,用用remove_vma_list释放vm_area_struct实例占用的空间,完成从内核中删除映射的工做。

三、非线性映射

普通的映射将文件中一个连续的部分映射到虚拟内存中一个一样连续的部分。若是须要将文件的不一样部分以不一样顺序映射到虚拟内存的连续区域中,则使用非线性映射。sys_remap_file_pages系统调用专门用于该目的,它能够将现存映射移动到虚拟内存中的一个新的位置。其代码流程图如图12所示。

12 sys_remap_file_pages代码流程图

  • 内核首先检查全部标志,并确保从新映射的范围有效后,经过find_vma选中目标区域的vm_area_struct实例,若是目标区域此前没有进行过非线性映射,则vm_area_struct->vm_flags不会设置VM_NONLINEAR标志,此时须要从优先树移除该线性映射,并将其插入到非线性列表中;
  • 而后由populate_range设置修改过的页帧项;
  • 最后一步是读入映射的页(在须要的状况下才会读入,经过设置MAP_NONBLOCK标志可阻止读入)。

8、反向映射

  • 在映射一页时,它关联到一个进程,但不必定处于使用中;
  • 对页的引用次数代表页使用的活跃程度,为肯定该数目,内核首先必须创建页和全部使用者之间的关联,接下来必须借助于一些技巧来计算出页使用的活跃程度。

内核经过页表创建了虚拟和物理地址之间的关系,内核还完成了进程的一个内存区域与其虚拟内存页地址之间的切换。除此之外,内核还采用了一种逆向映射方法(一些附加的数据结构和函数),创建页和全部映射了该页的位置之间的关联。

一、数据结构

内核使用了简洁的数据结构,以最小化逆向映射的管理开销。page结构包含了一个用于实现逆向映射的成员。

1 struct page {
2 ....
3     atomic_t _mapcount; // 内存管理子系统中映射的页表项计数,用于表示页是否已经映射,还用于限制逆向映射搜索。
4 ...
5 };

_mapcount代表共享该页的位置的数目。计数器的初始值为1。在页插入到逆向映射数据结构时,计数器赋值为0。页每次增长一个使用者时,计数器加1。这使得内核可以快速检查在全部者以外该页有多少使用者。此外,经过在优先查找树中嵌入属于非匿名映射的每一个区域和指向内存中同一页的匿名区域的链表即可在给定的page实例中找到全部映射了该物理内存页的位置。

内核在实现逆向映射时采用的技巧是,不直接保存页和相关的使用者之间的关联,而只保存页和页所在区域之间的关联。包含该页的全部其余区域(进而全部的使用者)均可以找到。该方法又名基于对象的逆向映射(object-based reverse mapping),由于没有存储页和使用者之间的直接关联。相反,在二者之间插入了另外一个对象(该页所在的区域)。

 二、创建逆向映射

在建立逆向映射时,有必要区分两个备选项:匿名页和基于文件映射的页。

1)匿名页

将匿名页插入到逆向映射数据结构中有两种方法。对新的匿名页必须调用page_add_new_anon_rmap;对已经有引用计数的页,则使用page_add_anon_rmap。这两个函数之间惟一的差异是,前者将映射计数器page->_mapcount设置为0(新初始化的页_mapcount的初始值为-1),后者将计数器加1。

2)基于文件映射的页

基于文件映射的页的逆向映射的创建比较简单,基本上,所须要作的只是对_mapcount变量加1(原子操做)并更新各内存域的统计量。

三、使用逆向映射

函数page_referenced使用了逆向映射方案所涉及的数据结构,统计了最近活跃地使用(即访问)了某个共享页的进程的数目,这不一样于该页映射到的区域数目。

该函数至关于一个多路复用器,对匿名页调用page_referenced_anon,而对基于文件映射的页调用page_referenced_file。分别调用的两个函数,其目的都是肯定有多少地方在使用一个页,但因为底层数据结构的不一样,两者采用了不一样的方法。

9、堆的管理

堆是进程中用于动态分配变量和数据的内存区域,堆的管理对应用程序员不是直接可见的。

堆是一个连续的内存区域,在扩展时自下至上增加。mm_struct结构包含了堆在虚拟地址空间中的起始和当前结束地址(start_brk和brk)。

brk系统调用只须要一个参数,用于指定堆在虚拟地址空间中新的结束地址,其入口是sys_brk函数,代码流程图如图13所示。

 

13 sys_brk代码流程图

  • brk机制不是独立的内核概念,是基于匿名映射实现的,以减小内部开销。
  • 内核首先检查用做brk值的新地址是否超出堆的限制;
  • 而后sys_brk将请求地址按页长度对其;
  • 接着若是须要收缩堆时将调用do_munmap,若是堆将要扩大,内核首先必须检查新的长度是否超出进程的最大堆长度限制,若超出限制,则什么也不作,不然,将扩大堆的工做交给do_brk并返回新的brk的值(实质上do_brk是do_mmap_pgoff的简化版本,它在用户地址空间中建立了一个匿名映射,省去了一些数处理)。

10、缺页异常的处理

若是进程访问的虚拟地址空间部分还没有与页帧关联,处理器自动地引起一个缺页异常,由内核处理此异常。图14给出了内核在处理缺页异常时,可能使用的各类代码路径的概述。

14 处理缺页异常的各类可能选项

缺页异常主要经过函数do_page_fault处理,其代码流程图如图15所示。

15 IA-32处理器上do_page_fault的代码流程图

do_page_fault须要传递两个参数:发生异常时使用中的寄存器集合(pt_regs *regs),提供异常缘由信息的错误代码(long error_code),具体检测关联流程如图15所示。若是页成功创建,则例程返回VM_FAULT_MINOR(数据已经在内存中)或VM_FAULT_MAJOR(数据须要从块设备读取)。内核接下来更新进程的统计量。但在建立页时,也可能发生异常。若是用于加载页的物理内存不足,内核会强制终止该进程,在最低限度上维持系统的运行。若是对数据的访问已经容许,但因为其余的缘由失败(例如,访问的映射已经在访问的同时被另外一个进程收缩,再也不存在于给定的地址),则将SIGBUS信号发送给进程。

11、用户空间缺页异常的校订

确认缺页异常是从容许的地址触发后,内核必须肯定将所需数据读取到物理内存的适当方法。该任务委托给函数handle_mm_fault,它不依赖于底层体系结构,而是在内存管理的框架下、独立于系统而实现。该函数确认在各级页目录中,通向对应于异常地址的页表项的各个页目录项都存在。函数handle_pte_fault分析缺页异常的缘由。

若是页不在物理内存中,则必须区分下面3种状况:

  • 若是没有对应的页表项(page_none),则内核必须从头开始加载该页,对匿名映射称之为按需分配(demand allocation),对基于文件的映射,则称之为按需调页(demand paging)。若是vm_ops中没有注册vm_operations_struct,则不适用上述作法。在这种状况下,内核必须使用do_anonymous_page返回一个匿名页;
  • 若是该页标记为不存在,而页表中保存了相关的信息,则意味着该页已经换出,于是必须从系统的某个交换区换入(换入或按需调页);
  • 非线性映射已经换出的部分不能像普通页那样换入,由于必须正确地恢复非线性关联,pte_file函数能够检查页表项是否属于非线性映射,do_nonlinear_fault在这种状况下可用于处理异常。

 一、按需分配/调页

按需分配页的工做委托给函数do_linear_fault,在转换一些参数以后,其他的工做委托给函数__do_fault,函数__do_fault的代码流程图如图16所示。

16 __do_fault代码流程图

对给定涉及区域的vm_area_struct的读取操做,内核进行如下三步操做:

  • 使用vm_area_struct->vm_file找到映射的file对象;
  • file->f_mapping中找到指向映射自身的指针;
  • 每一个地址空间都有特定的地址空间操做,从中选择readpage方法,使用mapping->a_ops->readpage(file, page)从文件中将数据传输到物理内存。

若是须要写访问,内核必须区分共享和私有映射。对私有映射,必须准备页的一份副本。

二、匿名页

对于没有关联到文件做为后备存储器的页,须要调用do_anonymous_page进行映射。除了无需向页读入数据以外,该过程几乎与映射基于文件的数据没什么不一样。在highmem内存域创建一个新页,并清空其内容。接下来将页加入到进程的页表,并更新高速缓存或者MMU。

三、写时复制

写时复制在do_wp_page中处理,主要步骤为:

  • 内核首先调用vm_normal_page,经过页表项找到页的struct page实例,本质上这个函数基于pte_pfn和pfn_to_page,这二者是全部体系结构都必须定义的。前者查找与页表项相关的页号,然后者肯定与页号相关的page实例;
  • 在用page_cache_get获取页以后,接下来anon_vma_prepare准备好逆向映射机制的数据结构,以接受一个新的匿名区域,因为异常的来源是须要将一个充满有用数据的页复制到新页,所以内核调用alloc_page_vma分配一个新页,cow_user_page接下来将异常页的数据复制到新页,进程随后能够对新页进行写操做;
  • 而后使用page_remove_rmap,删除到原来的只读页的逆向映射,新页添加到页表,此时也必须更新CPU的高速缓存;
  • 最后,使用lru_cache_add_active将新分配的页放置到LRU缓存的活动列表上,并经过page_add_anon_rmap将其插入到逆向映射数据结构。此后,用户空间进程能够向页写入数据。

四、获取非线性映射

因为异常地址与映射文件的内容并不是线性相关,所以必须从先前用pgoff_to_pte编码的页表项中,获取所需位置的信息(pte_to_pgoff分析页表项并获取所需的文件中的偏移量(以页为单位))。在得到文件内部的地址以后,读取所需数据相似于普通的缺页异常。所以内核将工做移交先前讨论的函数__do_page_fault,处理到此为止。

12、内核缺页异常

在访问内核地址空间时,缺页异常可能被如下条件触发:

  • 内核中的程序设计错误致使访问不正确的地址,这是真正的程序错误(这在稳定版本中应该永远都不会发生,但在开发版本中会偶尔发生);
  • 内核经过用户空间传递的系统调用参数,访问了无效地址;
  • 访问使用vmalloc分配的区域,触发缺页异常。

前两种状况是真正的错误,内核必须对此进行额外的检查。vmalloc的状况是致使缺页异常的合理缘由,须要加以校订。直至对应的缺页异常发生以前,vmalloc区域中的修改都不会传输到进程的页表,必须从主页表复制适当的访问权限信息。

在处理不是因为访问vmalloc区域致使的缺页异常时,异常修正(exception fixup)机制是一个最后手段。在某些时候,内核有很好的理由准备截取不正确的访问。例如,从用户空间地址复制做为系统调用参数的地址数据。

在向或从用户空间复制数据时,若是访问的地址在虚拟地址空间中不与物理内存页关联,则会发生缺页异常。当处于内核态时,该异常订单处理方式与用户状态稍有不一样。

每次发生缺页异常时,将输出异常的缘由和当前执行代码的地址。这使得内核能够编译一个列表,列出全部可能执行未受权内存访问操做的危险代码块。这个“异常表”在连接内核映像时建立,在二进制文件中位于__start_exception_table和__end_exception_table之间。每一个表项都对应于一个struct exception_table实例,该结构是体系结构相关的。

在异常处理过程当中,借助于函数fixup_exception搜索异常表,查找适当的匹配项;在找到修正例程时,将指令指针设置到对应的内存位置。在fixup_exception经过return返回后,内核将执行找到的例程。若是没有修正例程,就表示出现了一个真正的内核异常,在对search_exception_table(不成功的)调用以后,将调用do_page_fault来处理该异常,最终致使内核进入oops状态(出现了致命问题,给出各错误状态)。

十3、在内核和用户空间之间复制数据

内核常常须要从用户空间向内核空间复制数据(好比系统调用中采用指针间接传递冗长的数据结构),从内核空间向用户空间也有写数据需求。

因为用户空间程序不能访问内核地址,也没法保证用户空间中指针指向的虚拟内存页确实与物理内存页关联,因此不能只是传递并反引用指针。内核提供了几个标准函数,以处理内核空间和用户空间之间的数据交换。

17是用户空间和内核空间之间交换数据的标准函数示例。图18是处理用户空间数据中的字符串标准函数的定义。

 图17 用户空间和内核空间之间的交换数据的标准函数

 

18 处理用户空间数据中的字符串的标准函数

相关文章
相关标签/搜索