本文主要是从存储的角度来介绍一个程序是怎么在操作系统中运行的,借以来对操作系统的内存管理做一个简单的了解。

1、以前是怎么做的?

在早期的时候,存储器是没有抽象的,都是通过命令来直接访问物理内存,在这种情况下,想要在内存中同时运行两个程序是根本不可能的,因为当两个程序同时操作同一地址时,它就可能崩溃了。

既然物理地址直接暴露给不同的进程非常危险,那聪明的人类是有办法的啦!如果想要解决多个应用程序同时处于内存中而相互之间不会影响,要解决两个问题:保护和重定位。于是,一个新的概念诞生了:地址空间。所谓地址空间,就是一个进程可用于寻址内存的一套地址集合。每个进程都有一个自己的地址空间,并且这个地址空间独立于其他进程的地址空间(当然,有些时候不同的进程之间也会有共享的地址空间)。

如何让每个进程都能动态定位自己的地址空间呢?解决方法是给每个CPU配置两个特殊硬件寄存器:基址寄存器和界限寄存器。当一个进程运行的时,程序的起始物理地址装载到基址寄存器,程序的长度装载到界限寄存器。但是,由于软件越来越强大,大小也在不断的增加,我们会遇到内存不够用的情况啊。下面介绍两种解决方法。

最简单的就是交换技术,就是让一个进程在内存中运行一段时间,然后又放到硬盘。如果对IO有一定的了解,你肯定就想到,这个不断的硬盘读出写入,得多浪费时间啊!是的。于是,主角就要登场了!虚拟内存!!!说白了它就是把进程的一部分调入到内存运行。注意,我们程序在运行的时候,并不是要整个加载到内存才能运行的,部分加载,它就能跑,就是这么牛。

 

2、虚拟内存是怎么做的?

那么,虚拟内存是怎么做的呢?每个程序都拥有自己的地址空间(注意,这个地址空间应该叫做虚拟地址空间才对,对于32位的操作系统,它是4G,进程本身会认为它就是占用了4个G的物理地址,其实那个虚拟的),这个空间被分割成很多块,每个块我们称为一(一般来说,对于32位的系统,一页的大小为4KB;对于64位的系统,一页的大小为8KB)。每一页有连续的地址范围。这些页会被映射到物理内存,正如我们前面所说,并不是全部的也都要在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立刻执行必要的映射。当程序引用到的一部分不在物理内存的时候(即缺页),由操作系统负责将缺失的部分装入到物理内存并重新执行失败的指令。

如果你只想了解一个进程执行的时候,存储器是怎么工作的,到这里就可以了。后面是对一些概念和细节的说明。

 

3、什么是MMU/页框/TLB?

在计算机中,有个专门的硬件负责将虚拟地址转换为物理地址,这个硬件叫做内存管理单元(MMU)。大致过程如下图所示。

进程运行时系统做了啥(存储器篇)

在虚拟地址空间中,我们的最小单位就是页了。而将它映射到内存之后,对应的单元叫做页框。也就是页框的大小和页的大小是一样的。

对于一个进程来说,它肯定有很多页,当一个东西多的时候,就要有个管理了。在我们的系统中,有个专门管理页的物件,叫做页表。这个东西一般存放在内存中(其实严格来说,应该是放在告诉缓存中),MMU通过读取它,通过里面的地址转换,就能映射到物理内存了。后面我们会详细介绍页表!但是,这里还是有个问题,就是缓存的速度虽然已经非常快了,但是和CPU比较,完全就不是一个数量级的!在《程序员的自我修养》里有这么一句话,“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”!如果你是一个小白,你会越来越明白这句话。于是,我们毫不犹豫在CPU中增加一组寄存器,以提高访问页表的速度,这组寄存器叫做快表,书面语我们称之为TLB(Translation Lookaside Buffer)。说白了它就是通过局部性原理,从概论命中的角度来加速虚拟地址到物理地址的转换。对于CPU来说,它多核之间共享高速缓存,但是每个核都有自己的TLB。

由存储器体系结构我们知道,寄存器是最接近CPU速度的,但是它非常贵,所以TLB有以下特点。1、TLB容量有限,通常指记录16~512个页表项;2、转换虚拟地址到物理地址时,优先从TLB中寻找;3、如果TLB中找不到对应的页表项,则从内存页表中获取,同时更新TLB;4、更新TLB时,如果TLB满了,则会淘汰一个最老且被认为不再使用的页表项。

 

