第一阶段:没有内存抽象
没有内存抽象对于内存的管理通常非常简单,除去操作系统所用的内存之外,全部给用户程序使用。或是在内存中多留一片区域给驱动程序使用,如图1所示。
图1. 没有内存抽象时,对内存的使用
第一种情况操作系统存于RAM中,放在内存的低地址,第二种情况操作系统存在于ROM中,存在内存的高地址,一般老式的手机操作系统是这么设计的。
如果这种情况下,想要操作系统可以执行多进程的话,
缺陷:多线程直接操作内存,会产生冲突。唯一的解决方案就是和硬盘搞交换,当一个进程执行到一定程度时,整个存入硬盘,转而执行其它进程,到需要执行这个进程时,再从硬盘中取回内存,只要同一时间内存中只有一个进程就行,这也就是所谓的交换(Swapping)技术。
第二阶段:内存抽象——为了解决多线程
内存抽象允许每个进程拥有自己的地址。这还需要硬件上存在两个寄存器,基址寄存器(base register)和界址寄存器(limit register),第一个寄存器保存进程的开始地址,第二个寄存器保存上界,防止内存溢出。
问题1:内存大小不可能容纳下所有并发执行的进程。
解决方法:交换(Swapping)。和前面所讲的交换大同小异,交换的基本思想是,将闲置的进程交换出内存,暂存在硬盘中,待执行时再交换回内存,比如下面一个例子,当程序一开始时,只有进程A,逐渐有了进程B和C,此时来了进程D,但内存中没有足够的空间给进程D,因此将进程B交换出内存,分给进程D。如图2所示。
图2. 交换技术
问题2:如图2,进程D和C之间的空间由于太小无法另任何进程使用,这也就是所谓的外部碎片。
解决方法:比如内存整理软件,原理是申请一块超大的内存,将所有进程置换出内存,然后再释放这块内存,从而重新加载进程,使得外部碎片被消除。这也是为什么运行完内存整理会狂读硬盘的原因。
问题3:创建进程时分配多少内存。如果分配多了,会产生内部碎片,浪费了内存,而分配少了会造成内存溢出。
解决方法:一种是直接多分配一点内存空间用于进程在内存中的增长,另一种是将增长区分为数据段和栈(用于存放返回地址和局部变量),如图3所示。
图3. 创建进程时预留空间用于增长
当预留的空间不够满足增长时,操作系统首先会看相邻的内存是否空闲,如果空闲则自动分配,如果不空闲,就将整个进程移到足够容纳增长的空间内存中,如果不存在这样的内存空间,则会将闲置的进程置换出去。
问题4:操作系统如何管理内存
法I:位图。将内存划为多个大小相等的块,比如一个32K的内存1K一块可以划为32块,则需要32位(4字节)来表示其使用情况,使用位图将已经使用的块标为1,位使用的标为0.
法II:链表。将内存按使用或未使用分为多个段进行链接,如下图中的P表示进程,从0-2是进程,H表示空闲,从3-4表示是空闲。
图4. 位图和链表表示内存的使用情况
使用位图表示内存简单明了,但一个问题是当分配内存时必须在内存中搜索大量的连续0的空间,这是十分消耗资源的操作。相比之下,使用链表进行此操作将会更胜一筹。
当利用链表管理内存的情况下,创建进程时分配什么样的空闲空间也是个问题。通常情况下有如下几种算法来对进程创建时的空间进行分配。
- 临近适应算法(Next fit)—从当前位置开始,搜索第一个能满足进程要求的内存空间
- 最佳适应算法(Best fit)—搜索整个链表,找到能满足进程要求最小内存的内存空间
- 最大适应算法(Wrost fit)—找到当前内存中最大的空闲空间
- 首次适应算法(First fit) —从链表的第一个开始,找到第一个能满足进程要求的内存空间
第三阶段:虚拟内存(Virtual Memory)——为了解决大进程的内存要求
在早期的操作系统曾使用覆盖(overlays)来解决这个问题,将一个程序分为多个块,基本思想是先将块0加入内存,块0执行完后,将块1加入内存。依次往复,这个解决方案最大的问题是需要程序员去程序进行分块,这是一个费时费力让人痛苦不堪的过程。
虚拟内存的基本思想是,每个进程有用独立的逻辑地址空间,内存被分为大小相等的多个块,称为页(Page).每个页都是一段连续的地址。对于进程来看,逻辑上貌似有很多内存空间,其中一部分对应物理内存上的一块(称为页框,通常页和页框大小相等),还有一些没加载在内存中的对应在硬盘上,如图5所示。
图5. 虚拟内存和物理内存以及磁盘的映射关系
由图5可以看出,虚拟内存实际上可以比物理内存大。当访问虚拟内存时,会访问MMU(内存管理单元)去匹配对应的物理地址(比如图5的0,1,2),而如果虚拟内存的页并不存在于物理内存中(如图5的3,4),会产生缺页中断,从磁盘中取得缺的页放入内存,如果内存已满,还会根据某种算法将磁盘中的页换出。
MMU中存储页表,用来匹配虚拟内存和物理内存。页表中每个项通常为32位,即4byte,除了存储虚拟地址和页框地址之外,还会存储一些标志位,比如是否缺页,是否修改过,写保护等。因为页表中每个条目是4字节,现在的32位操作系统虚拟地址空间是2^32,假设每页分为4k,也需(2^32/(4*2^10))*4=4M的空间,为每个进程建立一个4M的页表并不明智。因此在页表的概念上进行推广,产生二级页表,虽然页表条目没有减少,但内存中可以仅仅存放需要使用的二级页表和一级页表,大大减少了内存的使用。
每个进程有4GB的虚拟地址空间,每个进程自己的一套页表。程序中使用的都是4GB地址空间中的虚拟地址。而访问物理内存,需要使用物理地址。
一个页表的大小为4K字节,页表项(PTE, page table entry)的大小为4个字节(32bit),所以一个页表中有1024个页表项。
- 二级页表中的每一项的内容高20bit用来放一个物理页的物理地址,低12bit放着一些标志。
- 一级页表中的每一项的内容高20bit用来放一个二级页表的物理地址,低12bit放着一些标志。
CPU把虚拟地址转换成物理地址:一个虚拟地址,大小4个字节(32bit),分为3个部分:第22位到第31位这10位(最高10位)是页目录中的索引,第12位到第21位这10位是页表中的索引,第0位到第11位这12位(低12位)是页内偏移。一个一级页表有1024项,虚拟地址最高的10bit刚好可以索引1024项(2的10次方等于1024)。一个二级页表也有1024项,虚拟地址中间部分的10bit,刚好索引1024项。虚拟地址最低的12bit(2的12次方等于4096),作为页内偏移,刚好可以索引4KB,也就是一个物理页中的每个字节。
页面替换算法:物理内存是极其有限的,当虚拟内存所求的页不在物理内存中时,将需要将物理内存中的页替换出去,选择哪些页替换出去就显得尤为重要。
- 最佳置换算法(Optimal Page Replacement Algorithm):将未来最久不使用的页替换出去,这听起来很简单,但是无法实现。但是这种算法可以作为衡量其它算法的基准。
- 最近不常使用算法(Not Recently Used Replacement Algorithm):这种算法给每个页一个标志位,R表示最近被访问过,M表示被修改过。定期对R进行清零。这个算法的思路是首先淘汰那些未被访问过R=0的页,其次是被访问过R=1,未被修改过M=0的页,最后是R=1,M=1的页。
- 先进先出页面置换算法(First-In,First-Out Page Replacement Algorithm):淘汰在内存中最久的页,这种算法的性能接近于随机淘汰。并不好。
- 改进型FIFO算法(Second Chance Page Replacement Algorithm):这种算法是在FIFO的基础上,为了避免置换出经常使用的页,增加一个标志位R,如果最近使用过将R置1,当页将会淘汰时,如果R为1,则不淘汰页,将R置0.而那些R=0的页将被淘汰时,直接淘汰。
- 时钟替换算法(Clock Page Replacement Algorithm):虽然改进型FIFO算法避免置换出常用的页,但由于需要经常移动页,效率并不高。因此在改进型FIFO算法的基础上,将队列首位相连形成一个环路,当缺页中断产生时,从当前位置开始找R=0的页,而所经过的R=1的页被置0,并不需要移动页。
- 最久未使用算法(LRU Page Replacement Algorithm):LRU算法的思路是淘汰最近最长未使用的页。这种算法性能比较好,但实现起来比较困难。