目录

前文列表

Linux 操作系统原理 — 内存 — 物理存储器与虚拟存储器
Linux 操作系统原理 — 内存 — 基于 MMU 硬件单元的虚/实地址映射技术
Linux 操作系统原理 — 内存 — 基于局部性原理实现的内/外存交换技术
Linux 操作系统原理 — 内存 — 内存分配算法

页式管理

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

Linux 操作系统原理 — 内存 — 页式管理、段式管理与段页式管理_Linux

更进一步的,页式存储管理方式使得加载程序的时候,不再需要一次性都把程序加载到内存中,而是在程序运行中需要用到的对应虚拟内存页里面的指令和数据时,再将其加载到内存中,这些操作由操作系统来完成。当 CPU 要读取特定的页,但却发现页的内容却没有加载时,就会触发一个来自 CPU 的缺页错误(Page Fault)。此时操作系统会捕获这个错误,然后找到对应的页并加载到内存中。通过这种方式,使得我们可以运行哪些远大于实例物理内存的程序,但相对的执行效率也会有所下降。

Linux 操作系统原理 — 内存 — 页式管理、段式管理与段页式管理_Linux 操作系统原理_02
通过虚拟存储器、内存交换、内存分页三个技术的结合。我们最终得到了一个不需要让程序员考虑实际的物理内存地址、大小和当前分配空间的程序运行环境。这些技术和方式对于程序员和程序的编译、链接过程而言都是透明的,印证了那句著名的话:所有计算机问题都可以通过插入一个中间层来解决。

页表管理机制中有两个很重要的概念:快表和多级页表。在分页内存管理中,很重要的两点是:

  1. 虚拟地址到物理地址的转换要快。
  2. 解决虚拟地址空间大,页表也会很大的问题。

快表

块表:为了解决虚拟地址到物理地址的转换速度,操作系统在 页表方案 基础之上引入了 快表 来加速虚拟地址到物理地址的转换。我们可以把块表理解为一种特殊的高速缓冲存储器(Cache),其中的内容是页表的一部分或者全部内容。作为页表的 Cache,它的作用与页表相似,但是提高了访问速率。由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。

使用快表之后的地址转换流程是这样的:

  1. 根据虚拟地址中的页号查快表;
  2. 如果该页在快表中,直接从快表中读取相应的物理地址;
  3. 如果该页不在快表中,就访问内存中的页表,再从页表中得到物理地址,同时将页表中的该映射表项添加到快表中;
  4. 当快表填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。

看完了之后你会发现快表和我们平时经常在我们开发的系统使用的缓存(比如 Redis)很像,的确是这样的,操作系统中的很多思想、很多经典的算法,你都可以在我们日常开发使用的各种工具或者框架中找到它们的影子。

多级页表

多级页表:引入多级页表的主要目的是为了避免把全部页表一直放在内存中占用过多空间,特别是那些根本就不需要的页表就不需要保留在内存中。多级页表属于时间换空间的典型场景,具体可以查看下面这篇文章:https://www.polarxiong.com/archives/多级页表如何节约内存.html。

为了提高内存的空间性能,提出了多级页表的概念;但是提到空间性能是以浪费时间性能为基础的,因此为了补充损失的时间性能,提出了快表(即 TLB)的概念。不论是快表还是多级页表实际上都利用到了程序的局部性原理,局部性原理在后面的虚拟内存这部分会介绍到。

共同点 :

  • 分页机制和分段机制都是为了提高内存利用率,较少内存碎片。
  • 页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的。

区别 :

  • 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。
  • 分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。

为了存储 64 位操作系统中 128 TiB 虚拟内存的映射数据,Linux 在 2.6.10 中引入了四层的页表辅助虚拟地址的转换,在 4.11 中引入了五层的页表结构,在未来还可能会引入更多层的页表结构以支持 64 位的虚拟地址。

Linux 操作系统原理 — 内存 — 页式管理、段式管理与段页式管理_Linux 操作系统原理_03

在如上图所示的四层页表结构中,操作系统会使用最低的 12 位作为页面的偏移量,剩下的 36 位会分四组分别表示当前层级在上一层中的索引,所有的虚拟地址都可以用上述的多层页表查找到对应的物理地址。

因为操作系统的虚拟地址空间大小都是一定的,整片虚拟地址空间被均匀分成了 N 个大小相同的内存页,所以内存页的大小最终会决定每个进程中页表项的层级结构和具体数量,虚拟页的大小越小,单个进程中的页表项和虚拟页也就越多。

因为目前的虚拟页大小为 4096 字节,所以虚拟地址末尾的 12 位可以表示虚拟页中的地址,如果虚拟页的大小降到了 512 字节,那么原本的四层页表结构或者五层页表结构会变成五层或者六层,这不仅会增加内存访问的额外开销,还会增加每个进程中页表项占用的内存大小。

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

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

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

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

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

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

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

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

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

Linux 操作系统原理 — 内存 — 页式管理、段式管理与段页式管理_Linux 操作系统原理_07

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

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

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

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

缺页中断

