频道栏目
读书频道 > 系统 > windows > Windows内核原理与实现
4.1.3 内存管理算法介绍
2013-05-18 15:05:00     我来说两句
收藏   我要投稿

本文所属图书 > Windows内核原理与实现

本书从操作系统原理的角度,详细解析了Windows如何实现现代操作系统的各个关键部件,包括进程、线程、物理内存和虚拟内存的管理,Windows中的同步和并发性支持,以及Windows的I/O模型。在介绍这些关键部件时,本...  立即去当当网订购

内存是计算机系统中除了处理器以外最为重要的资源,任何一个程序的运行都离不开内存资源的有效使用。前面两小节介绍了硬件支持的内存管理机制,尤其是如何将虚拟地址或者逻辑地址转译成物理内存地址。这一节我们将首先讨论在一个地址空间内部如何有效地进行动态内存管理,然后介绍常用的页面替换算法,以及在进程内存管理中常常用到的工作集概念和相应的算法。

假设操作系统或者一个进程已经获得了一块连续地址的内存,系统或进程在执行过程中需要利用这块内存来满足各种内存请求。由于内存请求存在动态性,即每次请求的内存大小可能不相同,甚至差异很大,并且这些小内存块的生命周期也不尽相同,所以,系统需要提供合适的算法来尽可能地满足这些动态的内存请求。在现代计算机系统中,堆(heap )正是这样一个提供动态内存分配能力的内存抽象。操作系统使用堆来满足各种动态内存请求,应用程序通过堆获得内存。我们常用的C/C++ 基本运行库提供了堆内存管理的能力,所以,C/C++ 程序中的 malloc/free 和new/delete 可以直接在堆的接口上工作。
 
就本质而言,内存管理算法可以分为两大类:位图标记法和空闲链表法。位图标记法的思路很简单:假设总的待分配内存的大小为N 字节,管理内存的粒度为 M 字节,并且N= M×K,也就是说,内存管理的基本单元为 M 字节,现在共有 K 个单元需要动态管理。为了记录这K 个内存单元的使用情况,位图标记法将使用一个共有K 位的位图(bitmap ),其中每一位的值(0 或者1)用来说明这一位所对应的内存单元是否空闲。由于位图精确地记录了每一个内存单元的空闲或已被使用的情况,所以,当内存管理器接收到一个新的内存申请时,只须扫描位图,就能确定是否有合适的空闲内存可以满足此请求。其做法是,根据所请求内存的大小,确定需要多少个连续的内存单元来满足此请求,然后在位图中扫描是否存在连续这么多0 位,如找到了,则将它们所对应的内存分配给客户,并且将这些位置成1。当释放内存时,要求客户指定待释放内存的起始地址和大小,这样内存管理器就可以计算出此次内存释放对应于位图中哪些连续位,并且将它们置成0。

位图标记法的实现比较简单,但是需要额外的内存开销,通常为N/8 × M,所以,只需适当地选取M,就可以控制这部分额外开销。当然,用户请求的内存大小不一定是M 字节的倍数,因而在分配内存时有一定的浪费。平均而言,每次用户请求将导致M/2 字节的浪费。另外,内存管理器在分配内存时需要扫描连续多个 0 位,此操作并不高效(复杂度为O(K)),这是该算法的一个缺点。

另外一类动态内存管理方法使用链表来描述已分配的和空闲的内存块,称为空闲链表法。在初始时,整个内存块被当做一个大的空闲块加入到空闲链表中。以后,当内存管理器接收到一个内存分配请求时,将从空闲链表中找到一个合适的、能提供足够内存的空闲块,并从该空闲块中分离出足够多的内存,交给客户,剩下的内存(如果还有的话)仍然是一个空闲块,而已分配的内存则加入到已分配内存链表中。当释放一块内存时,内存管理器将已分配的内存块从已分配链表转移到空闲链表中,如果有可能,与相邻的内存块合并以便构成更大的空闲块,从而尽可能地满足客户的大内存请求。在这一类方法中,当内存管理器接收到内存请求时,将按照以下不同的策略来查找适当的空闲内存块:

