进程地址空间

2015-6-17 chenhui 进程管理

在支持 MMU 的处理器中,每个程序都可以使用 4G 的虚拟内存并根据页表把不同的虚拟地址映射到不同的物理地址上去。在 Linux 中,每个用户进程都拥有自己的一套单独的页表,也就是说每个进程都拥有完全属于自己的 4G 虚拟内存,我们把这段虚拟内存中用户进程可用的部分虚拟内存叫做进程地址空间。


我们可以使用 struct mm_struct 结构来描述一个进程的地址空间,我们可以把他称为内存描述符



mmap :进程的地址空间是由一个个虚存区组成的,虚存区使用 struct vm_area_struct 来描述。一个进程的所有虚存区会连成一个队列,这个字段就是队列头。

mm_rb :有时候进程的虚存区会很多,那查找起来会比较麻烦,所以内核使用了红黑树来加速查找。这个字段就是树根。

mmap_cache :指向最后一个引用的虚存区。引入这个附加的字段是为了减少查找一个给定虚拟地址所在虚存区而花费的时间。为什么使用这个字段就能够减少查找时间呢?因为程序中有一个局部性原理,他放在这里就是:如果上次查找的虚拟地址属于某一个虚存区。那么本次查找的虚拟地址极有可能也属于这一个虚存区。所以说我们干脆先把上次查找到的虚存区记录下来。

get_unmapped_area :我们可以调用这个函数在进程地址空间中查找虚存区。

unmap_area :当释放一个虚存区时,可以调用这个函数。

mmap_base :第一个虚存区的虚拟地址。

free_area_cache :当我们需要在进程地址空间搜索一个虚存区时,他会从这个地址所处的虚存区开始搜索。

Pgd :我们说过,每个进程的进程地址空间都是通过页表实现的。这个字段就指向一级页表的基地址(虚拟地址)。

mmlist :系统内所有的内存描述符都根据这个字段连成一个链表。

total_vm、locked_vm、shared_vm、exec_vm :进程地址空间的大小(页框数)、锁住而不能换出的页的个数、共享文件内存映射中的页数、可执行内存映射的页数。

stack_vm、reserved_vm、def_flags、nr_ptes :用户态堆栈中的页数、在保留区中的页数或在特殊虚存区中的页数、虚存区默认的访问标志、进程的页表项数。

start_code、end_code、start_data、end_data :程序代码所在的虚存区的起始地址、程序代码所在的虚存区的结束地址、程序的已初始化数据所在的虚存区的起始地址,大致和数据段对应、程序的已初始化数据所在的虚存区的结束地址,大致和数据段对应。

start_brk、brk、start_stack :堆的超始地址、堆的当前最后地址、用户态堆栈的起始地址。

arg_start、arg_end、env_start、env_end :命令行参数的起始地址、命令行参数的结束地址、环境变量的起始地址、环境变量的结束地址。


虚存区

所谓的虚存区,就是虚拟内存区域。比如说0x50000000~0x60000000 这段虚拟内存区域就可以是一个虚存区。虚存区使用 struct vm_area_struct 结构来描述。



vm_mm :虚存区所属的进程地址空间。

vm_start :虚存区开始地址。

vm_end :虚存区结束地址。

vm_next :所有的虚存区通过这个字段形成一个队列。

vm_page_prot :虚存区内的页框的访问权限。

vm_rb :用于插入红黑树。

vm_pgoff :当虚存区映射了文件时,这个字段记录了文件开始地址离虚存区的开始地址的偏移量。

vm_file :当虚存区映射了文件时,他指向那个映射的文件。

 

进程的进程地址空间是由虚存区组成的,比如说我们需要增加进程地址空间,那么就要额外申请一个虚存区并把他插入进程地址空间。如果说我们需要减少进程的地址空间,那么我们就要根据减少的位置缩小虚存区的大小或者把一个虚存区分成两个虚存区。


