操作系统学习笔记:虚拟内存

时间:2022-02-20 00:57:26

一、绪论

操作系统的各种内存管理策略都出于同一个目的:同时将多个进程存放在内存中,以便允许多道程序设计。不过,这些策略都需要在进程执行之前将整个进程放在内存中。动态载入虽然能减轻这个限制,但需要程序员小心应用,并且花费额外的工作。

而虚拟内存则允许执行进程部分在内存中,一个显著的优点是程序可以比物理内存大。而且虚拟内存将内存抽象成一个巨大的数组,将用户视界的逻辑内存与物理内存分离,使得程序员不受内存存储的限制。简而言之,虚拟内存展现在程序员面前的是一个比物理内存要大得多的、地址连续的内存空间,而事实上是映射到支离破碎的物理内存,乃至磁盘上。

操作系统学习笔记:虚拟内存


然而,虚拟内存的实现并不容易,使用不当反而可能大大地降低性能。

二、按需调页

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;

如果页刚好大小为128字,那么上述写法就相当于每个内循环都要调一个页,且每调一次都只是为了修改一个数。如果分配给该进程的帧数小于128,那么一共会产生 128 * 128 = 16384 个页错误!

但假如这样写:

for(int i=0;i<128;i++)
     for(int j=0;j<128;j++)
           data[i][j] = 0;


每调一页,都将该页上的数修改完毕才调下一页,总共产生128个页错误。


6、I/O互锁

允许页在内存中被锁住。

在全局置换算法中,一个进程发出IO请求,被加入到IO设备等待队列,而CPU交给了其他进程。这些进程发生页错误,偏偏置换了等待进程用于IO的缓存页,这些页被换出。好了,请求IO的进程等待到了IO设备,针对指定地址进行IO,然而帧早被其他进程的不同页所使用。

对这个问题,通常有两种解决方法:

1)绝不对用户内存进行IO,如果要进行IO,将用户内存数据复制到系统内存。要复制一次,开销太高了。

2)物理帧有一个锁住位,允许页锁在内存中。如果锁住,则不能置换。当IO完成,页被解锁。

锁住位用处多多,比如操作系统内核页通常加锁;低优先级进程的页至少要运行一次才能解锁被置换。

版权声明:本文为博主原屙文章,喜欢你就担走。