(a)  最先匹配法(first-fit ),从空闲链表中找到第一个满足客户请求的空闲块。

(b)  最佳匹配法(best-fit ),从空闲链表中找到最接近于客户请求大小的空闲块。

(c)  最差匹配法(worst-fit),从空闲链表中找到最大的空闲块。

(d)  下一个匹配法(next-fit ),从空闲链表的当前位置往后扫描,找到第一个满足客户请求的空闲块。

除了以上介绍的位图标记法和空闲链表法以外,还有两种内存管理算法也值得介绍一下:slab 算法和伙伴系统(buddy system )。Slab算法实际上是以上介绍的位图标记法和空闲链表法的结合,它针对某个阈值以下的内存块使用位图法,按照2 的幂次,每一阶有一块内存和对应的位图;在此阈值以上,slab 算法使用链表来管理内存。Linux 和Solaris系统的内核使用了slab 算法。

Slab 算法对于小内存的分配非常快速高效,但也有空间浪费,当所请求的内存大小介于2 的两个连续幂次之间时,所分配的内存块(大小为2 的幂次)就存在部分空间浪费。浪费的部分称为内碎片(internal fragmentation ),因为它位于已分配的内存块内部。对应地,如果碎片位于已分配的两个内存块之间,则称为外碎片(external fragmentation)。例如,前面介绍的空闲链表法在经过一段时间的动态内存分配和释放以后,往往会造成很多外碎片,因此,即使实际的空闲内存还有很多,但由于外碎片的原因,对于较大内存的申请也往往无法满足。

解决外碎片问题的一个算法是伙伴系统(buddy system)。下面以二进制伙伴系统为例来说明它的主要思想。首先,假设待分配的整块内存的大小为2 的幂次,比如说2m字节,每个字节相对于基地址的偏移量为0,1,2,…,2m−1;另有一个数组avail[m],其中每个元素avail[i] 记录了大小为 2i+1的内存块的空闲链表头。内存的分配按照2 的幂次进行,也就是说,分配给客户的内存块总是2 的幂次方,不管客户是否真正需要这么多内存。对于任一大小为 2i的内存块,假设其相对于基地址的偏移量为 p,则该内存块的伙伴被定义为,p 的第i+1 位取补码加上基地址,这样得到的地址所指向的同样大小的内存块。图4. 9 演示了伙伴内存块和非伙伴内存块。


 

伙伴系统在初始时,整个待分配内存块都是空闲的,所以,avail[m-1]链表指向此内存块,该数组中其他所有的链表都是空的,当客户申请一块大小为k 的内存块时,伙伴系统在所有≥   logk  的avail[] 链表中查找空闲块,从第一个找到的非空链表中找出一个内存块,经过分裂变成合适大小,然后返回给客户。因此,伙伴系统的内存分配过程实际上是大内存块分裂成小内存块的过程,分裂得到的小内存块一部分给客户,剩下的挂到适当的空闲链表中,以备下次继续分配给客户。

在内存回收过程中,如果待回收的内存块与链表中已有的一块内存互为伙伴,则它们可以合并成更大的内存块,从而转移到大内存块对应的空闲链表中。因此,内存块的合并在互为伙伴的内存块之间进行,并且从小到大,一直到不能合并为止。

伙伴系统的内存分配和回收的执行效率比较高(O(logn) ),但也有问题:第一,空间利用率的问题,由于它总是按照 2 的幂次来分配内存块,所以,如果客户总是按照略大于2 的幂次来申请内存,则空间浪费的现象将较为严重;第二,外碎片问题仍然存在,例如,两个相邻的非伙伴内存块即使能满足客户的内存要求,伙伴系统也不会把它们连起来分配给客户。有一些改进的伙伴系统能缓解这些问题,但效率可能不如二进制伙伴系统这么好。关于伙伴系统的更详细信息,读者可以参考相关文献[TAOCP-1]。

