代码改变世界

计算机组成原理 — 存储系统

2019-07-18 16:35  云物互联  阅读(3354)  评论(0编辑  收藏  举报

目录

前文列表

《计算机组成的基本硬件设备》
《计算机组成原理 — 冯诺依曼体系结构》
《计算机组成原理 — 中央处理器》
《计算机组成原理 — 指令系统》

存储系统

存储系统是指计算机中由存放程序和数据的各种存储设备、控制部件及管理信息调度的设备(硬件)和算法(软件)所组成的系统。

基本术语

  • 存储器:存放程序和数据的器件。
  • 存储位:存放一个二进制数位的存储单元,是存储器最小的存储单位。
  • 存储字:计算机可寻址(作为一个整体存入或取出)的最小信息单位。
  • 存储单元:存放一个存储字的若干个记忆单元组成一个存储单元。
  • 存储体:大量存储单元的集合组成存储体。
  • 存储单元地址:存储单元的编号。
  • 字编址:对存储单元按字编址。
  • 字节编址:对存储单元按字节编址。
  • 寻址:通过地址寻找数据,从对应地址的存储单元中访存数据。

存储器分类

首先我们有必要来了解一下存储器的分类。

按存储介质分类

半导体存储器:用半导体器件组成的存储器。
磁表面存储器:用磁性材料做成的存储器。
激光存储器:信息以刻痕的形式保存在盘面上,用激光束照射盘面,靠盘面的不同反射率来读出信息。

按存取方式分类

随机存储器(RAM,Random Access Memory):可随机访问任意存储单元,且存取时间与存储单元的物理位置无关。主要充当高速缓冲存储器和主存储器。

  • 动态随机存储器(DRAM):只能将数据保持很短的时间。为了保持数据,DRAM 使用电容存储,所以必须隔一段时间刷新(Refresh)一次,如果存储单元没有被刷新,存储的信息就会丢失,关机就会丢失数据。常用于主存储器。
  • 静态随机存储器(SRAM):所谓的 “静态”,指这种存储器只要保持通电,里面储存的数据就可以恒常保持。相对之下,DRAM 里面储存的数据需要周期性地 Refresh。然而,当电力供应停止(关机)时,储存的数据还是会消失,所以也称为 Volatile Memory(易失性存储器),这与在断电后仍能储存数据的 ROM 或闪存不同的。但是 SRAM 也有它的缺点,即它的集成度较低,功耗较 DRAM 大,相同容量的 DRAM 内存可以设计为较小的体积,但是 SRAM 却需要很大的体积。同样面积的硅片可以做出更大容量的 DRAM,因此 SRAM 显得更贵。常用于高速缓存。

串行访问存储器(SAS):在存储器中只能按某种顺序来进行存取,存取时间与存储单元的物理位置有关。

  • 顺序存取存储器(SAM):是完全的串行访问,如:磁带,读写时要待磁带移动到合适位置之后才能顺序读写。由于价格便宜,主要用于数据备份容灾系统。
  • 直接存取存储器(DAM):是部分的串行访问,如:HHD 机械硬盘,对信息的存取有两步操作。首先,磁头直接移动到目标区域(磁道),然后再从磁道的合适位置开始读写,它介于顺序存取和随机存取之间。主要用于辅助存储器。

只读存储器(ROM):是一种只能读不能写入的存储器,即预先一次性写入的存储器。通常用来存放固定不变的信息,如微程序控制存储器。

按信息的可保存性分类

非永久记忆(易失性)的存储器:掉电后数据消失的存储器,如:半导体读/写存储器 RAM。
永久性记忆(非易失性)的存储器:掉电后仍能保存信息的存储器,如:磁性材料做成的存储器以及半导体 ROM。

三层存储结构

存储系统的性能在计算机中具有非常重要的地位,主要因为:

  • 冯诺伊曼体系结构是建筑在 “存储程序” 概念的基础上,访存操作约占中央处理器(CPU)时间的 70% 左右。
  • 存储管理与组织的好坏影响到整机效率。
  • 现代的信息处理,如图像处理、数据库、知识库、语音识别、多媒体等对存储系统的要求很高。

计算机的主存储器(内存)来存放当前正在执行的应用程序及数据,但主存储器不能同时满足存取速度快、存储容量大和成本低的要求,在计算机中必须有速度由慢到快、容量由大到小的多级层次存储器,以最优的控制调度算法和合理的成本,构成具有性能可接受的存储系统,这就是 “缓存-主存-辅存” 三层存储结构。

其中,高速缓冲存储器(Cache)用来改善主存储器与中央处理器的速度匹配问题(主存和 CPU 一直保持了大约一个数量级的差距),高速缓存-主存层级拥有接近于缓存的速度和接近于主存的容量,解决了速度和成本之间的矛盾,主要用于提高 CPU 的访问速度;而辅助存储器(外存)则使用来存放当前不在运行的大量程序及数据,主存-辅存层级拥有接近于主存储器的速度和接近于辅存储器的容量,解决了大容量和低成本的需求,主要用于扩大存储空间。

在这里插入图片描述

高速缓存

高速缓存通常为半导体存储器、静态随机存储器(SRAM)和非永久性记忆存储器。高速缓存的提出建立在著名的局部性原理之上,局部性原理表示:程序访问的地址往往集中在存储器逻辑地址空间很小的范围之内。这是因为程序的地址分布本来就是连续的,再加上循环程序流、子程序调用程序流的重复执行,所以程序的地址访问自然就会相对的集中了。在主存和 CPU 之间设置了 Cache 之后,如果当前正在执行的程序和数据存放在 Cache 中,则当程序运行时不必再从主存储器读取指令和数据,访问 Cache 即可。

Cache 的工作原理有三点值得我们注意:

  1. 主存和 Cache 都会采用分字块的方式进行管理,Cache 中保存的就是对应的主存字块的一个副本。每一个 Cache 字块都会有一个标记位,用于表示当前字块里存放的是哪一个内存字块的副本。通过这个标准位,CPU 就可以判断出希望访问的内存字块是否已经存在于 Cache 中了。
  2. 当 Cache 已经用满,但主存还将新的字块调入 Cache 时,就会执行一次 Cache 字块的替换。这种替换遵守一定的规律,最好使得被替换的字块是下一个时间段内估计最小使用的,这种规则成为替换策略或替换算法,有替换部件实现。
  3. 当程序对 Cache 的字块执行写入时,如何保证 Cache 字块和内存字块的一致性。通常的有两种写入方式:一个是先写 Cache 字块,待 Cache 字块被替换出去时再一次性写入内存字块;在一个是在写 Cache 字块的同时也写入内存字块。

主存储器

内存通常为半导体存储器、动态随机存储器(DRAM)和非永久记忆存储器。具有两个非常重要的特性:“可随机访问任意存储单元” 和 “掉电即失去数据”,这些特性均服务于冯诺依曼体系 “存储程序” 的核心理想。

主存储器和 CPU 的连接是由总线来支持的,包括数据总线、地址总线和控制总线。CPU 通过 AR(地址寄存器)& AB(地址总线)、DR(数码寄存器)& DB(数据总线)和主存进行数据传输。若 AR 为 K 位字长,表示 CPU 的寻址宽度,即允许主存包含有 2**K 个可寻址存储单位;若 DR 为 n 位字长,则表示在一个存储周期内,CPU 和主存之间通过总线进行 n 位数据传输。控制总线包括控制数据传输的读(READ)、写(WRITE)和表示存储器功能完成的(READY)的三种控制线。

CPU 读取主存数据:当 CPU 从存储器读取一个存储字时,CPU 必须指定该存储字的地址,将该地址送到 AR 再经 AB 送到存储器,同时,CPU 通过控制线发送 READ 信号到存储器。此后,CPU 等到存储器通过控制线发来一个 READY 信号,表示已经完成了数据的读,并将数据经 DB 放到 DR 上了,CPU 再从 DR 取出相应的数据。以此来完成了一个存储器字的读与取。

** CPU 存放数据到主存**:CPU 为了存放一个字到存储器,首先将存储字在存储器中的存放地址通过 AR 经 AB 发送到存储器,并将存储字放到 DR,同时发出一个 WRITE 信号到存储器。此后,CPU 等到接收 READY 信号。存储器会根据 AB 收到的地址来存放 DB 接收到的存储字,然后通过 READY 控制线发送 READY 信号给 CPU 接收。以此完成了一个存储字的存放。

可见,主存和 CPU 之间采用的是异步工作方式,以 READY 信号表示以此访存操作的结束。
在这里插入图片描述

辅助存储器

外存通常为磁表面存储器、串行访问存储器和永久性记忆存储器,有磁盘存储器和古老一些的磁带已经光盘。磁盘存储器有 HDD(Hard Disk Drive)和 HDC(Hard Disk Controller)组成,而当下比较时髦的 SSD(Solid-state Drive)是一种主要以闪存(NAND Flash)作为永久性存储器的计算机存储设备,区别于以机械臂带动磁头转动实现读写操作的磁盘存储器,NAND 或者其他固态存储以电位高低或者相位状态的不同记录 0 和 1。辅存的只要技术指标有:存储密度、存储容量、寻址时间、数据传输率、误码率和价格。

值得注意的是,HDD 的存取时间为毫秒(ms)级别,为主存储器的存取时间为纳秒(ns)级别,两种的速度差别十分大。因此 I/O 系统成为了整个计算机系统的瓶颈,所以为了进一步拉近距离,在主存储器和辅存储器之间也引入了缓存层,即磁盘 Cache。磁盘 Cache 同样基于局部性原理,对数据使用了预读策略。现在的 HDD 通常会带有高速缓存介质,并且通常也为 SRAM 或 DRAM,容量为十几 MB 以上。但又因为 RAM 是易失性存储器,所以为了防止掉电时数据丢失的问题,一些 HDD 还会带有不间断电设备。

虚拟存储器

在早期的计算机系统中,程序员会直接对主存储器的物理地址进行操作,这种编程方式导致了当程序出现寻址错误时有可能会导致整个系统崩溃,当一个进程出现寻址错误时也可能会导致另一个进程崩溃。显然,直接操作主存的物理地址不是一个好的方法。而且,由于不存在分页或分段的存储空间管理手段,所以 CPU 寻址宽度就成为了存储容量的限制,例如:32 位 CPU 只支持 4GB 内存的寻址。这导致了该计算机无法运行存储空间需求大于实际内存容量的应用程序。

为了解决这些问题,现代计算机系统通过操作系统和硬件的结合,把主存储器和辅存储器从逻辑上统一成了一个整体,这就是虚拟存储器,或称为虚拟存储系统。虚拟存储器是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的和私有的地址空间。

虚拟存储器的两大特点

  • 允许用户程序使用比实际主存空间大得多的空间来访问主存
  • 每次访存都要进行虚实地址转换。
    • 物理地址,即物理主存的地址空间。主存被组织成一个由 M 个连续的、字节大小的单元组成的数组,每字节都有一个唯一的物理地址(Physical Address,PA)。第一个字节的地址为 0,接下来的字节的地址为 1,依此类推。给定这种简单的结构,CPU 访问存储器的最自然的方式就是使用物理地址,即物理寻址。
    • 虚拟地址,即虚拟存储地址空间,它能够让应用程序误以为自己拥有一块连续可用的 “物理” 地址,但实际上从程序视角所见的都是虚拟地址,而且这些虚拟地址对应的物理主存空间通常可能是碎片的,甚至有部分数据还可能会被暂时储存在外部磁盘设备上,在需要时才进行数据交换。

虚拟存储器的核心思路是根据程序运行时的局部性原理,一个程序运行时,在一小段时间内,只会用到程序和数据的很小一部分,仅把这部分程序和数据装入主存即可,更多的部分可以在需要用到时随时从辅存调入主存。在操作系统和相应硬件的支持下,数据在辅存和主存之间按程序运行的需要自动成批量地完成交换。

虚拟存储器提供了三个重要的能力

  1. 它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。
  2. 它为每个进程提供了一致的地址空间,从而简化了存储器管理。
  3. 它保护了每个进程的地址空间不被其他进程破坏。

虚拟存储器解决了三个根本需求

  1. 确保可以运行存储空间需求比实际主存储容量大的应用程序
  2. 确保可执行程序被装载后占用的内存空间是连续的。因为 PC 程序计数器是自增的,换句话说就是程序执行必须顺序存放在存储器中,PC 才能够按照程序语句,一条一条的读取指令,不错乱。
  3. 确保同时加载多个程序的时候不会造成内存地址冲突。虽然在生成可执行文件时指令已经有了对应的内存地址,但实际加载的时候,其实没办法保证程序一定就运行在这些内存地址上,因为多个程序同时运行的话,预期的内存地址很可能已经被其他程序占用了。

NOTE:虚拟存储器指的是 “主存-辅存” 层次,由软硬件结合实现,而 “缓存-主存” 是由存硬件实现的。

主存-辅存间信息交换单位和存储管理方式

段式虚拟存储管理方式

段式存储管理是一种把主存按段分配的存储管理方式,主存-辅存间信息传送单位是不定长的段。优点是段的分界与程序的自然分界是相对应的。例如:过程、子程序、数据表和阵列等待程序的模块化性质都可以与段对应起来。于是段作为独立的逻辑单位可以被其他程序段调用,这样就形成了段间连接,产生规模较大的程序。这样的特性使得段易于编译、管理、修改和保护,也便于多道程序共享。而缺点是容易在段间留下许多空余的存储空间碎片,且不好收集利用。除此之外,段式存储管理还存在交换性能较低的问题。因为辅存的访问速度比主存慢得多,而每一次交换,我们都需要把一大段连续的内存数据写到硬盘上,导致了当内存交换一个较大的段时,会让机器显得卡顿。
在这里插入图片描述

页式虚拟存储管理方式

页式存储管理是一种把主存按页分配的存储管理方式,主存-辅存间信息传送单位是定长的页。对比段而言,因为管理的粒度更细致,所以造成内存页碎片的浪费也会小很多。而缺点也正好相反,由于页不是程序独立模块对应的逻辑实体,所以处理、保护和共享都不及段来得方便。同时也因为页要比段小得多,在 Linux 下通常默认设置为 4KB,所以页在进行交换时,不会出现段交换那般卡顿。所以,页式存储管理方式会更加的受到欢迎,Linux 操作系统采用的就是页式存储管理方式。

在这里插入图片描述

更进一步的,页式存储管理方式使得加载程序的时候,不再需要一次性都把程序加载到内存中,而是在程序运行中需要用到的对应虚拟内存页里面的指令和数据时,再将其加载到内存中,这些操作由操作系统来完成。当 CPU 要读取特定的页,但却发现页的内容却没有加载时,就会触发一个来自 CPU 的缺页错误(Page Fault)。此时操作系统会捕获这个错误,然后找到对应的页并加载到内存中。通过这种方式,使得我们可以运行哪些远大于实例物理内存的程序,但相对的执行效率也会有所下降。
在这里插入图片描述
通过虚拟存储器、内存交换、内存分页三个技术的结合。我们最终得到了一个不需要让程序员考虑实际的物理内存地址、大小和当前分配空间的程序运行环境。这些技术和方式对于程序员和程序的编译、链接过程而言都是透明的,印证了那句著名的话:所有计算机问题都可以通过插入一个中间层来解决。

页式虚拟存储器

虚拟缓存器(Virtual Memory,VM)的实现思想就是将主存作为辅存的缓存,使得计算机系统拥有了 “主存+辅存(交换空间)” 大小的存储空间,同时也拥有了接近于主存的访问速度。从概念上而言,虚拟存储器被组织为一个由存放在磁盘上 N 个连续的字节大小的单元组成的数组。每个字节都有一个唯一的虚拟地址,这个唯一的虚拟地址作为定向到数组的索引。磁盘上的数组的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘上的数据被分割成块,这些块作为磁盘(较低层)和主存(较高层)之间的传输单元。

虚页与实页

在页式虚拟存储器中,通过将虚拟存储空间分割成为了大小固定的虚拟页(Vitual Page,VP),简称虚页,每个虚拟页的大小为 P=2^n 字节。类似地,物理存储空间被分割为物理页(Physical Page,PP)也称为页帧(Page Frame),简称实页,大小也为 P 字节。在任意时刻,虚拟页面的集合都分为三个不相交的子集:

  • 未分配的(unallocated):虚拟存储系统还未分配(或创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间。
  • 缓存的(cached):当前缓存在物理主存中的已分配页。
  • 未缓存的(uncached):没有缓存在物理主存中的已分配页。

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

在这里插入图片描述

基于页表的虚实地址转换原理

同任何缓存设计一样,虚拟存储器系统必须有某种方法来判定一个虚拟页是否存放在物理主存的某个地方。如果存在,系统还必须确定这个虚拟页存放在哪个物理页中。如果物理主存不命中,系统必须判断这个虚拟页存放在磁盘的哪个位置中,并在物理主存中选择一个牺牲页,然后将目标虚拟页从磁盘拷贝到物理主存中,替换掉牺牲页。这些功能是由许多软硬件联合提供,包括操作系统软件,MMU(存储器管理单元)地址翻译硬件和一个存放在物理主存中的叫做页表(Page Table)的数据结构,页表将虚拟页映射到物理页。页表的本质就是一个页表条目(Page Table Entry,PTE)数组。

CPU 通过虚拟地址(Virtual Address,VA)来访问存储空间,这个虚拟地址在被送到存储器之前需要先转换成适当的物理地址。将一个虚拟地址转换为物理地址的任务叫做地址翻译(Address Translation)。就像异常处理一样,地址翻译需要 CPU 硬件和操作系统之间的紧密合作。比如:Linux 操作系统的交换空间(Swap Space)。如果当 CPU 寻址时发现虚拟地址找不到对应的物理地址,那么就会触发一个异常并挂起寻址错误的进程。在这个过程中,对其他进程没有任何影响。

虚拟地址与物理地址之间的转换主要有 CPU 芯片上内嵌的存储器管理单元(Memory Management Unit,MMU)完成,它是一个专用的硬件,利用存放在主存中的查询表(地址映射表)来动态翻译虚拟地址,该表的内容由操作系统管理。

在这里插入图片描述
由于页的大小为 2 的整数次幂,所以页的起点都落在低位字段为 0 的地址上,可以把虚拟地址分为两个字段,高位字段位虚拟页号,低位字段为虚页内地址。在页表中,对应每一个虚页号都有一个条目,格式为 (虚页号,实页号,控制字)。
在这里插入图片描述
实页号即为实页地址,被作为物理地址的高字段,而物理地址的低字段则同为虚拟地址的低字段(虚页内地址)。拼接成为了主存物理地址之后,就可以据此访问主存储器数据了。
在这里插入图片描述
通常的,页面中还包括有装入位、修改位、替换控制位以及其他保护位组成的控制字。e.g.

  • 装入位为 1:表示该条目对应的虚页以及辅存调入主存;
  • 装入位为 0:表示对应的虚页尚未装入主存,如果此时 CPU 访问该页就会触发页面失效中断,启动 I/O 子系统,根据外页表项目中查找到的辅存地址,进行辅存到主存的页面交换;
  • 修改位:表示主存页面的内容是否被修改过,从主存交换到辅存时是否要写回辅存。
  • 替换位:表示需要替换的页。

应用 TLB 快表提升虚实地址转换速度

当页表已经存放在主存中,那么当 CPU 访问(虚拟)存储器时,首先要查询页面得到物理主存地址之后再访问主存完成存取。显然,地址转换机制让 CPU 多了一次访问主存的操作,相当于访问速度下降一半。而且当发生页面失效时,还要进行主存-辅助的页面交换,那么 CPU 访问主存的次数就更多了。为了解决这个问题,在一些影响访问速度的关键地方引入了硬件的支持。例如:采用按内容查找的相联存储器并行查找。此外,还进一步提出了 “快表” 的概念。把页表中最活跃的部分存放在快速存储器中组成快表,是减少 CPU 访问时间开销的一种方法。

快表由硬件(门电路和触发器)组成,属于 MMU 的部件之一,通常称为转换旁路缓冲器(Translation lookaside buffer,TLB)。TLB 的本质也是一个 Cache,它比页表小得多,一般在 16 个条目 ~ 128 个条目之间,快表只是页表的一个小小的副本。查表时,带着虚页好同时差快表和慢表(原页面),当在快表中找打条目时,则马上返回主存物理地址到主存地址寄存器,并使慢表查询作废。此时,虽然使用了虚拟存储器但实际上 CPU 访问主存的速度几乎没有下降(CPU 不再需要多次访问主存)。如果快表不命中,则需要花费一个访主存时间查慢表,然后再返回主存物理地址到主存地址寄存器,并将此条目送入到快表中,替换到快表的某一行条目。

在这里插入图片描述

页式虚拟存储器工作的全过程

内页表:虚拟地址与主存地址的映射。
外页表:虚拟地址与辅存地址的映射。
虚地址格式:(虚页号,虚页内地址)
主存地址格式:(实页号,实页内地址)
辅存地址格式:(磁盘机号,磁头号,柱面号,块号,块内地址)

从三种地址格式可见,虚地址-主存地址的转换是虚实页号替换,有内页表完成;虚地址-辅存地址的转换是虚页号与 “磁盘机号,磁头号,柱面号,块号” 的替换,由外页表完成。

在这里插入图片描述
1、2:虚拟存储器每次访问主存时都需要将多用户虚地址转换层主存实地址,这个由虚页号转换为实页号的内部地址转换由内页表来完成;
3:当对应内页表条目的有效位为 1 时,就按照物理主存地址 np 进行主存储器访问。
4:如果对应内存表条目的装入位为 0 时,表示该虚页对应的实页不再主存中,那么就触发一个页面失效中断。有中断处理器到辅存中调用对应的实页。
5:到辅存中调页,首先要进行外部地址转换,查找外页表,将多用户虚拟地址转换为辅存实页地址 Nvd。
6:根据辅存实页地址 Nvd 到辅存中选页。
7:将选中的辅存实页经过 I/O 处理机送出到物理主存中。
9:此时还要确定调入的辅存实页应该放置到主存的什么位置上,这通过查找实存页面表来完成。
10:当主存对应的目标地址仍然空闲时,就会找到空页面。
11、12:但当主存已经装满时,就是执行页面替换操作,由替换算法来决定替换哪一个主存实页到辅存中。
13:把待替换主存实页放入 I/O 处理机,待替换主存页是否被修改了是可以通过页表替换位知道的,此时如果待替换的主存实页没有被修改过,那么是不需要回写到辅存的。
14:但如果待替换的主存实页被修改了,那么就需要写回辅存。
7:继续将目标实页写入到物理主存中,完成替换。新页调入主存时,需要修改相应的页表条目。
8:如果待替换页没能装入缓存,那么还要继续进入中断,进行出错处理或其他处理。

大页内存的性能问题

在页式虚拟存储器中,会在虚拟存储空间和物理主存空间都分割为一个个固定大小的页,为线程分配内存是也是以页为单位。比如:页的大小为 4K,那么 4GB 存储空间就需要 4GB/4KB=1M 条记录,即有 100 多万个 4KB 的页。我们可以相待,如果页太小了,那么就会产生大量的页表条目,降低了查询速度的同时还浪费了存放页面的主存空间;但如果页太大了,又会容易造成浪费,原因就跟段式存储管理方式一般。所以 Linux 操作系统默认的页大小就是 4KB,可以通过指令查看:

$ getconf PAGE_SIZE
4096

但在某些对性能要求非常苛刻的场景中,页面会被设置得非常的大,比如:1GB、甚至几十 GB,这些页被称之为 “大页”(Huge Page)。大页能够提升性能的主要原因有以下几点:

  • 减少页表条目,加快检索速度。
  • 提升 TLB 快表的命中率,TLB 一般拥有 16 ~ 128 个条目之间,也就是说当大页为 1GB 的时候,TLB 能够对应 16GB ~ 128GB 之间的存储空间。

值得注意的是,首先使用大页的同时一般会禁止主存-辅存页面交换,原因跟段式存储管理方式一样,大容量交换会让辅存读写成为 CPU 处理的瓶颈。再一个就是大页也会使得页内地址检索的速度变慢,所以并非是页面的容量越大越好,而是需要对应用程序进行大量的测试取得页面容量与性能的曲线峰值才对。

Linux 的页式虚拟存储器系统

Linux 操作系统采用了页式虚拟存储器。Linux 上所有的进程都工作在一个 4G 的地址空间上,同时 Linux 会为每个进程维护一个单独的虚拟地址空间。其中 0 ~ 3G 是应用进程可以访问的 User 地址空间,是某个进程独有的,进程之间互相隔离;剩下 3G ~ 4G 是 Kernel 地址空间,所有进程都会共享这部分地址空间。所以,我们习惯的将 Linux 虚拟存储器系统分为 “内核虚拟存储器” 和 “进程虚拟存储器”。

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

由于每个进程都有 3G 的私有进程空间,所以操作系统的物理内存无法对这些地址空间进行一一映射,因此 Kernel 需要一种机制,把进程地址空间映射到物理内存上。当一个进程请求访问内存时,操作系统通过存储在 Kernel 中的进程页表把这个虚拟地址映射到物理地址,如果还没有为这个地址建立页表项,那么操作系统就为这个访问的地址建立页表项。最基本的映射单位是 Page,对应的是页表项 PTE。由于页表项和物理地址是多对一的关系,即多个页表项可以对应一个物理页面,因而支持共享内存的实现(几个进程同时共享物理内存)

在 Linux 操作系统使用文件或管道来进行进程之间的通信会有很多局限性,比如效率问题,以及数据处理使用文件描述符不如内存地址访问来得方便,于是多个进程间以共享内存的方式进行通信就成了一个不错的选择。Linux 在编程上为我们准备了多种手段的共享内存方案。包括:

  • mmap 内存共享映射。
  • XSI 共享内存。
  • POSIX 共享内存。

Linux 下的 KSM 内存页共享的性能问题

KSM(Kernel Shared Memory)是 Linux Kernel 的一种内存共享机制,在 2.6.36 版本引入。简而言之,KSM 用于合并具有相同内容的物理主存页面以减少页面冗余。在 Kernel 中有一个 KSM 守护进程 ksmd,它会定期扫描用户向它注册的内存区域,寻找到相同的页面就会将其合并,并用一个添加了写保护的页面来代替。当有进程尝试写入该页面时,Kernel 会自动为其分配一个新的页面,然后将新数据写入到这个新页面,这就是典型的 COW 机制。类似的,存储技术中有一个称为去耦合(de-duplication)的技术,通过删除冗余数据(基于数据块,或者基于更大的数据片段,比如文件)来减少已存储的数据。公共数据片段被合并(以一种 COW 方式),释放空间供其他用途。使用这种方法,存储成本更低,最终需要的存储器也更少。

KSM 最初被应用到 KVM 上,因为事实证明,如果虚拟化了许多相同的操作系统和应用程序组,那么宿主机上许多内存页面都是相同的。假如操作系统和应用程序代码以及常量数据在 VMs 之间相同,那么这个特点就很有用。当页面惟一时,它们可以被合并,从而释放内存,供其他应用程序使用。将多个 VMs 具有的相同内存页合并(共享),可以腾出更多的可用物理内存。

在这里插入图片描述
但是事实上,KSM 可以应用于任何应用。KSM 仅仅会合并匿名页面,不会对文件映射的页面做处理,经过 KSM 合并的页面最初是被锁定的内存中的,但是现在已经可以像其他页面一样被换出到交换区了。但是共享页一经换出,其共享的特性就被打破,再次换入的时候,ksmd 必须重新对其处理。前面提到,KSM 仅仅会扫描那些向其注册的区域,就是向 KSM 模块注册了如果条件允许可以被合并的区域,通过 madvise 系统调用可以做到这点 int madvise(addr, length, MADV_MERGEABLE)。同时,应用也可以通过调用 int madvise(addr, length, MADV_UNMERGEABLE) 来取消这个注册,从而让页面恢复私有特性。但是该调用可能会造成内存超额,造成 unmerge 失败,很大程度上会造成唤醒 Out-Of-Memory killer,杀死当前进程。如果 KSM 没有在当前运行的 Kernel 启用,那么前面提到的 madvise 调用就会失败,如果内核配置了 CONFIG_KSM=y,调用一般是会成功的。

KSM 的管理和监控通过 sysfs(位于根 /sys/kernel/mm/ksm)执行。

  • pages_to_scan:定义一次给定扫描中可以扫描的页面数。
  • sleep_millisecs:定义执行另一次页面扫描前 ksmd 休眠的毫秒数。
  • max_kernel_pages:定义 ksmd 可以使用的最大页面数(默认值是可用内存的 25%,但可以写入一个 0 来指定为无限)。
  • merge_across_nodes:控制不同 NUMA 节点内存的合并,如果被设置成 0,则只合并当前 NUMA 节点的内存。
  • run:控制 ksmd 的运行
    • 0 表示停止 ksmd,但是保持合并的页面;
    • 1 表示运行 ksmd;
    • 2 表示停止 ksmd 并请求取消合并所有合并页面。

KSM 合并效果会实时显示在下面文件:

  • pages_shared:KSM 正在使用的不可交换的内核页面的数量。
  • pages_sharing:一个内存存储指示。
  • pages_unshared :为合并而重复检查的惟一页面的数量。
  • pages_volatile:频繁改变的页面的数量。
  • full_scans:表明已经执行的全区域扫描的次数。

KSM 作者定义:较高的 pages_sharing/pages_shared 比率表明高效的页面共享(反之则表明资源浪费)。

需要注意的是,应用 KSM 的时候要慎重考虑,因为 KSM 扫描相同的页面的过程会消耗较多的 CPU 资源,在对虚拟机性能要求苛刻的环境中一般都会禁用 KSM。关闭 KSM,可以让作为 Hypervisor 的 Linux Kernel(KVM)在负载增加时候,保证虚拟机的响应速度。这里再次印证了一句名言:计算机艺术永远是时间与空间的较量

通过动态链接来节省内存

从上文我们知道,链接(Link)是程序被装载到内存运行之前需要完成的一个步骤。链接又分为动态链接(Dynamic Link)和静态链接(Static Link)两种方式。在动态链接的过程中,我们希望链接的不是存储在磁盘上的目标文件代码,而是链接到了内存中的共享库(Shard Libraries)。这个加载到内存中的共享库会被很多程序的指令调用。在 Windows 中,这个共享库文件就是 .dll(Dynamic-Link Libary,动态链接库)文件。而在 Linux 下,这些共享文件就是 .so(Shared Object)文件,我们一般也称之为动态链接库文件。在这里插入图片描述
不过,要想在程序中运行时加载共享库代码,就要求这些共享库代码是 “地址无关” 的。也就是说,我们编译出来的共享库文件的指令代码,是地址无关吗。换句话说,共享库无论加载到那个内存地址,都能够正常的运行。否则,就是地址相关代码。幸运的是,大部分函数库代码都是可以做到地址无关的,因为它们都被实现为接收特定的输入,进行确定的操作,然后在返回结果。这些函数的代码逻辑和输入数据存放在内存什么位置并无所谓。

有了动态链接方式之后,我们得以把内存利用得更加的极致,动态链接库是有如共享单车一般的存在。