内核为了在虚存区过多时能够快速检索虚存区,他使用红黑树来管理进程地址空间下所有的虚存区,红黑树的树根存放在虚存区所属进程地址空间的内存描述符的 mm_rb 字段,而每个节点则对应每个虚存区的 vm_rb 字段。


关于红黑树的原理和操作,他实际上是数据结构的东西,这里就不讲了。

我们可以调用 find_vma() 来查找一个地址所属的虚存区。



mm :欲查找的进程地址空间的内存描述符。

addr :目标地址。


398 行,取出上次查找到的虚存区。由于局部性原理,使用他可以加快查找速度。

399 行,如果说局部性原理应验,即目标地址就在上次查找的虚存区中,那么就不会进入 if 代码段,直接在 423 行返回虚存区。

400 行,到了这里就说明,局部性原理未应验。

402 行,取出红黑树根,因为我们要使用红黑树来查找虚存区。

405~418 行,这里就是在红黑树中查找虚存区了。他先在 408 行取出第一个节点对应的虚存区,并在 411~414 行检查当前虚存区是否包含了目标地址,如果是则跳出循环在 423 行返回虚存区;如果不是则要看目标地址是在虚存区之前还是虚存区之后,如果是在虚存区之前,那么就在 415 行取出左子树(红黑树的特点之一:值比较小的放在左子树),否则就在 417 行取出右子树(值比较大的放在右子树),然后继续循环。


我们可以调用 do_mmap() 来分配虚存区并扩充进程地址空间。



fileoffset :如果新的虚存区将把一个文件映射到内存,则使用文件描述符指针file和文件偏移量offset。当不需要内存映射时,fileoffset都会为空。

addr :这个虚拟地址指定从休息开始查找一个空闲的区间。

len :虚存区的长度。

prot :这个参数指定这个虚存区所包含页的访问权限。

flag :这个参数指定虚存区的其他标志。


73~74 行,检查虚存区长度是否正确。不正确则退出。

75~76 行,检查 offset 是否对齐页,对齐的话,就调用 do_mmap_pgoff 执行真正的操作



912 行,虚存区长度为 0,那就没什么好申请的,退出。



920~922 行,虚存区长度按页对齐。如果对齐后的长度为 或者他超出了 TASK_SIZETASK_SIZE是用户空间的最大值,do_mmap是为进程分配地址空间,所以不得超过此值),那么就报错退出。

927~928 行,若进程映射了过多的虚存区,那么就报错退出。

930~932 行,搜索进程的整个地址空间,得到一个足够容纳我们的要求的虚存区的地址区间的开始地址,并检查这个虚存区的开始地址是否按页对齐。

953 行,如果虚存区映射文件,则取出文件的索引节点。这里不介绍文件的映射,所以下面会忽略掉文件映射的代码。



这段代码主要是映射文件用的,不多说。



009~014 行,检查红黑树,试图找到结束地址高于addr的第一个区间;如果找到了一个虚拟区,说明addr所在的虚拟区已经在使用,也就是已经有映射存在,因此要调用do_munmap()把这个老的虚拟区从进程地址空间中撤销,如果撤销不成功,就返回一个负数;如果撤销成功,就继续查找,直到在红黑树中找不到addr所在的虚拟区,并继续下面的检查。

38~42 行,为新虚存区分配虚存区描述符。

44~50 行,初始化新虚存区描述符。



88 行,把新虚存区插入进程地址空间。



104 行,新的虚存区已经插入成功,我们要把新虚存区的页数加到进程地址空间的总页数上去。

118~131 行,失败处理。


实际上我们还有很多对虚存区的操作函数没讲,但他们大多就是对红黑树的操作。懂红黑树的人看那些代码无压力,不懂红黑树的人我写出来估计也不懂。所以我就不讲了。

至于为什么普通虚存区不映射物理内存,很简单,我们先不映射,到时候访问的时候会触发缺页异常,到时候内核会再为进程地址空间映射物理内存。



发表评论:

Copyright ©2015-2016 freehui All rights reserved