接下来我们讨论在页式内存管理系统中,当物理内存紧缺时,该从哪些进程中选择哪些页面,把它们的内容写到磁盘上,从而腾出这些页面所对应的物理页面,以便用于后续的内存需要。由于在一个实际的多进程、多任务系统中,所有进程使用的页面总数可能会超过系统中的页面数量,因此,当一个进程向系统请求更多的物理页面时,系统必须有一套算法或策略来保证适时地满足该进程的需要。对于操作系统而言,这实际上也是在页面粒度上的物理内存管理,其中的算法往往称为页面替换算法。通常以下一些算法是值得考虑的[MOS]:

最优页面替换算法(The Optimal Page Replacement Algorithm)。这是一个理论上最优的算法,它要求能够预测每个页面下次使用的时间,从而确定该页面还需要等待多长时间才会真正使用,所以,在选择该换出哪些页面时,优先考虑那些等待时间最长的页面。此算法的基本原理是,越是频繁使用的页面,越应该留在物理内存中;相对而言,如果要换出换入的话,应尽可能选择那些不会被频繁使用的页面,或者在某段时间内不会被频繁使用的页面。最终达到的效果是减少页面换入换出的次数。但是,这一算法的问题是,预测一个页面下次被使用的时间在实际系统中往往是不可行的,除非是在做事后分析。因此这种算法可以当做一个基准来对已有的算法做性能评价,具体做法是,在一个系统中记录下每次页面被访问的历史痕迹数据,有了这些数据以后,就可以在模拟环境中,使用最优页面替换算法,得到最少的换页次数,作为被评估算法的一个理论最优参照。

最近未使用(NRU)页面替换算法(The Not Recently Used Page Replacement Algorithm)。这一算法的思路是,当系统需要物理页面时,检查所有的页面,优先替换那些最近(所谓最近,是一个相对时间,比如,最近的几个时钟滴答)一直没有被访问或修改的页面。为了建立起最近是否被访问或修改的参考依据,通常每个页面需要记录下它被访问的情况,从效率来考虑,这往往需要硬件的支持。有两个标志位具有特别的意义:访问位R 和修改位M。当一个页面被初次使用时,它的访问位 R 被置上,若页面被修改,则修改位M 被置上。在进程的运行过程中,R 位被定期清除,这样系统就能区分最近未被访问过的页面和最近被访问过的页面。当需要替换页面时,首先找那些未被访问过的页面;如果还不够,则继续找已被访问但未被修改过的页面;如果还需要更多的页面,则只好找那些已被访问且已被修改过的页面。这一算法相对简单并且易实现,尤其在硬件的支持下可以有较好的性能,虽然它不是最优的,但在实用中是有效的。

先进先出页面替换算法(The First-In First-Out Page Replacement Algorithm)。顾名思义,这种算法的思路是把所有已在内存中的页面组织成一个队列(也可以是一个链表),每次当有页面换入到内存中的时候,就添加到队列的末尾;当需要页面换出时,直接从队列中移除页面。这一算法听起来很有道理,把留在内存中时间最长的页面换出内存,但实际上,经常要访问的页面也不得不在队列中流动,从而会造成不必要的换出和换入开销。由于这样的原因,在实践中,这一算法很少被单独使用。

第二次机会页面替换算法(The Second Chance Page Replacement Algorithm)。这是对先进先出页面替换算法的改进,很自然,对于最老的页面,即队列头的页面,如果它的访问位R 为0,则说明这个页面不仅老,而且很久没用了,理应换出去;如果R 位非0,则说明该页面最近被访问过,因此再给它一次机会,做法是,先把 R 位清零,然后把它移到队列尾,就好像它是一个新换入的页面一样。然后系统再进一步检查队列头的页面。如果队列中的页面最近都被访问过,那么,它们将被依次检查一遍并清除其访问位,然后在下次再被检查到的时候被依次换出内存。

第二次机会页面替换算法在具体实现的时候,有一种优化方法,它可以避免在队列中频繁地移动页面,而是把页面组织成环形链表,然后用一个指针指向时间上最早加入的页面。这样做的好处是,如果有一个页面被检查了访问位以后,并不从链表中清除,则只须清除其访问位,并移动指针指向下一个页面即可,无须将页面从链表头移到链表尾。由于这种做法就像一个时钟的指针在钟面上移动一样,因此该算法有时候也被称为时钟页面替换算法(The Clock Page Replacement Algorithm )。其实质跟第二次机会页面替换算法完全相同,只是实现上不同而已。

