zoukankan      html  css  js  c++  java
  • 操作系统内存详解

    进程的简单介绍
    进程是占有资源的最小单位,这个资源当然包括内存。在现代操作系统中,每个进程所能访问的内存是互相独立的(一些交换区除外)。而进程中的线程可以共享进程所分配的内存空间。
    在操作系统的角度来看,进程=程序+数据+PCB(进程控制块)

    没有内存抽象
    在早些的操作系统中,并没有引入内存抽象的概念。程序直接访问和操作的都是物理内存。比如当执行如下指令时:
    mov reg1,1000

    这条指令会将物理地址1000中的内容赋值给寄存器。不难想象,这种内存操作方式使得操作系统中存在多进程变得完全不可能,比如MS-DOS,你必须执行完一条指令后才能接着执行下一条。如果是多进程的话,由于直接操作物理内存地址,当一个进程给内存地址1000赋值后,另一个进程也同样给内存地址赋值,那么第二个进程对内存的赋值会覆盖第一个进程所赋的值,这回造成两条进程同时崩溃。

    没有内存抽象对于内存的管理通常非常简单,除去操作系统所用的内存之外,全部给用户程序使用。或是在内存中多留一片区域给驱动程序使用,如图1所示。

     

    第一种情况操作系统存于RAM中,放在内存的低地址第二种情况操作系统存在于ROM中,存在内存的高地址,一般老式的手机操作系统是这么设计的。

    如果这种情况下,想要操作系统可以执行多进程的话,唯一的解决方案就是和硬盘搞交换,当一个进程执行到一定程度时,整个存入硬盘,转而执行其它进程,到需要执行这个进程时,再从硬盘中取回内存,只要同一时间内存中只有一个进程就行,这也就是所谓的交换(Swapping)技术。但这种技术由于还是直接操作物理内存,依然有可能引起进程的崩溃。
    所以,通常来说,这种内存操作往往只存在于一些洗衣机,微波炉的芯片中,因为不可能有第二个进程去征用内存。

    内存抽象
    为了解决直接操作内存带来的各种问题,引入的地址空间(Address Space)这个概念,这允许每个进程拥有自己的地址。这还需要硬件上存在两个寄存器,基址寄存器(base register)和界址寄存器(limit register),第一个寄存器保存进程的开始地址,第二个寄存器保存上界,防止内存溢出。在内存抽象的情况下,当执行
    mov reg1,20
    这时,实际操作的物理地址并不是20,而是根据基址和偏移量算出实际的物理地址进程操作,此时操作的实际地址可能是:
    mov reg1,16245
    在这种情况下,任何操作虚拟地址的操作都会被转换为操作物理地址。而每一个进程所拥有的内存地址是完全不同的,因此也使得多进程成为可能。

    但此时还有一个问题,通常来说,内存大小不可能容纳下所有并发执行的进程。因此,交换(Swapping)技术应运而生。这个交换和前面所讲的交换大同小异,只是现在讲的交换在多进程条件下。交换的基本思想是,将闲置的进程交换出内存,暂存在硬盘中,待执行时再交换回内存,比如下面一个例子,当程序一开始时,只有进程A,逐渐有了进程B和C,此时来了进程D,但内存中没有足够的空间给进程D,因此将进程B交换出内存,分给进程D。如图2所示。

     

    通过图2,我们还发现一个问题,进程D和C之间的空间由于太小无法令任何进程使用,这也就是所谓的外部碎片。一种方法是通过紧凑技术(Memory Compaction)解决,通过移动进程在内存中的地址,使得这些外部碎片空间被填满。使用紧凑技术会非常消耗CPU资源,一个2G的CPU每10ns可以处理4byte,因此多一个2G的内存进行一次紧凑可能需要好几秒的CPU时间。还有一些讨巧的方法,比如内存整理软件,原理是申请一块超大的内存,将所有进程置换出原内存,然后再释放这块内存,重新加载进程,使得外部碎片被消除。这也是为什么运行完内存整理会狂读硬盘的原因。

    进程内存是动态变化的
    实际情况下,进程往往会动态增长,因此创建进程时分配的内存就是个问题了,如果分配多了,会产生内部碎片,浪费了内存,而分配少了会造成内存溢出。一个解决方法是在进程创建的时候,比进程实际需要的多分配一点内存空间用于进程的增长。

    一种是直接多分配一点内存空间用于进程在内存中的增长,另一种是将增长区分为数据段和栈(用于存放返回地址和局部变量),如图3所示。

     

    空间不足的解决方案

    当预留的空间不够满足增长时,操作系统首先会看相邻的内存是否空闲,如果空闲则自动分配,如果不空闲,就将整个进程移到足够容纳增长的空间内存中,如果不存在这样的内存空间,则会将闲置的进程置换出去。

    内存的管理策略
    当允许进程动态增长时,操作系统必须对内存进行更有效的管理,操作系统使用如下两种方法之一来得知内存的使用情况,分别为1位图(bitmap) 和链表


    使用位图,将内存划为多个大小相等的块,比如一个32K的内存1K一块可以划为32块,则需要32位(4字节)来表示其使用情况,使用位图将已经使用的块标为1,未使用的标为0.

    而使用链表,则将内存按使用或未使用分为多个段进行链接。使用链表中的P表示从0-2是进程,H表示从3-4是空闲。

    使用位图表示内存简单明了,但一个问题是当分配内存时必须在内存中搜索大量的连续0的空间,这是十分消耗资源的操作。相比之下,使用链表进行此操作将会更胜一筹。还有一些操作系统会使用双向链表,因为当进程销毁时,邻接的往往是空内存或是另外的进程。使用双向链表使得链表之间的融合变得更加容易。还有,当利用链表管理内存的情况下,创建进程时分配什么样的空闲空间也是个问题。通常情况下有如下几种算法来对进程创建时的空间进行分配。

    临近适应算法(Next fit)—从当前位置开始,搜索第一个能满足进程要求的内存空间
    最佳- 适应算法(Best fit)—搜索整个链表,找到能满足进程要求最小内存的内存空间
    最大适应算法(Wrost fit)—找到当前内存中最大的空闲空间
    首次适应算法(First fit) —从链表的第一个开始,找到第一个能满足进程要求的内存空间
    虚拟内存(Virtual Memory)
    虚拟内存是现代操作系统普遍使用的一种技术。前面所讲的抽象满足了多进程的要求,但很多情况下,现有内存无法满足仅仅一个大进程的内存要求(比如很多游戏,都是10G+的级别)。在早期的操作系统曾使用覆盖(overlays)来解决这个问题,将一个程序分为多个块,基本思想是先将块0加入内存,块0执行完后,将块1加入内存。依次往复,这个解决方案最大的问题是需要程序员去程序进行分块,这是一个费时费力让人痛苦不堪的过程。后来这个解决方案的修正版就是虚拟内存。

    虚拟内存的基本思想是,每个进程有用独立的逻辑地址空间,内存被分为大小相等的多个块,称为页(Page).每个页都是一段连续的地址。对于进程来看,逻辑上貌似有很多内存空间,其中一部分对应物理内存上的一块(称为页框,通常页和页框大小相等),还有一些没加载在内存中的对应在硬盘上,如图5所示。


    由图5可以看出,虚拟内存实际上可以比物理内存大。当访问虚拟内存时,会访问MMU(内存管理单元)去匹配对应的物理地址(比如图5的0,1,2),而如果虚拟内存的页并不存在于物理内存中(如图5的3,4),会产生缺页中断,从磁盘中取得缺的页放入内存,如果内存已满,还会根据某种算法将磁盘中的页换出。

    而虚拟内存和物理内存的匹配是通过页表实现,页表存在MMU中,页表中每个项通常为32位,既4byte,除了存储虚拟地址和页框地址之外,还会存储一些标志位,比如是否缺页,是否修改过,写保护等。可以把MMU想象成一个接收虚拟地址项返回物理地址的方法。

    因为页表中每个条目是4字节,现在的32位操作系统虚拟地址空间会是2的32次方,即使每页分为4K,也需要2的20次方*4字节=4M的空间,为每个进程建立一个4M的页表并不明智。因此在页表的概念上进行推广,产生二级页表,二级页表每个对应4M的虚拟地址,而一级页表去索引这些二级页表,因此32位的系统需要1024个二级页表,虽然页表条目没有减少,但内存中可以仅仅存放需要使用的二级页表和一级页表,大大减少了内存的使用。

    分页机制:
    为什么使用两级页表

    假设每个进程都占用了4G的线性地址空间,页表共含1M个表项,每个表项占4个字节,那么每个进程的页表要占据4M的内存空间。为了节省页表占用的空间,我们使用两级页表。每个进程都会被分配一个页目录,但是只有被实际使用页表才会被分配到内存里面。一级页表需要一次分配所有页表空间,两级页表则可以在需要的时候再分配页表空间。

    两级页表结构

    两级表结构的第一级称为页目录,存储在一个4K字节的页面中。页目录表共有1K个表项,每个表项为4个字节,并指向第二级表。线性地址的最高10位(即位31~位32)用来产生第一级的索引,由索引得到的表项中,指定并选择了1K个二级表中的一个表。

    两级表结构的第二级称为页表,也刚好存储在一个4K字节的页面中,包含1K个字节的表项,每个表项包含一个页的物理基地址。第二级页表由线性地址的中间10位(即位21~位12)进行索引,以获得包含页的物理地址的页表项,这个物理地址的高20位与线性地址的低12位形成了最后的物理地址,也就是页转化过程输出的物理地址。

    线性地址到物理地址的转换:

    扩展分页
    从奔腾处理器开始,Intel微处理器引进了扩展分页,它允许页的大小为4MB。

    页面高速缓存:


    页面替换算法

    因为在计算机系统中,读取少量数据硬盘通常需要几毫秒,而内存中仅仅需要几纳秒。一条CPU指令也通常是几纳秒,如果在执行CPU指令时,产生几次缺页中断,那性能可想而知,因此尽量减少从硬盘的读取无疑是大大的提升了性能。而前面知道,物理内存是极其有限的,当虚拟内存所求的页不在物理内存中时,将需要将物理内存中的页替换出去,选择哪些页替换出去就显得尤为重要,如果算法不好将未来需要使用的页替换出去,则以后使用时还需要替换进来,这无疑是降低效率的,让我们来看几种页面替换算法。

    1) 最佳置换算法(Optimal Page Replacement Algorithm)

    最佳置换算法是将未来最久不使用的页替换出去,这听起来很简单,但是无法实现。但是这种算法可以作为衡量其它算法的基准。

    2) 最近不常使用算法(Not Recently Used Replacement Algorithm)

    这种算法给每个页一个标志位,R表示最近被访问过,M表示被修改过。定期对R进行清零。这个算法的思路是首先淘汰那些未被访问过R=0的页,其次是被访问过R=1,未被修改过M=0的页,最后是R=1,M=1的页。

    3) 先进先出页面置换算法(First-In,First-Out Page Replacement Algorithm)

    这种算法的思想是淘汰在内存中最久的页,这种算法的性能接近于随机淘汰。并不好。
    改进型FIFO算法(Second Chance Page Replacement Algorithm)
    这种算法是在FIFO的基础上,为了避免置换出经常使用的页,增加一个标志位R,如果最近使用过将R置1,当页将会淘汰时,如果R为1,则不淘汰页,将R置0.而那些R=0的页将被淘汰时,直接淘汰。这种算法避免了经常被使用的页被淘汰。

    4) 时钟替换算法(Clock Page Replacement Algorithm)

    虽然改进型FIFO算法避免置换出常用的页,但由于需要经常移动页,效率并不高。因此在改进型FIFO算法的基础上,将队列首位相连形成一个环路,当缺页中断产生时,从当前位置开始找R=0的页,而所经过的R=1的页被置0,并不需要移动页。如图6所示。
    最久未使用算法(LRU Page Replacement Algorithm)
    LRU算法的思路是淘汰最近最长未使用的页。这种算法性能比较好,但实现起来比较困难。

    算法 描述:
    最佳置换算法 无法实现,作为测试基准使用
    最近不常使用算法 和LRU性能差不多
    先进先出算法 有可能会置换出经常使用的页
    改进型先进先出算法 和先进先出相比有很大提升
    最久未使用算法 性能非常好,但实现起来比较困难
    时钟置换算法 非常实用的算法

    上面几种算法或多或少有一些局部性原理的思想。局部性原理分为时间和空间上的局部性
    1.时间上,最近被访问的页在不久的将来还会被访问。
    2.空间上,内存中被访问的页周围的页也很可能被访问。

    =============================================================================================================

    操作系统——分页式内存管理

    ==============================================================================================================


    为什么要引入内存管理?
    答:多道程序并发执行,共享的不仅仅只有处理器,还有内存,并发执行不过不进行内存管理,必将会导致内存中数据的混乱,以至于限制了进程的并发执行。

    扩充内存的两种方式?
    答:覆盖和交换技术是扩充内存的两种方法

    1:覆盖技术。覆盖的基本思想是:由于程序运行时并非任何时候都需要访问程序和数据的各个部分(尤其对大程序而言),因此可以把用户空间分成一个固定区和若干个覆盖区。经常活跃的部分放在固定区,其余部分按照调用关系分配。首先将那些即将要访问的段放入覆盖区,其他段放在外存中,在需要调用之前,系统再将其调入覆盖区,替换覆盖区中原有的段。

    特点:打破了必须将一个进程的全部信息装入主存后才能运行的限制,但是当同时运行的程序的代码量大于主存时仍不能允许,内存中常能更新的只有覆盖区的段

    2:交换技术。交换的基本思想是:把处于等待状态的程序从内存移到辅存,把内存空间腾出来,这一过程被称为换出;把准备好竞争CPU运行的而程序从辅存移到主存,这一过程称为换入。

    特点:交换技术主要是在不同的进程之间进行,覆盖则是用于同一个进程。

    连续内存分配管理
    答:连续分配方式,指为一个用户分配一个连续的内存空间。包括:

    1:单一连续分配。内存此时分为系统区和用户区,系统区只分配给操作系统使用,通常在低地址部分;用户区为用户提供。内存中只有一道程序,也无需进行内存保护。无外部碎片但是有内部碎片,且存储器效率低下

    2:固定分区分配。将内存空间划分为若干个固定大小的区域,每个分区只能装入一道作业。当有空闲分区时,便可以再从外存的后备作业队列中,选择适当大小的作业装入该区,分为(分区大小相等和分区大小不相等两种方式)无外部碎片但是有内部碎片(分区内部有空间的浪费),且存储器效率低下,但是可存在多道程序,是用于多道程序并发执行的最简单的内存分配方式。

    3:动态分区分配。也成为可变分区分配,它不预先对内存进行划分,而是在进程装入内存时,根据进程的大小动态的建立分区,并使分区的大小正好适合进程的需要,其分区的数目和大小是可变的。但是随着时间的推移,很容易产生外部碎片(区域1进程释放后20M(区域1),区域2中19M仍在 ,再进入14M,放在区域一原来的地方,进程间的内存空间被浪费6M),外部碎片指的是分区以外的存储空间被浪费

    解决问题1:为了解决外部碎片的问题,可以使用“紧凑”技术来解决:操作系统不时的对进程进行移动和整理(需要动态重定位寄存器的支持,较为费时)

    解决问题2:动态分区的分配问题,

    1:首次适应算法,空闲分区以地址递增的方式链接,分配内存时顺序查找,找到大小满足要求的第一个空闲分区

    通常该算法是最快最好的也是最简单的。

    2:最佳适应算法,空闲分区以容量递增形成分区链,找到第一个满足要求的空闲分区

    实际上新能不佳,因为每次最佳分配通常会留下很小的难以利用的内存块,产生外部碎片。

    3:最坏适应算法,又称最大适应算法,空闲分区以容量递减形成分区链,找到第一个满足要求的空闲分区,也就是挑选出最大的分区

    性能较差,因为算法开销也是需要考虑的一部分

    4:邻近适应算法,又称循环首次适应算法,也就是从首次适应算法中演变而来,不同的是,从上次查找结束的位置开始继续查找

    性能较差

    非连续内存分配管理
    答:根据分区的大小是否固定主要分为分页和分段的存储管理方式

    非连续分配允许一个程序分散的装入到不相邻的内存分区中,这需要额外的空间去存储它们的存储区索引,使得非连续分配方式的存储密度低于连续存储方式

    1.分页式存储管理方式
    分页式存储管理方式,根据运行时是否需要把作业的所有页面都装入内存才能运行分为基本分页存储管理方式和请求分页存储管理方式

    @基本分页式存储管理方式

    答:在连续存储管理方式中,固定分区会产生内部碎片,动态分区会产生外部碎片。这两种技术对内存的利用率都比较低,分页:把主存空间划分为大小相等且固定的块,块相对较小,作为主存的基本单位,每个进程也以块为基本单位划分,进程在执行时,以块为单位逐个申请主存中的块空间。

    分析:从形式上来看,很像固定分区,但却有着本质的不同点:1:块的大小相对于分区来说要小得多 2:进程也按照块来划分,运行时按照块来申请主存,尽管这样也会产生内部碎片,但是相对于进程的大小来说是非常小的,每个进程平均只产生半个块大小的内部碎片

    基本概念
    进程中的块称为页。

    主存中的块称为页框。

    外存也以同样的单位进行划分,也称为块。

    进程执行时,向主存申请块,就产生了页与页框的一一对应关系

    为方便地址转换,页面的大小应该是2的整次幂,页面的大小也应该适中,太小的话回使得进程的页面数过多,页表过长,占用大量的内存且增加硬件地址转换的开销,降低页面的换入/换出效率。过大会使得页内碎片增大,降低内存的利用率。所以空间效率和时间效率都应该被考虑在内。

    (逻辑地址)地址结构:包含两部分,第一部分为页号P,后一部分为页内偏移量W,地址长度为32位,其中0~11位为页内地址,即每页大小为4 KB,12~31位为页号,地址空间最多允许有2的30次方页。(二进制与十进制之间的转换)

    页表:为了便于在内存中找到进程的每个页面对应的物理块,系统为每个进程建立了一张页表,记录页面在内存中对应的物理块号,页表一般存在内存中。页表的第一部分存的是页号,第二部分存的是物理内存中的块号,页表项的第二部分与地址的第二部分共同组成物理地址。

    基本地址变换机构
    地址变换机构的任务是将逻辑地址转换为内存中的物理地址,地址变换是借助于页表实现的(注意十进制与二进制的转换)地址空间为一维。

    第一步:根据逻辑地址计算逻辑地址(页号 * 页长 + 偏移地址)中的页号(P = A/L)和地址偏移量(W = A %L)

    逻辑地址A = 2500 B,页面大小(块的大小) L = 1 KB, 得到 p = 2, W = 452

    第二步:比较页号P和页表长度M,若P > M,则产生越界中断,否则继续执行

    第三步:页表寄存器中,分为两部分(页表起始地址F和页表长度M),计算页号P在页表中对应的物理地址的页表项地址(对应块号b) p = 页表起始项F +页号P * 页表项长度,得到物理块号,直接对应的2页号对应块号8(页号与块号在页表中有直接对应),注意:页表长度:指的是一共有多少页。页表项长度:指的是一页占多大内存。

    第四步:计算物理地址E = b L +W (块号 块大小+地址偏移量)得到E = 8 * 1 KB +452 B = 8644 B ,得到物理空间之后,就可以访问内存了。

    页表项大小的确定
    以32位逻辑地址为例,字节为编码单位,一个页面的大小为4 KB,所以2的32次方 B 除以4 KB地址空间一共有 1 M 页,则需要log 2 (1 M) = 20 位才能保证表示范围能容纳所有的页面,又以字节为编码单位,[20 / 8] = 3 B,所以页表项的大小应该大于等于3 B,取4 B为常见。

    将页表始址与页号和页表项长度的乘积相加,便得到该表项在页表中的位置,

    于是可从中得到该页的物理块号,将之装入物理地址寄存器中。

    列出式子出来: 页表始址+页号x页表项长度

    1)页表项长度是页面长度是吗?

    2)如果是页面长度,那两者相乘就是整个内存的大小来,你想一想整个内存都用来存储页表可能吗?

    当然是不可能了,首先内存被划分成若干个和页面大小相等的片。

    每个页表项代表一个页面的地址,一般很小。

    假设内存大小是2GB,页面大小(物理块)是4KB,页表项长度是4B。

    则整个内存可以被划分成2GB/4KB=512K个页面。

    页表的长度=页表项的长度x页面的个数=4Bx512K=2M。

    内存中用2M的大小来存放页表。

    这下清楚了吧,实际上是取了每一个页号对应的页面的起始地址,或许还有对应的物理块号(应该有)。

    下面抄写操作系统中的一句话便于理解:

    对于一个具有32位逻辑地址空间的分页系统(4GB),规定页面大小为4KB,则在每个进程页表中的页表项

    可达1M个之多。4GB/4KB=1M

    又因为每个页表项占用1个字节(1B),故每个进程仅仅其页表就要占1MB的内存空间。

    而且还要求是连续的,显然这是不现实的,解决问题方法:

    1)采用离散分配方式来解决难以找到一块连续的大内存空间的问题。

    2)只将当前需要的部分页表项调入内存,其余的页表项仍然驻留在磁盘上,需要时再调入。

    快速地址变换机构
    上述方法需要访问两次内存,一次是访问页表,确定所存取数据或指令的物理地址,一次是根据该物理地址存取数据或者指令。显然这样的方法较慢。

    为此我们可以在地址变换机构中增设一个具有并行查找能力的高速缓冲存储器——快表,又称联想寄存器,用来存放当前访问的若干页表项,加速地址变换过程,命中率达到90%以上

    第一步就变为了:将逻辑地址中的页号直接送入高速缓存寄存器,与快表进行匹配,未找到则按慢表处理~

    有些处理器设计为快表和慢表同时查找,快表查找成功则终止慢表的查找

    两级页表
    由于引入了分页管理,进程在执行时不需要将所有页都调入内存页框中,只要将保存有映射关系的页表存入内存中即可,我们这里考虑一下页表的大小,以32位逻辑空间,页面大小4 KB ,页表项大小4 B 为例,若要实现进程对全部逻辑地址空间的映射,则每个进程需要需要2的20次方(2的32次方 / 2的12次方(也就是页面的大小 4 KB ))(表示的也就是页面的个数),约100万个页表项。2的20次方个页表项 * 4 B ,为4 MB ,也就是说每个进程在页表项这一块就需要4 MB 的主存空间,显然这是比较大的内存占用。即使不考虑对全部逻辑地址空间的映射,一个逻辑地址空间稍大的进程,其页表项所占用的主存空间也是过大的。

    例:40 MB 的进程,页表项总共占有(40 MB / 4 KB * 4 B ) 40 KB 的主存空间,页面大小为4 KB,那么就需要10 个内存页面来存储整个页表,整个进程需要(40 MB / 4 KB )为1万个页面,在实际执行中只需要几十个页面进入内存框就可以运行, 所以这10个页面的页表相对于实际执行的几十个进程页面来说,内存利用率肯定是比较低的。从另一方面来说,这10个页面的页表也并不需要同时保存在主存中,大多数情况下,映射所需要的页表项都在页表的同一个页面。

    综上,为了压缩页表,我们将页表映射的思想进一步延伸,使用一个层次结构的页表——两级页表,从上例中,我们将10页的页表进行地址映射,建立上一级页表,用于存储页表的映射关系。这里对占10个页面的页表进行映射,在上一级页表中只需要10个页表项,所以上一级页表只需要1个页面(4 KB)就足够表示了。在进程的执行过程中,只需要将占用1页的上一级页表存入主存中即可,进程的页表和进程的页面可以后续进行调入。

    实际上,我们需要的就是一张索引表,告诉我们第几张页表应该上哪去找,这样就不用将所有的页表存入主存,所以,这就是页表的页表,称为二级页表。规定:为了查询方便,顶级页表最多只能有一个页面。

  • 相关阅读:
    MVC框架理解及优缺点
    ThinkPHP 小于5.0.24 远程代码执行高危漏洞 修复方案
    Nginx负载均衡配置与负载策略
    【高级】PHPFPM和Nginx的通信机制
    浅谈Facebook的服务器架构(组图) 狼人:
    【观点】什么是REST? 狼人:
    10款对开发者有用的Android应用 狼人:
    【书摘】Linux内核编程 狼人:
    6款强大的jQuery插件 创建和加强网站布局 狼人:
    【评论】是什么造就了伟大的程序员? 狼人:
  • 原文地址:https://www.cnblogs.com/sea520/p/12618733.html
Copyright © 2011-2022 走看看