zoukankan      html  css  js  c++  java
  • 深入理解计算机系统结构——虚拟存储器

    虚拟存储器

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

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

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

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

    物理和虚拟寻址

    物理寻址

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

    虚拟寻址

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

    wps_clip_image-4956

        MMU(memory management unit,存储器管理单元),利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容由OS管理。

     

    地址空间

     

    地址空间(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的,在程序中还没有被引用的)

     虚拟页0和3还没有被分配,因此在磁盘上还不存在。虚拟页1、4和6被缓存在物理存储器中。页2、5和7已经被分配了,但是当前并未缓存在主存中,只存在于磁盘中。

     

    页表

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

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

    Linux虚拟存储器系统

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

    其中内核虚拟存储器包含内核中的代码和数据结构。内核虚拟存储器的某些区域被映射到所有进程共享的物理页面。例如,每个进程共享内核的代码和全局数据结构。内核虚拟存储器的其他区域包含每个进程都不相同的数据。例如,页表、内核在进程的上下文中执行代码时使用的栈(内核栈),以及记录虚拟地址空间当前组织的各种数据结构。


     

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

     

    1、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:指向链表中下一个区域结构。
     2、linux缺页异常处理
    假设MMU在试图翻译某个虚拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:
    1)虚拟地址A是合法的吗?换句话说,A在某个区域结构定义的区域内吗?为了回答这个问题,缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较。如果这个指令时不合法的,那么缺页处理程序就触发一个段错误(说明这个虚拟地址还没有映射),从而终止这个进程。这个情况在图9-28中标识为“1”
    2)试图进行的存储器访问是否合法?换句话说,进程是否有读、写或者执行这个区域内页面的权限?例如,这个缺页是不是由一条试图对这个代码段里的只读页面进行写操作的存储指令造成的?这个缺页是不是因为一个运行在用户模式中的进程试图从内核虚拟存储器中读取字造成的?如果试图进行的访问时不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。这种情况标识为"2"
    3)此刻,内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。它是这样来处理这个缺页的:选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU。这次,MMU就能正常地翻译A,而不会产生缺页中断了。
     
    存储器映射(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 plaincopy
     
    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 plaincopy
     
    1. munmap函数删除虚拟存储器的区域:  
    2. #include<unistd.h>  
    3. #include<sys/mman.h>  
    4.   
    5.   
    6. int munmap(void *start,size_t length);  
    7.     //返回:若成功则为0,若出错则为-1  
     
     虚拟存储器是对主存的一个抽象。支持虚拟存储器的处理器通过使用一种叫做虚拟寻址的间接形式来引用主存。处理器产生一个虚拟地址,在被发送到主存之前,这个地址被翻译成一个物理地址。从虚拟地址空间到物理地址空间的地址翻译要求硬件和软件紧密合作。专门的硬件通过使用页表来翻译虚拟地址,而页表的内容是由操作系统提供的。
    虚拟存储器提供三个重要的功能。第一,它在主存中自动缓存最近使用的存放磁盘上的虚拟地址空间的内容。虚拟存储器缓存中的块叫做页。对磁盘上页的引用会触发缺页,缺页将控制转移到操作系统中的一个缺页处理程序。缺页处理程序将页面从磁盘拷贝到主存缓存,如果必要,将写回被驱逐的页。第二,虚拟存储器简化了存储器管理,进而又简化了链接、在进程间共享数据、进程的存储器分配以及程序加载。最后,虚拟存储器通过在每条页表条目中加入保护位,从而简化了存储器保护。
    地址翻译的过程必须和系统中所有的硬件缓存的操作集成在一起。大多数页表条目位于L1高速缓存中,但是一个称为TLB的页表条目的片上高速缓存,通常会消除访问在L1上的页表条目的开销。
    现代系统通过将虚拟存储器片和磁盘上的文件片关联起来,以初始化虚拟存储器片,这个过程称为存储器映射。存储器映射为共享数据、创建新的进程以及加载程序提供了一种高效的机制。应用可以使用mmap函数来手工地创建和删除虚拟地址空间的区域。然而,大多数程序依赖于动态存储器分配器,例如malloc,它管理虚拟地址空间区域内一个称为堆的区域。动态存储器分配器是一个感觉像系统级程序的应用级程序,它直接操作存储器,而无需类型系统的很多帮助。分配器有两种类型。显式分配器要求应用显式地释放他们的存储器块。隐式分配器(垃圾收集器)自动释放任何未使用的和不可达的块。

     

  • 相关阅读:
    A1066 Root of AVL Tree (25 分)
    A1099 Build A Binary Search Tree (30 分)
    A1043 Is It a Binary Search Tree (25 分) ——PA, 24/25, 先记录思路
    A1079; A1090; A1004:一般树遍历
    A1053 Path of Equal Weight (30 分)
    A1086 Tree Traversals Again (25 分)
    A1020 Tree Traversals (25 分)
    A1091 Acute Stroke (30 分)
    A1103 Integer Factorization (30 分)
    A1032 Sharing (25 分)
  • 原文地址:https://www.cnblogs.com/wuchanming/p/4458724.html
Copyright © 2011-2022 走看看