地址映射过程中,若在页面中发现所要访问的页面不在内存中,则发生缺页中断。缺页中断 就是要访问的页不在主存,需要操作系统将其调入主存后再进行访问。在这个时候,被内存映射的文件实际上成了一个分页交换文件。当发生缺页中断时,如果当前内存中并没有空闲的页面,操作系统就必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。用来选择淘汰哪一页的规则叫做页面置换算法,我们可以把页面置换算法看成是淘汰页面的规则。

  • OPT 页面置换算法(最佳页面置换算法) :理想情况,不可能实现,一般作为衡量其他置换算法的方法。
  • FIFO 页面置换算法(先进先出页面置换算法) : 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。
  • LRU 页面置换算法(最近未使用页面置换算法) :LRU(Least Currently Used)算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间T,当须淘汰一个页面时,选择现有页面中其T值最大的,即最近最久未使用的页面予以淘汰。
  • LFU 页面置换算法(最少使用页面排序算法) : LFU(Least Frequently Used)算法会让系统维护一个按最近一次访问时间排序的页面链表,链表首节点是最近刚刚使用过的页面,链表尾节点是最久未使用的页面。访问内存时,找到相应页面,并把它移到链表之首。缺页时,置换链表尾节点的页面。也就是说内存内使用越频繁的页面,被保留的时间也相对越长。

处理的流程如下:

Linux 操作系统原理 — 内存 — 页式管理、段式管理与段页式管理_Linux_09

  • cr2:访问到线性地址、
  • err_code:异常发生时由控制单元压入栈中,表示发生异常的原因。
  • regs:发生异常时寄存器的值。

为什么 Linux 默认页大小是 4KB?

Linux 同时支持正常大小的内存页和大内存页(Huge Page),绝大多数处理器上的内存页的默认大小都是 4KB,虽然部分处理器会使用 8KB、16KB 或者 64KB 作为默认的页面大小,但是 4KB 的页面仍然是操作系统默认内存页配置的主流。除了正常的内存页大小之外,不同的处理器上也包含不同大小的大页面,我们在 x86 处理器上就可以使用 2MB 的内存页。

4KB 的内存页其实是一个历史遗留问题,在上个世纪 80 年代确定的 4KB 一直保留到了今天。虽然今天的硬件比过去丰富了很多,但是我们仍然沿用了过去主流的内存页大小。在今天,4KB 的内存页大小可能不是最佳的选择,8KB 或者 16KB 说不定是更好的选择,但是这是过去在特定场景下做出的权衡。

我们需要关注的是两个影响内存页大小的因素:

  • 过小的页面大小会带来较大的页表项增加寻址时 TLB(Translation lookaside buffer)的查找速度和额外开销;
  • 过大的页面大小会浪费内存空间,造成内存碎片,降低内存的利用率;

上个世纪在设计内存页大小时充分考虑了上述的两个因素,最终选择了 4KB 的内存页作为操作系统最常见的页大小。

段式管理
  • 段式管理:页式管理虽然提高了内存利用率,但是页式管理其中的页实际并无任何实际意义。段式管理把主存分为一段段的,每一段的空间又要比一页的空间小很多 。但是,最重要的是段是有实际意义的,每个段定义了一组逻辑信息,例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。段式管理通过段表对应逻辑地址和物理地址

段式存储管理是一种把主存按段分配的存储管理方式,主存-辅存间信息传送单位是不定长的段。

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

Linux 操作系统原理 — 内存 — 页式管理、段式管理与段页式管理_Linux_10

段页式管理

段页式管理结合了段式管理和页式管理的优点。简单来说,段页式管理机制就是把主存先分成若干页,每个页又分成若干段。如此的,段页式管理机制中段与段之间以及段的内部的都是离散的。显然,段页式管理是一种折中的方式,具有广泛的通用性。

在 Linux 内部的地址的映射过程为:逻辑地址–>线性地址–>物理地址。物理地址,即:地址总线中传输的数字信号,而线性地址和逻辑地址所表示的则是一种转换规则,线性地址规则如下:
Linux 操作系统原理 — 内存 — 页式管理、段式管理与段页式管理_Linux 操作系统原理_11

这部分由 MMU 完成,其中涉及到主要的寄存器有 CR0、CR3。机器指令中出现的是逻辑地址,逻辑地址规则如下:

Linux 操作系统原理 — 内存 — 页式管理、段式管理与段页式管理_Linux 操作系统原理_12

在 Linux 中的逻辑地址等于线性地址。

而段页式管理的 MMU 硬件电路,它包含两个部件,一个是分段部件,一个是分页部件:

  • 分段机制把一个逻辑地址转换为线性地址。
  • 分页机制把一个线性地址转换为物理地址。

Linux 操作系统原理 — 内存 — 页式管理、段式管理与段页式管理_Linux 操作系统原理_13

内存分段的原理:逻辑地址的段寄存器中的值提供段描述符,然后从段描述符中得到段基址和段界限,然后加上逻辑地址的偏移量,就得到了线性地址。

段选择符:

  • 为了方便快速检索段选择符,处理器提供了 6 个分段寄存器来缓存段选择符,它们是:CS、SS、DS、ES、FS 和 GS。
  • 段的基地址(Base Address):在线性地址空间中段的起始地址。
  • 段的界限(Limit):在虚拟地址空间中,段内可以使用的最大偏移量。

Linux 操作系统原理 — 内存 — 页式管理、段式管理与段页式管理_Linux_14

内存分页(32 位)原理:是在分段机制之后进行的,它进一步将线性地址转换为物理地址:

  • 10 位页目录,10 位页表项, 12 位页偏移地址。
  • 单页的大小为 4KB。

Linux 操作系统原理 — 内存 — 页式管理、段式管理与段页式管理_Linux_15