Linux虚拟存储系统

时间:2021-08-20 15:48:54

Linux虚拟存储系统 

2012-07-19 22:25:27|  分类: Linux |  标签:linux  存储系统  虚拟内存  |字号订阅

 
 

 

 

    Linux为每一个进程单独维护了一个单独的虚拟地址空间,形式如图所示。

Linux虚拟存储系统

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

    内核虚拟存储器的其他区域包含每个进程都不相同的数据,比如页表,内核在进程上下文中执行代码时用到的栈。

 

    Linux将虚拟存储器组织成一些区域(也叫做段)的集合。一个区域就是已经存在的(已分配的)虚拟存储器的连续片,这些页是以某种方式相关联的。比如说代码段,数据段,堆,共享库段以及用户栈都是不同的区域。每个存在的虚拟页面都保存在某个区域中,而不属于某个区域的虚拟页是不存在的,并且不能被进程使用。

    在Linux系统中发生缺页异常时,导致控制转移到内核的异常处理程序,这个处理程序将执行以下步骤:

  1. 检查虚拟地址A是合法的吗?换句话说就是A是不是在某个区域内?所以缺页异常处理程序首先搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较。如果这个地址是不合法的,那么缺页处理程序就会发出一个段错误,从而终止这个进程。
  2. 检查存储器访问是否合法?就是说,该进程是否有读,或者写或执行这个区域内页面的权限。如果试图进行访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。
  3. 如果不满足条件1和2,那么此刻,内核知道了这个缺页是由于对合法的虚拟地址进行合法的访问操作造成的。那个内核就选择牺牲一个页面,如果牺牲的这个页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回的时候,CPU将冲洗启动引起缺页的那个指令,这条指令将再次发送地址A到MMU。这次MMU就能正常的翻译A,而不会产生缺页中断了。

 

存储器映射

    Linux通过将虚拟存储器区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容,这个过程就叫做存储器映射

    一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件之间换来换去。交换文件也叫做交换空间。需要意识到的很重要的一点是,在任何时候,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。

共享对象

    存储器映射的概念其实来源于一个聪明的发现:如果虚拟存储器系统可以集成到传统的文件系统中,那么就能够提供一种简单而高效的把程序和数据加载到存储器中的方法。

    进程这一抽象的概念能够为每个进程提供自己私有的虚拟地址空间,可以避免受其他进程的错误读写。不过有许多进程有同样的只读文本区域,而且许多程序需要访问只读运行时库代码的相同拷贝。比如每个C程序都需要来自标准C库的诸如printf这样的函数。那么如果每个进程都在物理存储器中保持这些常用代码的复制拷贝,那就是一种极大的浪费。所以,我们需要存储器映射给我们提供一种清晰的机制,来控制多个进程如何共享对象。

   一个对象可以被映射到虚拟存储器的一个区域,要么作为共享对象,要么作为私有对象。如果一个进程将一个共享对象映射到它的虚拟之地空间的一个区域内,那么这个进程对这个区域的任何写操作,对于那些也把这个共享对象映射到他们虚拟存储器的其他的进程来说也是可见的。而且这些变化会反应在磁盘上的原始对象中。

    对一个映射到私有对象的区域做改变的时候,对于其他进程来说是不可见的,并且进程对这个区域所做的任何写操作都不会反应在磁盘上的对象中。

fork函数

   理解了虚拟存储器和存储器映射之后,我们就可以清晰的知道fork函数是如何创建一个带有自己独立虚拟地址空间的新进程的了。

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

    当fork在新进程中返回的时候,新进程现在的虚拟存储器刚好和调用fork时存在的虚拟存储器相同。当这父子进程中的任何一个后来进行写操作的时候,写时拷贝机制就会创建新页面,因此,子进程对任何数据的操作(比如更改数据大小),对父进程是没有影响的。

execve函数

    虚拟存储器和存储器映射在将程序加载到存储器到的过程中扮演者重要的角色。假设当前的进程中执行了如下的调用: execve(“a.out”, NULL, NULL);

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

  1. 删除已存在的用户区域:删除当前进程虚拟地址的用户部分中的已存在的区域结构。
  2. 映射私有区域:为新程序的文本,数据,bss,和栈区创建新的区域结构。所有这些新的区域都是私有的,写时拷贝的。文本和数据区域被映射为a.out文件中的文本和数据区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。栈和堆区域也是请求二进制零的,初始长度为0.
  3. 映射共享区域:如果a.out程序和共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后在映射到用户虚拟地址空间中的共享区域内。
  4. 设置程序计数器(PC):exevce做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向文本区域的入口点。
mmap函数(用户级存储映射)
    Unix进程可以使用mmap函数来创建新的虚拟存储器区域,并将对象映射到这些区域中。
#include <unistd.h> 
#include <sys/mman.h> 
void *mmap(void *start, size_t length, int prot, int flag 
           int fd, off_t offset); 

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

参数prot包含描述新映射的虚拟存储器区域的访问权限位(在相应结构中的vm_prot位)

  • PROT_EXEC:这个区域内的页面由可以被CPU执行的指令组成。
  • PROT_READ:这个区域内的页面可读。
  • PROT_WRITE:这个区域内的页面可写。
  • PROT_NONE:这个区域内的页面不能被访问。

参数flag由描述被映射对象类型的位组成。如果设置了MAP_ANON标记为,那么被映射的对象就是一个匿名对象,而相应的虚拟页面是请求二进制零的。MAP_PRIVATE表示被映射的对象是一个私有的,写时拷贝的对象。而MAP_SHARED表示是一个共享的对象。

可以写一个C程序,使用mmap函数将一个任意大小的磁盘文件拷贝到stdout。

int main(int argc, char **argv) 
{ 
  struct stat stat; 
  int fd; 
  if(argc != 2) 
  { 
    printf("Usage Wrong"); 
    exit(0); 
  } 
  fd = Open(argv[1], O_RDONLY, 0); 
  fstat(fd, &stat); 
  char * bufp; 
  bufp = mmap(NULL, stat.st_size, PROT_READ, MAP_PRIVATE, 
              fd, 0); 
  Write(1, bufp, stat.st_size); 
  exit(0); 
}