深刻理解计算机系统之虚拟存储器

http://blog.csdn.net/al_xin/article/details/38590931html

进程提供给应用程序的关键抽象:linux

 

  • 一个独立的逻辑控制流,它提供一个假象,好像咱们的程序独占地使用处理器
  • 一个私有的地址空间,它提供一个假象,好像咱们的程序独占地使用存储器系统.
 
 

虚拟存储器算法

虚拟存储器是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每一个进程提供了一个大的、一致的和私有的地址空间。经过一个很清晰的机制,虚拟存储器提供了三个重要的能力:数组

(1)它将主存当作是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据须要在磁盘和主存之间来回传送数据,经过这种方式,它高效地使用了主存。缓存

(2)它为每一个进程提供了一致的地址空间,从而简化了存储器管理。数据结构

(3)它保护了每一个进程的地址空间不被其余进程破坏app

 

物理和虚拟寻址dom

物理寻址函数

计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每字节都有一个惟一的物理地址(Physical Address,PA)。第一个字节的地址为0,接下来的字节的地址为1,再下一个为2,依此类推。给定这种简单的结构,CPU访问存储器的最天然的方式就是使用物理地址,咱们把这种方式称为物理寻址。工具

虚拟寻址

使用虚拟寻址时,CPU经过生成一个虚拟地址(Virtual Address,VA)来访问主存,这个虚拟地址在被送到存储器以前先转换成适当的物理地址。将一个虚拟地址转换为物理地址的任务叫作地址翻译(address translation)。就像异常处理同样,地址翻译须要CPU硬件和操做系统之间的紧密合做。CPU芯片上叫作存储器管理单元(Memory Management Unit,MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容是由操做系统管理。

 

地址空间

地址空间(adress space)是一个非整数地址的有序集合:{0,1,2,...}

若是地址空间中的整数是连续的,那么咱们说它是一个线性地址空间(linear address space)。在一个带虚拟存储器的系统中,CPU从一个有N = 2 ^ n个地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间(virtual address space):{0,1,2,3,...,N-1}

一个地址空间的大小是由表示最大地址所须要的倍数来描述的。例如,一个包含N=2^n个地址的虚拟地址空间叫作一个n位地址空间。如今系统典型地支持32位或者64位虚拟地址空间是。

一个系统还有一个物理地址空间(physical addresss space),它与系统中物理存储器的M字节相对应:{0,1,2,...M-1}

M不要求是2的幂,可是为了简化讨论,咱们假设M = 2 ^ m。

地址空间的概念是很重要的,由于它清楚地区分了数据对象(字节)和它们的属性(地址)。一旦认识到了这种区别,那么咱们就能够将其推广,容许每一个数据对象有多个独立的地址,其中每一个地址都选自一个不一样的地址空间(不连续的意思吗?)。这就是虚拟存储器的基本思想。主存中每一个字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址。(这段没怎么看懂~~)

 

虚拟存储器做为缓存的工具

概念上而言,虚拟存储器(VM)被组织为一个由存放在磁盘上N个连续的字节大小的单元组成的数组。每一个字节都有一个惟一的虚拟地址,这个惟一的虚拟地址是做为到数组的索引的。磁盘上的数组的内容被缓存在主存中。和存储器层次结构中其余缓存同样,磁盘(较低层)上的数据被分割成块,这些块做为磁盘和主存(较高层)之间的传输单元。VM系统经过将虚拟存储器分割称为虚拟页(Vitual Page,VP)的大小固定的块来处理这个问题。每一个虚拟页的大小为P = 2 ^ n字节。相似地,物理存储器被分割为物理页(Physical Page,PP),大小也为P字节(物理页也称为页帧(page frame))。

在任意时刻,虚拟页面的集合都分为三个不相交的子集:

 

  • 未分配的:VM系统还未分配(或者建立)的页。未分配的块没有任何数据和它们相关联,所以也就不占用任何磁盘空间。(没有调用malloc或者mmap的)
  • 缓存的:当前缓存在物理存储中的已分配页。(已经调用malloc和mmap的,在程序中正在引用的)
  • 未缓存的:没有缓存在物理存储器中的已分配页。(已经调用malloc和mmap的,在程序中尚未被引用的)

 

 

页表

同任何缓存同样,虚拟存储器系统必须有某种方法来断定一个虚拟页是否存放在DRAM中的某个地方。若是是,系统还必须肯定这个虚拟页存放在哪一个物理页中。若是不命中,系统必须判断这个虚拟页存放在磁盘的哪一个位置,在物理存储器中选择一个牺牲页,并将虚拟页从磁盘拷贝到DRAM中,替换这个牺牲页。

这些功能是由许多软硬件联合提供的,包括操做系统软件,MMU(存储器管理单元)中地址翻译硬件和一个存放在物理存储器中叫作页表(page table)的数据结构,页表将虚拟页映射到物理页。页表就是一个页表条目(Page Table Entry,PTE)的数组。

 

 

 

Linux虚拟存储器系统

linux为每一个进程维持了一个单独的虚拟地址空间


 

内核虚拟存储器包含内核中的代码和数据结构。内核虚拟存储器的某些区域被映射到全部进程共享的物理页面。例如,每一个进程共享内核的代码和全局数据结构

 

一、Linux虚拟存储器区域(Windows下也有区域的概念)

Linux将虚拟存储器组织成一些区域(也叫作段)的集合。一个区域(area)就是已经存在着的(已分配的)虚拟存储器的连续片(chunk),这些页是以某种方式相关联的。例如,代码段、数据段、堆、共享库段,以及用户栈都不一样的区域。每一个存在的虚拟页面保存在某个区域中,而不属于某个区域的虚拟页是不存在的,而且不能被进程引用。区域的概念很重要,由于它容许虚拟地址空间有间隙。内核不用记录那些不存在的虚拟页,而这样的页也不占用存储器。磁盘或者内核自己的任何额外资源

内核为系统中的每一个进程维护一个单独的任务结构(源代码中的task_struct)。任务结构中的元素包含或者指向内核运行该进程所须要的全部信息(例如,PID,指向用户栈的指针、可执行的目标文件的名字以及程序计数器)。

 

task_struct中的一个条目指向mm_struct,它描述了虚拟存储器中的当前状态。其中pgd指向第一级页表(页全局目录)的基址,而mmap指向一个vm_area_struct(区域结构)的链表,其中每一个vm_area_structs都描述了当前虚拟地址空间的一个区域(area)。当内核运行这个进程时,它就将pgd存放在CR3控制寄存器中。

一个具体区域结构包含下面的字段:

 

  • vm_start:指向这个区域的起始处。
  • vm_end:指向这个区域的结束处。
  • vm_prot:描述这个区域的内包含的全部页的读写许可权限。
  • vm_flags:描述这个区域内页面是与其余进程共享的,仍是这个进程私有的(还描述了其余一些信息)。
  • vm_next:指向链表中下一个区域结构。
 
 
 
存储器映射(Windows下也有相似的机制,名叫内存映射) 
Linux(以及其余一些形式的Unix)经过将一个虚拟存储器区域与一个磁盘上的对象(object)关联起来,以初始化这个虚拟存储器区域的内容,这个过程称为存储器映射(memory mapping)。虚拟存储器区域能够映射到两种类型的对象的一种:
(1) Unix文件上的普通文件:一个区域能够映射到一个普通磁盘文件的连续部分,例如一个可执行目标文件。文件区(section)被分红页大小的片,每一片包含一个虚拟页面的初始化内容。由于按需进行页面高度,因此这些虚拟页面没有实际进行物理存储器,直到CPU第一次引用到页面(即发射一个虚拟地址,落在地址空间这个页面的范围以内)。若是区域文件区要大,那么就用零来填充这个区域的余下部分。
 
(2) 匿名文件:一个区域也能够映射到一个匿名文件,匿名文件是由内核建立的,包含的全是二进制零。CPU第一次引用这样一个区域内的虚拟页面时,内核就在物理存储器中找到一个合适的牺牲页面,若是该页面被修改过,就将这个页面换出来,用二进制零覆盖牺牲页面并更新页表,将这个页面标记为是驻留在存储器中的。注意在磁盘和存储器之间没有实际的数据传送。由于这个缘由,映射到匿名文件的区域中的页面有时也叫作请求二进制零的页(demand-zero page)。
不管在哪一种状况下,一旦一个虚拟页面被初始化了, 它就在一个由内核维护的专门的交换文件(swap file)之间换来换去。交换文件也叫作交换空间(swap space)或者交换区域(swap area)。须要意识到的很重要的一点, 在任什么时候刻,交换空间都限制着当前运行着的进程可以分配的虚拟页面的总数
 
再看共享对象
一个对象能够被映射到虚拟存储的一个区域,要么做为共享对象,要么做为私有对象。若是一个进程将一个共享对象映射到它的虚拟地址空间的一个区域内,那么这个进程对这个区域的任何写操做,对于那些也把这个共享对象映射到它们虚拟存储器的其余进程而言也是可见的。并且,这此变化也会反映在磁盘上的原始对象中。(IPC的一种方式)
另外一方面,对一个映射到私有对象的区域作的改变,对于其余进程来讲是不可见的,而且进程对这个区域所作的任何写操做都不会反映在磁盘上的对象中。一个映射到共享对象的虚拟存储器区域叫作共享区域。相似地,也有私有区域。
 

共享对象的关键点在于即便对象被映射到了多个共享区域,物理存储器也只须要存放共享对象的一个拷贝。

一个共享对象(注意,物理页面不必定是连续的。)


 

私有对象是使用一种叫作写时拷贝(copy-on-write)的巧妙技术被映射到虚拟存储器中的。对于每一个映射私有对象的进程,相应私有区域的页表条目都被标记为只读,而且区域结构被标记为私有的写时拷贝。


 

再看fork函数

当fork函数被当前进程调用时,内核为新进程建立各类数据结构,并分配给它一个惟一的PID。为了给这个新进程建立虚拟存储器,它建立了当前进程的mm_struct、区域结构和页表的原样拷贝。它将两个进程中的每一个页面都为标记只读,并将两个进程中的每一个区域结构都标记为私有的写时拷贝。

当fork在新进程中返回时,新进程如今的虚拟存储器恰好和调用fork时存在的虚拟存储器相同。当这两个进程中的任一个后来进行写操做时,写时拷贝机制就会建立新页面,所以,也就为每一个进程保持了私有地址空间的抽象概念。

 

再看execve函数

假设运行在当前进程中的程序执行了以下的调用:

execve("a.out",NULL,NULL) ;

execve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效地替代了当前程序。加载并运行a.out须要如下几个步骤:

 

  • 删除已存在的用户区域。删除当前进程虚拟地址用户部分中的已存在的区域结构。
  • 映射私有区域。为新程序的文本、数据、bss和栈区域建立新的区域结构。全部这些新的区域都是私有的、写时拷贝的。文本和数据区域被映射为a.out文件中的文本和数据区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。栈和堆区域也是请求二进制零的。
  • 映射共享区域。若是a.out程序与共享对象(或目标)连接,好比标准C库libc.so,那么这些对象都是动态连接到这个程序的,而后再映射到用户虚拟地址空间中的共享区域内。
  • 设置程序计数器(PC)。execve作的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向文本区域的入口点。
下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据须要换入代码和数据页面。
 
 
使用mmap函数的用户级存储器映射
[cpp]  view plain copy
 
  1. #include<unistd.h>  
  2. #include<sys/mman.h>  
  3.   
  4.   
  5. void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset) ;  
  6.                 //返回:若成功时则为指向映射区域的指针,若出错则为MAP_FAILED(-1)  

