一、绪论
操作系统的各种内存管理策略都出于同一个目的:同时将多个进程存放在内存中,以便允许多道程序设计。不过,这些策略都需要在进程执行之前将整个进程放在内存中。动态载入虽然能减轻这个限制,但需要程序员小心应用,并且花费额外的工作。
而虚拟内存则允许执行进程部分在内存中,一个显著的优点是程序可以比物理内存大。而且虚拟内存将内存抽象成一个巨大的数组,将用户视界的逻辑内存与物理内存分离,使得程序员不受内存存储的限制。简而言之,虚拟内存展现在程序员面前的是一个比物理内存要大得多的、地址连续的内存空间,而事实上是映射到支离破碎的物理内存,乃至磁盘上。
然而,虚拟内存的实现并不容易,使用不当反而可能大大地降低性能。
二、按需调页
1、基本概念
页需要用到的时候才调入内存。
这种方案需要硬件支持区分哪些页在内存,哪些在磁盘。采用有效/无效位来表示。当页表中,一个条目的该位为有效时,表示该页合法且在内存中;反之,可能非法,也可能合法但不在内存中。
当进程试图访问这些尚未调入内存的页时,会引起页错误陷阱(page-fault trap)。按以下步骤进行处理:
1)检查进程内部页表,通常与PCB一起保存。以确定该引用的合法性
2)如果非法,进程终止;否则进行调入:
3)找到一个空闲帧
4)调度一个磁盘操作,将所需页调入刚分配的帧
5)磁盘读操作完成后,修改内部表和页表(有效无效位?),表示该页已在内存中
6)重新开始因陷阱而中断的指令。
2、按需调页的性能
对于按需调页,降低页错误率至关重要。
另外是对交换空间的处理的使用。磁盘IO到交换空间通常比到文件系统要快,因为交换空间是按大块进行分配,并不使用文件查找和间接分配方法。因此,在进程开始时将整个文件镜像复制到交换空间,并从空间交换执行按页调度,那么有可能获得更好的性能。
另一种选择是开始时从文件系统进行按需调页,但置换出来的页写入交换空间,而后的调页则从交换空间中读取。这种方法确保只有需要的页才从文件系统中调入,又可以保证一定的性能。
三、写时复制
有些进程,比如fork()出来的子进程,并不需要按需调页,而是一开始与父进程共享页面,当子进程需要修改页的时候,才对该页复制一个副本,在副本上进行修改。是为写时复制。
当一个页需要写时复制的时候,从哪里分配空闲页很重要。许多操作系统为此提供空闲缓冲池。
四、页面置换
内存有时会过度分配,进程需要使用的页大于可分配内存;加上内存并不仅用于进程的页,IO缓存也需要使用大量的内存,会出现内存相对需求僧多粥少的局面,这时进程发生页错误的时候,操作系统准备好了要调入的所需页,却发现没有空闲帧可供分配。正所谓房子看好了,车也看好了,一切都看双色球了。
1、页置换
遇到这种情况,操作系统可以选择终止该嗷嗷待哺的进程,也可以交换出一个倒霉的进程。更多的时候,会采用页置换的方式:
如果没有空闲帧,就查找当前没有使用的帧,将其释放,空出来保存进程出错的页(也就是需要换入的页)。
如果换出的页有修改的话,还必须将页写回磁盘。可以通过设置修改位或脏位来提高性能。
页置换算法:
2、FIFO页置换
最简单的页置换算法。选择最旧的页进行置换。具体为创建一个FIFO队列来管理内存中的所有页,队列中的首页被置换,而新调入的页则加到队列的尾部。
FIFO算法容易理解和实现,但性能不总是很好。所替代的页可能仍在使用,换出去以后马上报页错误,要求换回来。
3、最优置换
置换最长时间不使用的页(不是久未使用,而是预测其未来经过最长时间才被使用?)。这种算法页错误率最低。
这种算法问题在于难以实现。
4、LRU页置换
最优置换的近似。最优置换与FIFO的关键区别在于,FIFO使用的是页调入时间,而最优置换看重的是页将来使用的时间。如果使用离过去最近作为不远将来的近似,那么可置换最长时间没有使用的页。根据过去来猜测未来。这种方法称为 最近最少使用算法。
实现LRU算法,可用计数器,也可用栈:凡用过的页,就放到顶部,不用的就沉到栈底。
5、近似LRU页置换
很少有计算机系统能提供足够的硬件来支持真正的LRU页置换。然而,许多系统通过引用位方式来进行近似置换:
页表内的每个条目都关联一个引用位,每当引用一个页时,相应的引用位就被硬件置位;
刚开始时,所有引用位都清零,后来许多被置为1。通过检查引用位,可以知道哪些页使用过而哪些没有。这个信息是近似LRU置换算法的基础。
近似LRU置位算法有几种:
1)附加引用位算法
每页有一个8位的字节做引用位,定期刷新引用位。有引用的时候该字节最高位置1,其他位右移,挤掉原来的最低位。那么,引用位为最小值的页就可以被置换。
2)二次机会算法
当一个倒霉的页被选中时,检查其引用位,如果为0,直接置换掉;如果引用位为1,就给它一次机会,放过它,继续找下一张倒霉页。那张获得重生机会的页,其引用位清零,重置时间。在所有页都被寻找过一遍之前,它起码不会被替换掉。
3)增强型二次机会算法
通过将引用位和修改位作为一有序对来考虑:
(0,0)最近无使用也无修改:换吧,别犹豫了
(0,1)最近无使用但有修改:置换前要写回磁盘,请三思!
(1,0)最近有使用但无修改:可能很快又要使用
(1,1)最近有使用且有修改:可能很快又要使用,且置换前要写回磁盘,请三思!
6、基于计数的页置换
为每个页设置一个计数器,形成两种方案
1)最不经常使用页置换算法(LFU)
置换计数最小页。理由是活动页应该有更大的引用次数。但可能有如下问题:一个页可能开始时使用很多,但以后就不再使用。解决方法是定期将次数寄存器右移一位,以形成指数衰减的平均使用次数。
2)最常使用页置换算法(MFU)
置换计数最大页。理由:最小次数页可能刚刚调进来,且还没使用。
7、页缓冲算法
保留一个空闲帧缓冲池。
1)维护一个已修改页的列表。每当调页设备空闲时,就选择一个修改页写到磁盘上,并重置其修改位。这种方案增加了干净页,降低了置换时写出的概率。
2)保留一个空闲帧池,记住页与帧的对应关系。当帧需要重用时,就先从池中取,没有磁盘IO。
8、应用程序与页置换
有时,应用程序通过操作系统使用虚拟内存结果会更坏。数据库就是一个例子。因为数据库可提供自己的内存管理和IO缓冲,因为它更能理解自己的内存使用和磁盘使用。基于此,操作系统允许特殊程序将磁盘当成逻辑块数组使用,而无需通过操作系统的文件系统。
五、帧分配
如何在各个进程之间分配一定的空闲内存?
简单办法是将帧挂在空闲帧链表上,当发生页错误之时即进行分配。进程终止时帧再次放回空闲帧链表。
帧分配策略受到多方面限制。例如, 分配数不能超过可用帧数,也必须分配至少最少数量。保证最少量的原因之一是性能。页错误增加会减慢进程的执行。并且,在指令完成前出现页错误,该指令必须重新执行。所以有足够的帧至关重要。
每个进程帧的最少数量由体系结构决定,而最大数量是由可用物理内存数量决定。
1、帧分配算法有
1)平均分配,每个进程一样多
2)按进程大小使用比例分
3)按进程优先级分
4)大小和优先级组合分
2、全局分配和局部分配
全局置换允许进程从所有帧集合中选择一个进行置换,而不管该帧是否已分配给其他进程,即它可以从其他进程抢夺帧,比如高优先级抢夺低优先级的帧;局部分配则要求每个进程只能从自己的分配帧中分配。
全局置换通常有更好的吞吐量,且更为常用。
六、系统颠簸
进程如果没有它所需要的帧,那么很快产生页错误,这时必须置换某个页。然而所有页都在使用,置换一个,立刻又要换回来,页错误频繁在发生,称为颠簸。
颠簸导致严重的性能问题。操作系统时刻注视CPU的使用率,如果CPU使用率太低,系统会引入新进程。采用全局置换算法,可不管页属于哪个进程,抢到就换。假设一个进程需要更多帧,开始出现页错误,从其他进程抢到帧。被抢的进程从就绪队列移出,CPU使用率下降;CPU调度程序发现后,调入更多进程,企图让CPU嗨起来。新进来的进程嗷嗷待哺,帧被抢夺得更激烈,等待队列更长,CPU使用率进一步下降,CPU调度程序更努力地调入更多的进程。。。
最终,进程主要忙于调页,系统不能完成一件工作。
使用局部置换可以限制系统颠簸,但不能完全解决这个问题。
1、工作集合模型
为了防止颠簸,进程必须获得足够多的帧才可以启动。操作系统跟踪每个进程的工作集合,为其分配大于其工作集合的帧数。如果还有空闲,才有可能启动另一进程。如果某个进程所有工作集合之和超过了可用帧总数,那么会被暂停,其帧分配给其他进程。挂起的进程等待以后重启。此为工作集合模型。困难在于跟踪工作集合。
2、页错误频率策略
除了工作集合,另一种防止颠簸的方案是页错误频率策略。
如果一个进程,页错误频率太高,说明需要更多的帧,给它!如果页错误频率太低,说明帧有富余,分些给别人。为进程设置页错误率上下限,机动地分配帧。
与工作集合模型一样,如果需要帧却无帧可分配,那么进程应该暂停,释放给其他同样高页错误频率的进程。
七、内存映射文件
通常,文件每次访问都需要一个系统调用和磁盘访问,但还有另一种方法:使用虚拟内存技术将文件IO作为普通内存进行访问。意思就是说,访问文件就像访问内存一样。
1、基本机制
将磁盘块映射成内存页(一页或多页)。刚开始时,页面调度,会产生页错误,这样,文件内容陆续读入物理内存矣。文件的读写就像内存访问一样,通过内存操作文件而不是系统调用read()和write(),从而简化。
其中,对文件的写可能不会立即写到磁盘上,除非脏页置换或操作系统定期检查,或者文件关闭?
如果一个文件多个进程共用,那么将其映射到各自的虚拟内存中,以允许数据共享。任一进程修改虚拟内存中的数据,其他进程都可以见到。如果有修改,则是修改各自的副本,写时复制。可能还有互斥。
2、WIN32 API 的共享内存
将存在于磁盘的文件放进一个进程的虚拟地址空间,并在该进程的虚拟地址空间中产生一个区域用于“存放”该文件,这个空间就叫做File View(存放在进程的虚拟内存中),系统并同时产生一个File Mapping Object(存放于物理内存中)用于维持这种映射关系,这样当多个进程需要读写那个文件的数据时,它们的File View其实对应的都是同一个File Mapping Object,这样做可节省内存和保持数据的同步性,并达到数据共享的目的。
3、内存映射IO
将IO设备映射到内存,那么对该部分内存进行读写,就如同对IO设备进行读写,而不必直接操作IO设备。比如说,屏幕上每一个点都对应一个内存地址,程序控制内存,就能控制屏幕显示。
八、内核内存的分配
当用户态进程需要额外内存时,可以从内核所维护的空闲页帧链表中获取页。通常,页帧分散在物理内存中,但是内核内存通常从空闲内存池中获取,主要由两个原因:
1)内核需要为大小不同的数据结构分配内存,因此必须节省使用,并尽量减低碎片浪费。许多操作系统的内核代码与数据不受分页系统控制
2)有的硬件需要直接与物理内存打交道,而不经过虚拟内存接口,因此需要内存常驻在连续的物理页中
内核进程进行内存管理的两个方法:
1、Buddy系统
从物理上连续、大小固定的段上进行分配,按2的幂大小来进行分配,如4K、8K等。优点是可通过合并而快速形成更大的段,但容易产生碎片。
2、slab分配
按照内核对象的数据结构要求的大小,预先分配好若干内存块,等待召唤使用。
具体来说,内核对象对应有高速缓存,而高速缓存含有若干个slab(就是尺寸合适的内存块?)。slab可有三种状态:满的、空的、部分。当分配的时候,先从空闲状态部分分配,不够从空的部分分配;还不够就从物理连续页上分配新的。
优点:
1)尺寸因应内核对象要求可变,没有碎片
2)预先准备,可快速满足要求
九、其他考虑
1、预调页
纯按需调页的一个显著特性是当一个进程开始时会出现大量页错误。而预调页的策略是同时将所需的所有页调入内存。关键是成本是否小于相应页错误的成本。
2、页大小
该用大页还是小页,是个问题。
1)大页有利于减少页表
2)小页有利于减少碎片,可更好地利用内存
3)小页传输快,大页IO好,但又不一定,小页因为寻址、传输快,局部性得以改善,总的IO就会降低,那么,应该用小页?
4)然而,大页可以降低页错误数量
……
切克闹,现在你告诉我,该用大页还是小页?
3、TLB范围
TLB可提高内存访问速度,如果没有TLB,则每次取数据都需要两次访问内存,即查页表获得物理地址和取数据。
TLB只维护页表中的一小部分条目,逻辑地址转换物理地址过程中,先在TLB中查找,如果找到,那么物理地址唾手可得;如果TLB中没有,那么使用置换算法,将相关条目置换进TLB,然后再得到物理地址。
那么提高TLB命中率至关重要。
提高TLB命中率可增加TLB条数,但代价不小,因为用于构造TLB的相关内存既昂贵又费电。另一个方法是增加页的大小,或提供多种页大小。
4、反向页表
反向页表可以节省内存,不过,当进程所引用的页不在内存中时,仍然需要一个外部页表以获得物理帧保存哪个虚拟内存页面的信息。所幸这只是在页错误时才需要用到,外部页表本身可以换出换入,不苛求一定完备。
5、程序结构
我们平常写程序,对内存根本不用关心。但有时了解一点内存知识可改善系统性能:
比方说,有一个128*128的二维数组,数据按行存放,如何遍历性能高?
int i,j; int[128][128] data;
for(int j=0;j<128;j++) for(int i=0;i<128;i++) data[i][j] = 0;
但假如这样写:
for(int i=0;i<128;i++) for(int j=0;j<128;j++) data[i][j] = 0;
6、I/O互锁
允许页在内存中被锁住。
在全局置换算法中,一个进程发出IO请求,被加入到IO设备等待队列,而CPU交给了其他进程。这些进程发生页错误,偏偏置换了等待进程用于IO的缓存页,这些页被换出。好了,请求IO的进程等待到了IO设备,针对指定地址进行IO,然而帧早被其他进程的不同页所使用。
对这个问题,通常有两种解决方法:
1)绝不对用户内存进行IO,如果要进行IO,将用户内存数据复制到系统内存。要复制一次,开销太高了。
2)物理帧有一个锁住位,允许页锁在内存中。如果锁住,则不能置换。当IO完成,页被解锁。
锁住位用处多多,比如操作系统内核页通常加锁;低优先级进程的页至少要运行一次才能解锁被置换。