zoukankan      html  css  js  c++  java
  • 操作系统学习笔记:虚拟内存

    一、绪论

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

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



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

    二、按需调页

    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完成,页被解锁。

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

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

  • 相关阅读:
    【LeetCode OJ】Remove Element
    【LeetCode OJ】Remove Duplicates from Sorted Array
    【LeetCode OJ】Swap Nodes in Pairs
    【LeetCode OJ】Merge Two Sorted Lists
    【LeetCode OJ】Remove Nth Node From End of List
    【LeetCode OJ】Two Sum
    【LeetCode OJ】Majority Element
    最长公共子序列问题
    php fopen与file_get_contents的区别
    PHP 技巧集合
  • 原文地址:https://www.cnblogs.com/leftfist/p/4764241.html
Copyright © 2011-2022 走看看