mmap函数要求内核建立一个新的虚拟存储器区域是,最好是从地址start开始的一个区域,并将文件描述符fd指定的对象的一个连续的片(chunk)映射到这个新区域。连续的对象片大小为length字节,从距文件开始处偏移量为offset字节的地方开始。start地址仅仅是一个暗示,一般被定义为NULL。










[cpp]  view plain copy
 
  1. munmap函数删除虚拟存储器的区域:  
  2. #include<unistd.h>  
  3. #include<sys/mman.h>  
  4.   
  5.   
  6. int munmap(void *start,size_t length);  
  7.     //返回:若成功则为0,若出错则为-1  

一、须要额外的虚拟存储器时,使用一种动态存储器分配器(dynamic memory allocator)。一个动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆(heap)。在大多数的unix系统中,堆是一个请求二进制0的区域;对于每一个进程,内核维护着一个变量brk,它指向堆的顶部。

wps_clip_image-14482

二、分配器将堆视为一组不一样大小的块(block)的集合来维护。每一个块就是一个连续的虚拟存储器组块(chunk),要么是已分配的,要么是未分配的。

1)显式分配器(explicit allocator):如经过malloc,free或C++中经过new,delete来分配和释放一个块。

2)隐式分配器(implicit allocator):也叫作垃圾收集器(garbage collector)。自动释放未使用的已分配的块的过程叫作垃圾回收(garbage collection)。