最近最久未使用(LRU )页面替换算法(The Least Recently Used Page Replacement Algorithm)。这一算法的思路是,当系统在选择换出一个页面的时候,优先考虑最近最久未使用的那个页面。此算法实际上是对最优页面替换算法的一个估计,其依据是页面访问的局部性原理。既然无法直接测量页面将来被访问的时间,不妨用最近一段时间内页面被访问的频率来猜测它将来被访问的情况,从而协助作出页面替换决策。若在最近一段时间内,某些页面被频繁地访问,则在将来的一段时间内,它们还可能会被频繁地访问。反之,若某些页面长时间未被访问,则在将来,它们极有可能仍然长时间不会被访问,所以,在选择页面的时候优先考虑这些页面。

最近最久未使用(LRU)页面替换算法与前面介绍的最近未使用(NRU)算法并不一样。NRU算法基于页面的访问位和修改位来作出决定,而 LRU算法基于页面最近被访问的时间长短来作出选择。在实现 LRU算法时,要求能够定位到最久没有使用过的页面,这可以通过维护一个页面链表来实现,但每次访问一个页面都要把这个页面移到链表首,表示它刚刚被访问过。因而算法的维护成本较高,难以硬件实现。

最不经常使用(NFU)页面替换算法(The Not Frequently Used Page Replacement Algorithm)。此算法的思路与 LRU一致,它主要是提供了一种软件实现。其做法是,为每个页面维护一个计数器,初值为 0。每次时钟中断时,系统对所有页面,把它们的访问位(0 或者1)加到计数器上,这样,经常被访问的页面就有机会增加其计数器,而不常访问的页面其计数器相对较少得到增加。但这个算法的问题在于,计数器只增不减,这意味着页面的历史会长久地影响页面替换算法的决策。

对NFU算法的一个改进算法称为页面老化算法(page aging algorithm)。它对 NFU做了修改,使其更好地模拟LRU算法。其做法是,在时钟中断修改页面计数器的时候,并不是简单地递增计数器,而是先把计数器的值右移一位,然后把访问位R 加到计数器的最左边位上,而不是最右边位。经过这样修改以后,如果一个页面经过了一段频繁访问的时间过后,它慢慢地不再被访问了,则因频繁访问而对计数器的影响在经过几次右移位以后,逐渐消除了,取而代之的是最近该页面被访问的情况。

老化算法只用有限个位来模拟页面最近被访问的情况,它提供的计数器并非精确的时间计数值,而只是一个相对的最近被访问的参照,但是其优点在于,它能够逐渐地抹去历史的影响,而让最近一段时间的被访问情况参与到决策中。在实践中,这些相对久远的历史对于决策的重要性并不高,所以,老化算法比较具有实际意义。

以上介绍了操作系统在替换页面时的一些常用算法和选择依据。现在我们来看一看,系统在进程层次上是如何管理和控制物理内存资源的。这对于多进程系统有重要的意义,因为尽管每个进程都有非常大的虚拟地址空间(比如在32位Windows 上有2 GB或3 GB私有的虚拟空间),但它们能得到的物理内存往往只是相对较少的一部分,进程之间实际上是在争抢有限的物理内存资源。所以,操作系统必须小心地平衡每个进程的需求和分配给它的内存。为了衡量进程得到的物理内存资源,这里首先介绍进程工作集的概念。

当一个进程被创建并开始运行时,最初所有的页面都还在磁盘上(包括进程的可执行文件),随着控制流不断前进,全局数据和栈的地址范围被不停地访问到,并且动态内存的需求也开始出现,该进程逐渐获得越来越多的物理内存页面。对内存页面的请求和满足通常是以中断或异常的方式来完成的。随着进程占有的物理内存越来越多,它的运行趋向平滑,因为对于物理内存的需求开始减少。操作系统根据需要而分配物理页面的做法称为按需换页(demand paging)。

