用户虚拟地址空间的管理比内核地址空间的管理复杂:node
(如下默认系统有一个内存管理单元MMU,支持使用虚拟内存)程序员
各个进程的虚拟地址空间起始于地址0,延伸到TASK_SIZE - 1,其上是内核地址空间,用户程序只能访问整个地址空间的下半部分,不能访问内核部分。不管当前哪一个用户进程处于活动状态,虚拟地址空间内核部分的内容老是一样的,虚拟地址空间由许多不一样长度的段组成,用于不一样的目的,必须分别处理。缓存
虚拟地址空间包含了若干区域,其分布方式特定于体系结构,但它们有如下共同成分:安全
系统中,各个进程都具备一个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 }
(各个体系结构能够经过几个配置选项影响虚拟地址空间的布局,好比在不一样mmap区域布局之间选择,建立新内存映射时指定具体地址,寻找新的内存映射低端内存位置的方式等等。)app
进程有一个标志PF_RANDOMIZE,设置标志后,内核不会为栈和内存映射的起点选择固定位置,而是每次新进程启动时随机改变这些值的设置。(引入复杂性防止攻击)框架
图1为前述各部分在大多数体系结构里虚拟地址空间中的分布状况。dom
图1 进程的线性地址空间的组成编辑器
图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代码流程图
因为全部用户进程总的虚拟地址空间比可用的物理内存大得多,因此只有最经常使用的部分才与物理页帧关联。以文本编辑器为例,一般用户只关注文件结尾处,所以尽管整个文件都映射到内存中,实际上只用了几页来存储文件末尾的数据,至于文件开始处的数据,内核须要在地址空间保存相关信息(如数据在磁盘上的位置,以及须要数据时如何读取)。
内核提供一种数据结构创建虚拟地址空间的区域和相关数据所在位置之间的关联。
按需分配和填充页称为按需调页法(demand paging),它基于处理器和内核之间的交互,使用的数据结构如图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实例描述,进程各区域按两种方法排序:
用户虚拟地址空间中的每一个区域由开始和结束地址描述。现存的区域按起始地址以递增次序被纳入链表中。扫描链表找到与特定地址关联的区域,在有大量区域时是很是低效的操做(数据密集型的应用程序就是这样)。所以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_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 管理共享的相同映射所涉及各个数据结构的关联
内核提供了各类函数操做进程的虚拟内存区域,还负责在管理这些数据结构时进行优化。
图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,其处理逻辑与上文所述相似)
文件的内存映射能够认为是两个不一样的地址空间之间的映射,一个地址空间是用户进程的虚拟地址空间,另外一个是文件系统所在的地址空间。
在内核建立一个映射时,必须创建两个地址空间之间的关联,以支持两者以读写请求的形式通讯。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的概念。
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可设置的标志集以下:
sys_map在大多数体系结构上行为相似,最终都会进入do_mmap_pgoff函数,mmap2系统调用入口是sys_mmap2,它会当即将工做委托给do_map2,内核在此找到所处理文件的全部特征数据,随后工做委托给do_mmap_pgoff。
do_mmap_pgoff与体系结构无关,图10为它的代码流程图。
图10 do_mmap_pgoff代码流程图
内核维护了进程用于映射的页数目统计。因为能够限制进程的资源用量,内核必须始终确保资源使用不超出容许值。对于每一个进程能够建立的映射,还有一个最大数目的限制。
内核必须进行普遍的安全性和合理性检查,以防应用程序设置无效参数或可能影响系统稳定性的参数。例如,映射不能比虚拟地址空间更大,也不能扩展到超出虚拟地址空间的边界。
从虚拟地址空间删除现存映射,必须使用munmap系统调用,它须要两个参数:解除映射区域的起始地址和长度,sys_munmap是该系统调用的入口,sys_munmap系统调用将工做委托给do_munmap函数,其代码流程图如图11所示。
图11 do_munmap代码流程图
普通的映射将文件中一个连续的部分映射到虚拟内存中一个一样连续的部分。若是须要将文件的不一样部分以不一样顺序映射到虚拟内存的连续区域中,则使用非线性映射。sys_remap_file_pages系统调用专门用于该目的,它能够将现存映射移动到虚拟内存中的一个新的位置。其代码流程图如图12所示。
图12 sys_remap_file_pages代码流程图
内核经过页表创建了虚拟和物理地址之间的关系,内核还完成了进程的一个内存区域与其虚拟内存页地址之间的切换。除此之外,内核还采用了一种逆向映射方法(一些附加的数据结构和函数),创建页和全部映射了该页的位置之间的关联。
内核使用了简洁的数据结构,以最小化逆向映射的管理开销。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。分别调用的两个函数,其目的都是肯定有多少地方在使用一个页,但因为底层数据结构的不一样,两者采用了不一样的方法。
堆是进程中用于动态分配变量和数据的内存区域,堆的管理对应用程序员不是直接可见的。
堆是一个连续的内存区域,在扩展时自下至上增加。mm_struct结构包含了堆在虚拟地址空间中的起始和当前结束地址(start_brk和brk)。
brk系统调用只须要一个参数,用于指定堆在虚拟地址空间中新的结束地址,其入口是sys_brk函数,代码流程图如图13所示。
图13 sys_brk代码流程图
若是进程访问的虚拟地址空间部分还没有与页帧关联,处理器自动地引起一个缺页异常,由内核处理此异常。图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信号发送给进程。
确认缺页异常是从容许的地址触发后,内核必须肯定将所需数据读取到物理内存的适当方法。该任务委托给函数handle_mm_fault,它不依赖于底层体系结构,而是在内存管理的框架下、独立于系统而实现。该函数确认在各级页目录中,通向对应于异常地址的页表项的各个页目录项都存在。函数handle_pte_fault分析缺页异常的缘由。
若是页不在物理内存中,则必须区分下面3种状况:
按需分配页的工做委托给函数do_linear_fault,在转换一些参数以后,其他的工做委托给函数__do_fault,函数__do_fault的代码流程图如图16所示。
图16 __do_fault代码流程图
对给定涉及区域的vm_area_struct的读取操做,内核进行如下三步操做:
若是须要写访问,内核必须区分共享和私有映射。对私有映射,必须准备页的一份副本。
对于没有关联到文件做为后备存储器的页,须要调用do_anonymous_page进行映射。除了无需向页读入数据以外,该过程几乎与映射基于文件的数据没什么不一样。在highmem内存域创建一个新页,并清空其内容。接下来将页加入到进程的页表,并更新高速缓存或者MMU。
写时复制在do_wp_page中处理,主要步骤为:
因为异常地址与映射文件的内容并不是线性相关,所以必须从先前用pgoff_to_pte编码的页表项中,获取所需位置的信息(pte_to_pgoff分析页表项并获取所需的文件中的偏移量(以页为单位))。在得到文件内部的地址以后,读取所需数据相似于普通的缺页异常。所以内核将工做移交先前讨论的函数__do_page_fault,处理到此为止。
在访问内核地址空间时,缺页异常可能被如下条件触发:
前两种状况是真正的错误,内核必须对此进行额外的检查。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状态(出现了致命问题,给出各错误状态)。
内核常常须要从用户空间向内核空间复制数据(好比系统调用中采用指针间接传递冗长的数据结构),从内核空间向用户空间也有写数据需求。
因为用户空间程序不能访问内核地址,也没法保证用户空间中指针指向的虚拟内存页确实与物理内存页关联,因此不能只是传递并反引用指针。内核提供了几个标准函数,以处理内核空间和用户空间之间的数据交换。
图17是用户空间和内核空间之间交换数据的标准函数示例。图18是处理用户空间数据中的字符串标准函数的定义。
图17 用户空间和内核空间之间的交换数据的标准函数
图18 处理用户空间数据中的字符串的标准函数