三、malloc不初始化它返回的存储器,calloc是一个基于malloc的包装(wrapper)函数,它将分配的存储器初始化为0。想要改变一个之前已分配的块的大小,可使用realloc函数。

四、分配器必须对齐块,使得它们能够保存任何类型的数据对象。在大多数系统中,以8字节边界对齐。

不修改已分配的块:分配器只能操做或者改变空闲块。一旦被分配,就不容许修改或者移动它。

五、碎片(fragmentation)

有内部碎片(internal)和外部碎片(external)。

外部碎片:在一个已分配块比有效载荷在时发生的。(如对齐要求,分配最小值限制等)

外部碎片:当空闲存储器合计起来足够知足一个分配请求,可是没有一个单独的空闲块足够大能够来处理这个请求时发生的。

六、隐式空间链表

wps_clip_image-14485

wps_clip_image-15582

    放置分配的块的策略有:首次适配(first fit),下一次适配(next fit),和最佳适配(best fit)。

若是空闲块已经最大程度的合并,而仍然不能生成一个足够大的块,来知足要求的话,分配器就会向内核请求额外的堆存储器,要么是经过调用nmap,要么是经过调用sbrk函数;分配器都会将额外的(增长的)存储器转化成一个大的空闲块,将这个块插入到空闲链表中,而后将被请求的块放置在这个新的空闲块中。

七、书中对分配器的设计举了一个小例子,10.9.12节。

八、一种流行的减小分配时间的方法,称为分离存储(segregated storage),维护多个空闲链表,其中每一个链表中的块有大体相等的大小。

    关于“简单分离存储”、“分离适配”、“伙伴系统”等概念,10.9.14节进行了叙述。

 

垃圾回收

一、垃圾收集器将存储器视为一张有向可达图(reachability graph)。

wps_clip_image-11717

二、Mark%Sweep垃圾收集器由标记(mark)阶段和清除(sweep)阶段组成。标记阶段标记出根节点的全部可达的和已分配的后继,然后面的清除阶段释放每一个被标记的已分配块。典型地,块头部中空闲的低位中的一位来表示这个块是否被标记了。

wps_clip_image-1930

    Note that the arrows in this example denote memory references, and not free list pointers.

三、在10.11中,讲述了与存储器相关的错误,值得一读。

如:指针的算术运算是以它们指向的对象的大小为单位来进行的。

相关文章
相关标签/搜索