操作系统复习(一)内存管理的故事
这是一个关于操作系统内存管理的故事
内存管理是什么
故事从我们需要运行一个程序开始说起。。。
我们知道,我们写完编译后的程序是保存在磁盘中的,那么CPU怎样才能访问到我们的程序并执行呢?
由于CPU只能访问到内存空间,所以我们需要将磁盘中的程序拷贝到内存,然后将该程序中每一条指令的物理地址交给CPU,CPU才能不断地取址执行。
这就是操作系统的内存管理。
并且引出了两个问题:
- 我们的进程在物理内存中是怎样存在的
- CPU以怎样的方式来得到物理地址
第一个版本:直接使用物理地址
于是我们可以想象这样一个简单的内存管理方式:
我们将我们的进程中整个的拷贝进内存中。
我们在程序中全都使用物理地址,这样的话CPU在执行指令时就能直接使用该物理地址来访问我们的代码或者数据。
这样的方式足够简单,但是却存在很多问题:
- 我们并不能确定加载进内存的物理地址,所以需要在运行时对程序中用到的地址进行重新计算
- 我们的进程中使用的权限不一,比如代码是只读的,而数据是可写的
- 我们的进程中存在堆栈、动态数组等结构,整个拷贝的话不能方便的扩容
第二个版本:+分段
为了阶段上述问题,早期的操作系统设计出了分段的概念:
- 将进程中的内容分为多个逻辑段(代码段、数据段、OS段),这些段被不连续的分配到物理内存中
- 维护一个段表用于寻找每个段的起始地址,然后在程序中只要使用段号+段内偏移即可使CPU根据段表找到物理地址
分段很贴切我们编程人员的思想,即将进程分为多个模块,这样可以为每个模块分别设立保护模式,并且段和段不会有内存上的影响。
那么CPU是怎么寻址的呢?
上面提到了我们在进程中维护了一个段表,而在进程切换时CPU的段寄存器中会切换为该进程段表的指针,我们首先可通过这个指针来找到我们的段表,它长这样:
段号|段基地址 |段长度 |保护模式 |
这样我们就可以通过段号找到物理内存中段的起始地址,然后通过偏移地址找到物理地址,并可以用段长度和保护机制做一些安全检查。
那么怎么分配段的物理内存呢?
一般有三种算法:
- 首次适配法
- 最佳适配法
- 最差适配法
三种算法都很简单,根据字面意思就能了解其含义,这里就不阐述了,但是这三种方法毫无疑问的都会产生很多的内存碎片,最后随着时间可使用的内存会越来越小。
我们想想这样一个场景:一块面包,每个人过来都根据自己的食量切一块,久而久之,面包屑会越来越多,造成了食物浪费。。。
第三个版本: +分页
而在现实生活中,我们往往会把面包切成很多大小相同的小块,每个人只要根据食量拿走几块便是,每个人浪费的也就最多一块。
分页就是这样的机制。我们把物理内存都分成一样大小的页(一般是4KB),每一个段可以使用多个页,并且这些页不需要连续存在,这样就不会再产生外部碎片,减少了内存浪费。
那么还是那个问题,引入分页后CPU怎么寻址呢?
这里用到了和分段一样的思想,即页表。
和分段一样,进程也存在一个页寄存器来存放我们的页表指针,我们根据这个指针可以找到我们的页表,它长这样:
页号|页框|
因为页的大小都是一样的,所以不需要存储页的大小。
所以CPU按照同样的映射关系就能找到对应的物理地址。
这样就完美了吗?
我们虽然解决了外碎片的问题,但我们也加入了新的问题:由于页的大小很小,而且物理内存是不断变大的,要建立所有的映射就要存放很大的页表,在内存中存放如此之大的页表也是极大的开销。
第四个版本:+多级页表+快表
由于我们的进程实际上并不会使用这么多的页,所以很自然的想到了不存储没有使用的页,这样就极大的减少了页表的大小。
但是空间的减少带来的是时间的增加,由于页框之间不再自增排列,那么我们就需要进行不断的二分查找来找到对应的页框,这回带来很大的时间开销,是极为不划算的。
回归生活,我们在书的设计上用到了多级目录的结构,这样就极大的减少了我们检索消耗。
操作系统中也用到了这样的思想,即首先根据页目录和偏移找到我们的页表,然后在页表中根据页号找到我们的页基地址,然后进行寻址。
随着物理内存的增大,需要建立的页目录级数就越多,我们进行寻址的次数也会变多,这也会影响到我们的性能。
咋办呢?
一般来说,我们最近使用的地址会有更大的概率再此使用,所以加一层缓存是非常实用的。
这里缓存的实现叫块表,顾名思义,他很快,他会缓存我们最近使用过的页号对应的页框,这样如果缓存中存在该页号,我们就不用多次去寻址了。
第五个版本:分页+分段
前面说到,分页作为一种物理内存的分配策略,它在保证了快的同时又减少了内存的浪费,而我们用户想使用的还是基于分段的方式来编写代码,那么怎么将分段和分页结合成段页式管理结构呢?
这就引出了一个非常重要的概念:虚拟内存
虚拟内存:虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存
简单来说,虚拟内存只是一个地址,他能包含我们能访问到的最大的地址空间,但她只是一个地址,并不拥有真实的内存。
那他有什么用呢?
我们可以拿他当一个中间人,即对于分段和虚拟内存,我们就和使用分段+物理内存一样的使用,而对于虚拟内存和分页机制,我们也和使用物理内存+分页机制一样的使用。
再细一点,就是我们需要做两次映射:
第一次使用段号和段偏移得到虚拟内存的地址,这对于我们用户来说,这就是内存,我们可以使用就行了。
第二次使用虚拟内存地址中的页号和偏移再此得到在真实物理内存中的地址。
这样的话内部的分页机制对用户来说就是透明的,其实我们用户也根本不需要知道怎么分的页。
这样我们解决了分页和分段结合的问题,即使用虚拟内存。
但我们知道虚拟内存可以寻址到我们所有的地址空间,他甚至可以比我们物理内存空间还要大,并且我们在使用多进程时,每一个进程都有自己的虚拟空间
那么物理内存怎么能存下这么大的空间呢?
第六个版本:+页面置换
既然内存存不下,那么就存到磁盘中。
每次请求地址时,首先查看内存中是否存在这个页框,若存在则直接使用,若不存在则请求调页。
那么页面置换的算法有哪些呢?
这里的话资料比较多了,我这里就列出来不再阐述:
FIFO先入先出置换
LRU最近最久未使用
LFU最不常用
Clock时钟置换