4、什么是页表?

页表就是一个存放在物理内存的数据结构,它将虚拟页映射到物理页。如下图。

进程运行时系统做了啥(存储器篇)

对于一级的页表,页表的翻译过程如下图所示。

进程运行时系统做了啥(存储器篇)

6、二级页表是怎么节约内存的?

对于二级页表,在32位的系统中,它的地址空间会被分为三个部分,如下:

进程运行时系统做了啥(存储器篇)

它的地址转化分为两步:1、基于二级转换表,查找到地址所在的页帧;2、基于偏移,在页帧中查找到对应偏移的物理地址。示意图如下。

进程运行时系统做了啥(存储器篇)

那么二级页表到底是怎样来节约内存的呢?对于32位的系统来说,每个进程的虚拟地址是4G,每页的大小是4KB,那么就需要1M个页表项了,而一般一个页表项是32bit的,即4Byte。所以对于一级页表来说,需要4MB的内存空间。对于二级页表来说,按照上述条件,但是它的一级页表映射4MB,二级页表映射4KB,那么,则需要1K个一级页表(4G/4M=1K),二级页表也需要1K(4MB/4KB=1K),这样的页表需要占用4.004MB(1K*4B+1K*1K*4B=4.004MB)的内存空间了。怎么越来越大了?

很多书上告诉我们不是所有的二级页表都要放在内存的。但是具体是怎么回事?

其实需要从两个方面考虑,第一,二级页表可以不存在;第二,二级页表可以不在内存。

对于大多数的进程,其实并没有4G那么大,但是每个进程的虚拟内存都是4G的啊,如果虚拟内存中找不到页表项,操作系统就没有办法运行这个进程了。这里的第一种方法比较讨巧,我的第一级页表全部加载到内存,那么我就可以把4G的虚拟地址完全覆盖了,而二级页表的时候我就不去创建那些没有用到的页表了。如果说50%的一级页表被用到了,那么页表占用到的内存就是2.004MB(1K*4B+0.5*1K*1K*4B=2.004MB)。对于只有一级页表是不是大大节约了内存。

另外还有一种更牛更讨巧的方式。我们的页面只有在需要的时候才会将其加载到内存(当然,依靠局部性原理,当内存中没有满时,我们也会加载周边一些页面进内存,以提高命中率)。那么在极端的情况下,我们只需要将一级页表加载到内存,二级我就加载一个,那么就是8KB(1K*4B+1*1K*4B=8KB),这样就大大的节约了内存空间了。

 

7、缺页中断时系统做了什么?

进程运行时系统做了啥(存储器篇)

当缺页发生时,CPU硬件执行过程如上图所示:

<1>处理器生成一个虚拟地址,并将它传给MMU;

<2>MMU生成物理地址的页表条目(PTE)地址,并从内存中请求得到它;

<3>内存向MMU返回请求结果,此时没有请求的页表地址,也就是PTE的有效位为0;

<4>MMU触发了一次异常,即缺页中断,从而触发一个缺页异常处理程序;

<5>缺页处理程序就会从页表中确定一个牺牲页,如果该页的内容相较于硬盘存放的发生了改变,即脏页,则需要写回到硬盘,不然就丢弃;

<6>缺页处理程序将新的页写入内存;

<7>操作系统重新中中断发生的地方继续往下执行程序。

 

8、页面置换算法

前面提到当发生缺页的时候,我们会将页面进行置换,那么依据是什么呢?我们下面说说常用的一些页面置换算法。

进程运行时系统做了啥(存储器篇)

参考资料

《深入理解计算机系统》

《现代操作系统》

《Linux内核设计与实现》

若干网络上的博客

相关文章:

  • 2022-12-23
  • 2021-05-16
  • 2021-05-20
  • 2021-11-10
  • 2021-12-28
  • 2022-12-23
  • 2022-01-18
  • 2022-02-22
猜你喜欢
  • 2021-12-30
  • 2022-01-08
  • 2021-07-26
  • 2021-06-05
  • 2022-01-13
  • 2021-11-13
  • 2021-04-28
相关资源
相似解决方案