《Linux 操作系统原理 — 内存 — 物理存储器与虚拟存储器》
物理地址:即物理主存的地址空间。主存被组织成一个由 M 个连续的、字节大小相同的单元组成的数组,每字节都有一个唯一的物理地址(Physical Address,PA)。第一个字节的地址为 0,接下来的字节的地址为 1,依此类推。给定这种简单的结构,CPU 访问存储器的最自然的方式就是使用物理地址,即物理寻址。
虚拟地址:即虚拟存储地址空间,它能够让用户态应用程序以为自己拥有一块连续可用的 “物理” 地址,但实际上从程序视角所见的都是虚拟地址,而且这些虚拟地址对应的物理主存空间通常可能是碎片的,甚至有部分数据还可能会被暂时储存在外部磁盘设备上,在需要时才进行数据交换。
虚拟存储器为了实现虚拟地址空间的 隔离性 和 连续性,就必须满足以下条件:
为了让数据得以在内存和外存之前高速交换,也为了让虚拟地址和物理地址之间可以进行高效转换。若使用传统的字节流方式进行内-外存是数据传输,总线的传输速率和磁盘的读写速率必然会成为瓶颈。因此,虚拟存储器需要实现特殊的内存组织方式来满足以上两点需求。
虚拟内存管理方式可以简单分为连续分配管理方式和非连续分配管理方式这两种:
从物理地址和虚拟地址的特征可以看出,虚拟存储器地址映射的实现是建立在离散分配的内存管理方式的基础之上的。
块式管理 (远古时代的计算机操系统的内存管理方式,暂且不暂开):将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间,称之为碎片。
页式管理 :把内存分为大小相等且固定的一页一页的形式,页较小,相对相比于块式管理的划分力度更大,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址和物理地址。
段式管理:页式管理虽然提高了内存利用率,但是页式管理其中的页对于用户程序而言并无实际意义。段式管理则是把内存分为多个段,每段的空间要比一页的空间大些。但最重要的是段对于用户程序而言是有实际意义的,每个段定义了一组逻辑信息,例如:有主程序段 main、子程序段 X、数据段 D 及栈段 S 等。段式管理通过段表对应逻辑地址和物理地址。
段页式管理:结合了段式管理和页式管理的优点。先将用户程序分成若干个段,再把每个段分成若干个页,并为每一个段赋予一个段名。如此的,段页式管理机制中段与段之间以及段的内部的都是离散的。
详见《Linux 操作系统原理 — 内存 — 页式管理、段式管理与段页式管理》
在 32 位 CPU 中,Linux 上的虚拟地址空间为 4G。在 64 位 CPU 中的虚拟地址空间为 16G。在 64 位的很多应用场景中,实际的物理内存可能远远小于虚拟内存的大小。所以这里以 32 位举例,Linux 会为每个进程维护一个单独的虚拟地址空间。
所以,我们习惯的将 Linux 虚拟存储器系统分为 内核虚拟存储器 和 进程虚拟存储器。则虚拟地址空间又可以分为 内核地址空间 与 用户地址空间。
内核态地址空间包含了内核的代码和数据结构,其中的某些区域被映射到给所有进程共享的物理内存页面存储所有进程共享的内核代码和全局数据结构;其他区域则包含每个进程都不相同的数据。例如:用户进程页表、内核在进程的上下文中执行代码时使用的栈(Stack),以及记录虚拟地址空间当前组织的各种数据结构。
·执行用户进程时,需要先从内存中读取该进程的指令,然后执行,获取指令时用到的就是虚拟地址。这个虚拟地址是程序链接时确定的(内核加载并初始化进程时会调整动态库的地址范围)。为了获取到实际的数据,CPU 需要将虚拟地址转换成物理地址。
由于每个进程都有 3G 的用户态地址空间,所以操作系统的物理内存无法对这些地址空间进行一一映射。因此 Linux kernel 需要一种机制,把进程的用户态地址空间映射到物理内存上。当一个用户进程请求访问物理内存时,内核通过存储在内核态地址空间中的进程页表(Page Table),把这个虚拟地址映射到物理地址。最基本的映射单位是页(Page),对应为页面上的是页表项(PTE)。
页表是由内核维护的,里面的每个内存映射(Memory Mapping)都将一块虚拟地址映射到一个特定的物理地址空间(物理内存或者磁盘存储空间)。每个进程拥有自己的页表,和其他进程的页表没有关系,以此实现了用户程序间虚拟空间的隔离。
另外,值得注意的是,在每个进程创建加载时,内核只是为进程 “创建” 了虚拟内存的布局(e.g. 初始化进程控制表中内存相关的链表),实际上并不立即就把虚拟内存对应位置的程序数据和代码(e.g. .text、.data 段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射(叫做存储器映射)。等到运行到对应的程序时,才会通过缺页异常,来拷贝数据到内存空间。还有进程运行过程中,要动态分配内存,比如:malloc 时也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常,继而写入数据。
用户进程申请并访问物理内存(或磁盘存储空间)的过程如下:
我们可以简易的认为虚拟空间都被映射到了磁盘空间中(事实上这种映射是按需的,通过 mmap 函数),并且由页表记录映射位置,当访问到某个地址的时候,通过页表中的有效位,可以得知此数据是否在内存中,如果不是,则通过缺页异常,将磁盘对应的数据拷贝到内存中,如果没有空闲内存,则选择牺牲页面,替换其他页面。
mmap 是用来建立从虚拟空间到磁盘空间的映射的,可以将一个虚拟空间地址映射到一个磁盘文件上,当不设置这个地址时,则由系统自动设置,函数返回对应的虚拟内存地址,当访问这个地址的时候,就需要把磁盘上的内容拷贝到内存了,然后就可以读或者写,最后通过 man_map 可以将内存上的数据换回到磁盘,也就是解除虚拟空间和内存空间的映射,这也是一种读写磁盘文件的方法,也是一种进程共享数据的方法,即共享内存。