结合前面介绍的页面替换策略,我们可以理解:操作系统在内存紧缺时将根据一定的算法和规则,向进程要回物理页面(也就是说,选择哪些页面被替换);而进程则在必要的时候向系统请求更多的物理页面。操作系统就在这两者之间管理着有限的物理内存资源。那么,对于一个进程而言,一方面,当它占用物理内存太多时,自然地,有些页面会被操作系统收回去;另一方面,当它的执行逻辑需要更多物理内存时,操作系统可以把当前空闲的或者收回来的(既可能从其他进程收回来,也可能从它自身所占的内存中收回来)物理页面分配给它。在任一给定时刻,进程所占的物理内存是确定的;从一个过程来看,它所占的内存数量呈现出动态变化的特性。工作集模型正是刻画一个进程的内存使用情况的模型。这里,工作集(working set)指一个进程当前正在使用的物理页面的集合。

应用程序在工作时,对于内存的访问通常呈现出一定的局部性,也就是说,在一段时间内,程序对内存的访问往往集中在一定的范围内。这也意味着,进程的工作集的变化相对而言是缓慢的。进程工作集变化越缓慢,则单位时间内页面换入换出发生的次数越少,这当然有利于进程的运行,它的性能自然越好;另一方面,进程的工作集也是由操作系统来管理和控制的,工作集越大,则它的变化自然越缓慢。因此,工作集管理也是操作系统内存管理的一个重要方面。
 
在工作集理论模型中,进程的工作集可以用一个二元函数 w( t ,   δ  )来表示,其中 t 代表时间点,δ   代表一段时间间隔,也称为工作集窗口。w( t ,   δ  )表示t - δ  与t 两个时间点之间进程所访问到的页面集合,显然,随着δ  的增大,w( t ,   δ  )只可能增加而不会减小,即 w( t ,   δ  )是δ  的单调非递减函数。但由于程序的内存访问的局部性原则,w( t ,   δ  )的递增在δ  较小时很快,然后就会稳定下来,其曲线大致如图4. 10所示。当δ  大到一定程度,w  ( t ,  δ  )可能又会有一段快速增长,然后稳定下来。当δ  较大时,w 曲线取决于程序的执行逻辑。


 

工作集理论模型可以用来指导对进程页面的有效管理,例如,在进程初始执行期间,进程的工作集很快增长,但到一定时候,工作集就会稳定下来。因此,一种有效的优化手段是,记录下工作集稳定下来以后某一时刻的工作集内容(即进程中的哪些页面被访问了),下次该进程启动时,直接为这些页面赋予物理内存,并且从磁盘加载相应的内容,这样可以避免以按需换页的方式逐渐地从磁盘读取文件内容,从而大大加快进程的启动速度。Windows 使用了这种优化手段,称为逻辑预取器(Logical Prefetcher)。

那么,如何维护进程工作集信息呢?一种简便的做法是,记录每个页面最近被访问的时间,这样,根据预设的δ  值,一旦当前时间超过了该页面最近被访问的时间再加上δ 值,则从工作集中删除此页面。根据工作集理论模型,预设的δ   值(当然不能太小)对于工作集并没有很大的影响。而且,工作集的这一维护机制可以与页面老化算法有机地结合起来,在实践中这不难做到,例如直接根据页面的老化程度来决定是否从工作集中移除一个页面。

有了进程工作集的信息以后,我们可以用这些信息来改进页面替换算法。例如,在前面介绍的时钟页面替换算法中,如果指针所指的页面的访问位为0,则意味着该页面可以被替换,但现在有了工作集信息以后,需进一步检查此页面是否属于当前进程的工作集,如果是,则不被替换,算法继续往前查找其他的页面。这一改进算法称为WSClock[WSCLOCK]。

您对本文章有什么意见或着疑问吗?请到论坛讨论您的关注和建议是我们前行的参考和动力  
上一篇:4.1.2 段式内存管理
下一篇:4.1.4 Windows内存管理概述
相关文章
图文推荐
排行
热门
最新书评
文章
下载
读书
特别推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站