操作系统的内存管理

学习视频:https://www.bilibili.com/video/BV1uW411f72n/?spm_id_from=333.337.search-card.all.click&vd_source=7183963aa2ea0a9940e5c1f8e866ce15

OS的内存管理

操作系统进行内存管理主要想要完成四个目标:

  • 维护好各个进程逻辑地址与物流地址的映射关系

  • 给每个进程分配独立的地址空间,使得进程与进程之间的数据不会被篡改

  • 维护好进程的共享资源

  • 尽量分配更多的内存空间(交换技术,内存分配算法)

  • 为什么需要逻辑地址(虚拟地址/线性地址)?其是为了保证进场间的独立性。EG:假如此时都使用真实的物理地址。现有两个进程,进程1通过JMP20的指令跳转到物理地址为20的位置,进程2此时也通过JMP跳转到20的物理地址,若此时并行运行则一定会产生二异性,因此我们需要逻辑地址,将这些指令都翻译成为相对于基地址的偏移量。当然如果完全依靠操作系统从软件的方面来维护进程的独立性那么开销是非常昂贵的,因此在CPU中有两个专门的寄存器来帮助操作系统完成进程间的独立性:基地址寄存器,界限寄存器(段式内存管理)【关于具体的逻辑地址,物理地址,线性地址的区别与联系见虚存技术】,逻辑地址之于程序而言其实是偏移地址

    • 基地址寄存器:将进程加载到主存时,操作系统会将进程在物理内存的起始地址保存在基地址寄存器中
    • 界限寄存器:将进程占据内存的长度保存到界限寄存器中

image-20230505201459840

如图:进程1的基地址为16380,此时在执行JMP20时不会在物理地址上真实的跳转到20的位置,而是找到相对于基地址偏移20的地址,若此地址在界限寄存器中保存的界限范围内则允许执行该语句

  • 逻辑地址的生成:在进行汇编成为.o文件时,会将一些符号语言转换成为地址,这些地址都是从零开始的连续地址,那么如果有多个依赖关系,连接器就会去链接多个.o文件最终将他们变为一个可执行程序,并且合理进行全局的地址分布,此时得到的就是最终的逻辑地址,逻辑地址的生成其实并不需要操作系统过多的帮助,编译器就能很好的完成任务

    image-20230426104733365

  • 为什么逻辑地址通常比物理地址大,因为CPU的地址总线的寻址能力比真实物理内存大很多,EG:64位的CPU理论寻址能力为16TB,很显然物理内存并没有这么大,那么这多出的寻址空间如何解决?

  • 物理地址:物理地址是直接与硬件相对应的

  • CPU在执行某条语句时的具体过程:

    1. ALU(计算逻辑单元)需要读取该地址的内容,因此它会发送一个请求,这个请求的参数会携带此逻辑地址
    2. CPU里的MMU(内管理单元)会去查找这个逻辑地址和物理地址的映射关系,如若不存在,则会产生一个硬件中断,然后便去内存中的map查找,找到了此映射关系,cpu就会给主存发送一个请求,表示需要某一个物理地址中的内容,主存便会通过主线将内容传给cpu
  • 从上述过程我们便可以看出,操作系统主要需要完成的事情便是,逻辑地址和物理地址的映射

  • 操作系统的安全性检测:操作系统会检查地址的访问是否在合理的分配的区间

内存的分配

​ 内存分配主要分为两个方面:连续内存的分配,非连续内存的分配

  • 内存碎片问题:所谓内存碎片就是当我们分配空间时,有一些无法进一步去利用的空闲空间

  • 内存碎片分类:

    1. 内碎片:已经分配给应用程序,但这个应用程序没法进一步去使用在这个分配了的单元之内的空闲空间

    2. 外碎片:分配单元之间的碎片

image-20230426112453802

连续内存分配

  • 连续内存分配的时机:

    1. 将磁盘内容加载到主存时

    2. 程序在运行起来时想要访问数据且这个数据要求是连续的空间

  • 分区的动态分配算法:

    1. 首次适配算法:分配n字节空间,从低地址到高地址依次探寻空闲的空间,寻找第一个能容纳n字节的空间

      image-20230426113404289

      • 优点:容易产生更大的空闲块,因为是从上向下一次使用的方式,就不会破坏掉下面的更大的连续空间
      • 缺点:很容易产生外碎片(EG:申请1k,申请2k,归还1k,申请900,那么此时在原1k和2k之间的100字节就很难再被分配出去了)
    2. 最优适配算法:分配n字节空间,寻找与待分配空间差值最小的空间

      image-20230426113949767

      • 优点:能够最小化外碎片的尺寸,能够避免分割掉大的空闲块,适合大部分分配小块时
      • 缺点:容易产生很多没用的细小的外碎片,不利于后续空间的分配与管理
    3. 最坏适配算法:分配n字节空间,寻找与待分配空间差值最大的空间

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GdTqr87T-1683975691370)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230426114950223.png)]

      • 优点:能够避免太多微小碎片的产生,适用于分配的都是中尺寸的快
      • 缺点:容易破碎掉大的空闲空间,导致大的块无法被满足,且任然有内存碎片的问题
  • 这些简单算法都很难满足应用程序产生的随机内存需求,在他们的基础上还有两种优化方式

    1. 紧致算法:调整内存中进程的位置,使得能获得更大的连续块

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Snx9TBUl-1683975691371)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230426161532929.png)]

      使用紧致算法需要考虑两个问题:

      • 使用紧致算法的时机:已近获取cpu资源的进程不能进行紧致
      • 紧致算法的开销:频繁的内存拷贝会引起极大的开销,如何进行改进?
    2. 交换技术:

      • 将整个进程挂起,这样可以获得更多的空间,但拷贝开销也比较大
      • 将某些数据换出(虚存技术)

非连续空间的分配

为什么需要非连续分配的内存管理方式?因为无论采取哪一种连续的内存分配方式,或多或少都会出现内存碎片的问题。需要一种方式来避免碎片的问题

  • 非连续分配的优点:程序的物理地址可以不用是连续的,能更好的利用内存,能更好的管理数据,保证进程的独立性,可以允许共享数据

  • 非连续分配的缺点:维护非连续分配的开销大,需要与硬件结合进行优化

  • 分段机制:

    起源:

    1. 在早期intel想要将其cpu的寻址地址扩展到20根来扩大程序能使用地址的范围,来减轻程序员编程的负担(扩大程序空间)。但问题在于,CPU地址总线在扩大至20后,ALU(算术逻辑运算单元)的数据总线只有16位,那么如何弥补这4位的空缺呢?为此intel设计出了分段的方法。

      为了能够支持分段,intel在CPU中加入了4个寄存器:CS,DS,SS,ES,分别用于代码段,数据段,堆栈段,其他段。x86将物理内存划分很多段(所有的操作都是在真实的物理内存上,当时没有操作系统这么一说,这样就是所谓的实模式),段基地址采用两字节对齐(16的倍数),即16位的段寄存器只需要记录地址的高16位,ip寄存器记录段内偏移量。因此,进程要访问的物理地址 = (段寄存器 << 4) + (ip寄存器(偏移量))

    2. 将应用程序的各个段(堆,栈,代码段,数据段… )离散化存储,方便管理

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zS4EzMZk-1683975694793)(null)]

    通过分段机制将连续的逻辑地址中的每一段分别进行存储,这样可以让这些段相互直接相对隔离,能够很好的对他们进行管理,例如:对于有的数据读写权限不同,那么他们位于的区域也就不同,这样就会减少很多开销,易于管理

    • 分段的寻址方案:

    对于分段机制来说,真实的物理内存也将被看作是一块一块,来与逻辑地址中的一段一段进行对应

    对于连续的逻辑地址来说,我们可以将这样的逻辑地址看作是一个大的一维数组他们提供字节流服务,对于分段机制来说,其核心就是将这样一个大的一维数组按照段划分成为两部分,一部分完成段的寻址(当前段对应的那一块物理内存),一部分完成偏移量的寻址(当前地址相对与对应块的偏移量)

    对于具体的寻址方案现有两种方式:

    • 段寄存器+地址寄存器,将段号和, 地址分开存储(X86):也就是说我们反汇编看到的地址就是某个段的偏移量
    • 单地址实现(Linux)

image-20230427101643008

  • 分段寻址的具体过程:

image-20230427102714385

cpu中的ALU(计算逻辑单元)读取某个地址的内容时会发出一个请求,这个请求的参数会携带这个逻辑地址,查询段表(段表本质上就是一个数组,那么段号其实就是其索引),段表中主要存储三个内容:段在物理内存中起始地址,以及段的大小,以及段的限制。CPU在查询到地址后进行安全性检测,看当前的地址是否在段的限制范围之内,若不满足则会产生一个异常,交给操作系统处理,操作系统如果认为这是异常的访问,则会将进程kill

段表的建立应当在正式的寻址之前就应当由操作系统建立好(操作系统初始化)

  • 分页机制:

    • 起源:
    1. 段机制,每个段大小不是固定的,维护开销较大
    2. 为了离散线性地址和物理地址的一一对应关系(段机制的缺陷)

    由于分段机制,每个段的大小是不确定的,因此分段机制的维护开销是比较大的,(且)目前使用的较多的非连续分配方式还是分页机制

    • 分页机制与分段机制最大的区别:分页机制中每一页的大小是固定的,而分段机制中,每一段的大小是不固定的
    • 分页机制主要的方法:将物理内存划分至相同大小的页帧,将逻辑地址划分为相同大小的页,建立二者的转换方式,并对其优化

    页帧:物理内存的组织与布局方式,页帧由两部分组成:页帧号 + 页内偏移

    处于分页机制中物理地址的计算:

    16位地址空间,9位大小的页帧,现物理地址为(3,6),请问物理地址为多少?

    image-20230427144943372

    也可以使用公式:

    image-20230427145201956

    页:一个程序的逻辑地址空间被划分成为相等的页(页的大小可能和页帧的大小不相同,但页内偏移和页帧内偏移一定相同)

    页的计算方式和页帧相同

    • 分页之于分段的优势:分页的一个最大的好处在于,每一个页或者页帧的大小是固定的,使得操作系统在维护时能通过更加简便的方式实现,不想分段机制,还需要考虑段的大小的问题

    问题:逻辑地址一般都比物理地址大,那么如何映射?虚拟内存解答

    页表

    ​ 每个运行的程序都会有一个页表,页表项的内容:最重要的是所对应的帧号,其次还有一些bit用于表示存在属性(逻辑地址可能是很大的,因此有一部分逻辑地址空间可能就没有对应到物理地址空间,1表示物理内存真实存在对应关系,0则相反),读写情况(写过?读过?还说没有读,没有写)

    • 分页寻址的具体过程:

      image-20230427153910761

      程序运行到某一条语句,cpu进行寻址,cpu先通过页表基地址寄存器信息找到页表所在位置,将页号作为页表的索引(页表本质也是数组) 找到对应的页帧号,再通过偏移量获得真实的物理地址空间,页表是由操作系统在初始化时进行建立。在进行索引查询时会进行安全性检测,查看标志位是否为1,若否则会触发内存异常交给操作系统处理

    • 分页机制的性能问题:分页机制主要存在两方面问题:时间效率问题,空间效率问题

      1. 问题1:cpu每次进行一次内存寻址需要进行两次内存访问,第一次查询页表项,第二次用于访问数据
      2. 问题2:64位机器若每页大小位1024bit,那么一个进程则需要花费2^54的空间,这是一笔极大的开销,且只是一个进程需要消耗的空间
    • 一般来说,操作系统在解决时间和空间这两方面的问题会有两种手段:缓存(TLB快表) + 间接访问(多级页表机制)

    • 解决页表的时间性能问题:TLB(快表),TLB位于MMU中,用于缓存页表中经常被访问的内容、

    • TLB为什么快?TLB是用相关存储器(硬件)实现,相关存储器是一种快速查询的存储器, 它的速度很快可以支持并发的进行查找,但它的容量是比较有限的

TLB miss:当cpu在快表中无法查询到对应的项,则会去主存中查询页表,若flag位为1,则将最常用的放在TLB中

  • TLB miss会经常发生吗?一般来说32位系统一个页是4k,且每一个地址都要访问的话,也需要访问4k次才会出现一次TLB miss,因此我们在写程序时也应当注意:尽量将要访问的数据集中在一个区域,这样可以有效的减少TLB的缺失

  • TLB表项的填充:根据cpu的不同:x86的cpu是完全由硬件完成,对于另外一些cpu来说可能需要操作系统来完成。这两种情况目前都存在

  • 解决空间效率问题:多级页表机制

    1. 二级页表

      image-20230505212026111

      采用二级页表寻址时会先查找一级页表,通过一级页表下表访问到某个二级页表的起始位置,然后通过p2段来作为二级页表的下标进行查找页号与页帧号的映射关系

      • 如何通过多级页表提高空间效率?
        1. 逻辑空间的地址都比物理空间的地址大很多。因此我们可能会访问到一些物理内存中不存在的页(驻留位为0),那么通过1级页表的筛选我们就可以不用真实的存储一级页表中驻留位为0的二级页表。
        2. 我们可以暂时只将常用的页表加载到主存中
    2. 多级页表模型

      image-20230505212626278

  • 反向页表:无论是一级页表还是多级页表存储的内容都是通过页号来查找页帧号,但由于逻辑地址空间可能会远远大于真实的物理地址空间,因此页表可能会比较大,且需给每一个进程都维护独立的页表这样的开销是比较大的,因此反向页表其实就是想通过维护页帧号所映射页号的方式来改善这一情况,通过反转页表我们可以不关心逻辑空间的大小,页表的大小仅仅与真实的物理空间相关,且整个操作系统只需要维护一个页表即可,因此效率会大大提升

  • 反向页表的问题:如何通过页帧号作为index索引页号?

    1. 使用关联存储器,设计一个类似TLB的结构进行查询页号对应的页帧号

      弊端:设计复杂,耗电高,且关联存储器的容量很小

    2. 基于hash计算的反向页表,通过hash计算来获取页帧号

      问题:需要设计一个高效的hash函数,需要有解决冲突的手段(这里在设计hash函数时通常会将pid作为参数传递,来减少冲突)

      为了降低访问反向页表的次数依旧需要设计TLB

虚存技术

  • 在多道程序运行的操作系统中出现内存不够用的时候,一般会有以下三种方案:

    1. 覆盖技术
    2. 交换技术
    3. 虚存技术

虚存技术的起因:程序对内存的需求比起硬件进步的水平快了很多,因此我们需要通过一些办法将更多的程序运行在有限的内存中,同时我们希望竟可能的减少过大内存的费用(磁盘大且便宜),充分利用CPU更大的寻址空间。

  • 存储器的结构以及理想的存储器:

    image-20230505215337569

  • 虚存技术的两个重要先驱技术:

    1. 覆盖技术:在上世纪最普遍的操作系统还是微软设计的DOS操作系统,DOS操作系统并不能提供很友好的内存管理机制,需要程序员自己关心程序运行起来内存不够的问题。覆盖技术想要达到的目标便是:希望能在较小的内存中运行一个超出内存大小的程序。覆盖技术的核心便是:不具有调用关系的函数共享分区,分时使用,覆盖同一块内存,平常不常用的部分放在外存中,并不将全部内容加载到内存中运行程序。采用覆盖技术的程序会有一个常驻区,常驻区的代码负责决定什么时候该谁使用分区。

      image-20230505220917042

      如图在调用B时,C便放在外存上,直到需要运行C才将C从外存覆盖到覆盖区0

      • 覆盖技术的缺点:开发程序的成本高昂,需要时刻设计覆盖技术,且由于覆盖技术只需要满足互相不调用的函数就能共享分区,因此在设计覆盖技术时方案就多种多样,其效率也不竟相同。且频繁的进行换入换出操作开销很大
    2. 交换技术:在最早Uinx最先提出让操作系统来进行内存的管理,减少程序员内存管理的负担,UNIX内存管理的核心在于,将暂时无法运行的进程整个映射换出到外存上。这样直接换出一个进程的粒度比较大,因此开销也比较大

      • 交换技术需要关心的三个问题:
        1. 换出的时机?只有在确实是内存空间不足是才进行换出,因为磁盘io的开销是非常大的,操作系统一直等到磁盘io效率也是及低的
        2. 硬盘上交换区的大小应该多大?如果有一个进程需要的内存特别大,那么久有可能需要将其他所有进程换出,那么这个时候所需要的交换区的大小也就是比较大的,因此提前需要预留多少大小的交换区也是问题之一
        3. 程序换入后的重定位?程序换出后原本物理地址可能已近被别的进程占用了,那么就需要修改页表或者段表的映射关系
  • 覆盖技术与交换技术的区别:

    1. 覆盖技术发生在程序内,通过没有相互调用关系的函数共享分区,不需要将程序需要的所有内容都加载到内存中,内存利用率高,代价是需要程序员设计覆盖的逻辑关系
    2. 交换技术的粒度比较大,是以进程为单位,在内存吃紧的时候,将暂时不会运行的进程放在外存挂起,优点是这样的内存管理方式是由操作系统完成,可以减少程序员内存管理的负担,且可以通过交换获取更多的内存空间,但是缺点是交换的粒度太大,若进程很大那么磁盘io的开销就过于昂贵
  • 综上所述,在内存不足时覆盖技术和交换技术或多或少都能帮助我们在一定程度上解决内存告急的问题,但同时他们又或多或少的存在一些问题,而虚存技术就是为了取二者精华,取其糟粕。虚存技术希望能像覆盖技术那样不将所有程序都放在内存中,因而能运行比当前内存更大的程序,同时他希望这样的操作方式由OS完成不需要程序员干涉,虚存技术也希望能像交换技术那样,能够实现内存与外存之间的交换,从而获得更多的空闲的内存空间,但虚存技术希望能以更小的粒度来完成交换,降低换入换出的开销

  • 虚存技术:虚拟存储器(virtual memory).虚拟存储器的基本思想是程序,数据,堆栈的总的大小可以超过物理存储器的大小,操作系统把当前使用的部分保留在内存中,而把其他未被使用的部分保存在磁盘上。比如对一个16MB的程序和一个内存只有4MB的机器,操作系统通过选择(即覆盖技术的常驻区),可以决定各个时刻将哪4M的内容保留在内存中,并在需要时在内存和磁盘间交换程序片段,这样就可以把这个16M的程序运行在一个只具有4M内存机器上了。而这个16M的程序在运行前不必由程序员进行分割。

    • 虚拟存储器:虚拟存储并非真实的存储结构,而是一种虚假的,逻辑的存储结构

线性地址,逻辑地址,物理地址

​ 在区分这几个地址间相互的关系时我们主要需要从两方面入手:平台与cpu发展历史

物理地址

​ 在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址,又叫实际地址或绝对地址。

线性地址

  • 未打开页机制的线性地址 = 物理地址
  • 打开页机制的线性地址 = 虚拟地址

逻辑地址与x86cpu

  • 逻辑地址 = 链接分布过后程序看见的地址 = 段内偏移量,机器语言指令中出现的内存地址,本质是偏移量

    • 16位cpu:随着cpu技术进步,cpu地址总线增加成为20根,提升了cpu的寻址范围,但由于ALU(逻辑计算单元)的数据总线只有16位。因此就产生了4位的缺差,为了弥补这四位的寻址。intel采用了分段式内存管理的机制,在CPU内部增加了4个寄存器,分别来存储代码段,数据段,堆栈段,其他段的段基地址。
    • 那么具体的cpu如何通过16位的数据总线访问20位的地址总线呢?x86将物理内存划分为很多段(所有的操作都是真实的物理内存上完成EG:jmp 20,当时的操作系统对于内存管理并没有提供太大的帮助,这样直接操作物理内存也被称为实模式),16位的段寄存器只需要保存地址的高16位,ip寄存器记录段内偏移量。因此进程要访问的物理地址 = (段寄存器 << 4) + (ip寄存器(偏移量))【逻辑地址】,在那个时候由于是写实模式,在多道程序的情况下就及其容易导致程序间数据的互相篡改,或者直接修改到操作系统的数据
    • 因此具有保护模式的32位cpu应运而生:32位cpu地址总线为32位,但为了兼容之前体系的版本,因此寄存器的大小依旧是16位,同时增加了两个寄存器GDTR(全局段描述符表寄存器),LDTR(局部段描述符寄存器),通过新的寄存器可以使得不和上个版本兼容,成为32位cpu。在x86保护模式下段的信息(段大小,长度,权限)即段描述符占8个字节,无法直接存放在段寄存器中。intel的设计是将段描述符放在GDT(全局段表),或者LDT(局部段表中),然后放入GDTR或者LDTR。而原来的段寄存器放的是段描述符在段表中的索引号。此时cpu的寻址方式变为:CPU通过段寄存器找到段表,通过段表中的信息做安全性检测,在通过检测后,得出对应的线性地址
  • cpu寻址与地址的转换

    • 在16位以及更早的的cpu,CPU的工作方式为实模式,直接使用物理地址
    • 在32为x86保护模式下:分段单元先将逻辑地址转换为线性地址,再通过MMU采用分页机制,将线性地址映射在物理地址上
    • 在x86_64位上,处理器将各个段寄存器中的段基索引都设置为0,实际上摒弃了段管理机制(线性地址 = 逻辑地址 + 段基地址,现在所有段基地址都为0),完全采用页机制,逻辑地址在此刻便完全等价于线性地址
    • 值得注意的是Linux操作系统其实早在32位cpu时就想完全摒弃段机制,完全采用页机制进行内存管理,但由于cpu硬件必须进行段的转换,Linux通过将段基设置为0避开了段机制,因此无论32位还是64位,在Linux中逻辑地址都等价与线性地址
  • 为什么现在更多使用页机制?

    • 本质上是为了提高空间利用率

    当下大部分操作系统的方案是,将一些进程不常用的内存段换出到硬盘中,腾出内存空间供需要的进程使用。虽然硬盘是比内存还低速的设备,在两个速度不匹配的设备之间进行数据交换对内存来说是一种浪费,但使用合理的置换算法可以减少置换次数。 在保护模式下,内存段的各种属性由段描述符保存,CPU在引用一个段时,都要先查看段描述符。段描述符存放在GDT或LDT中,即使段并不在内存中,即CPU允许在段描述表中已注册的段不在内存中存在。如果该描述符中的P位为1,表示该段在内存中存在,访问过该段后,CPU将段描述符中的A位置1,表示近期访问过该内存段。 段描述符的A位由CPU置1,清0则由操作系统来完成。操作系统每发现该位置1后就将该位清0,同时统计一个周期内该位为1的次数,就可以知道该内存段的使用频率,从而在物理内存不足时找出使用频率最低的段将其换出到硬盘以腾出内存空间给需要使用内存的进程。当段被换出到硬盘后,操作系统将该段的段描述符的P位置0,表示该段已不在内存中。 如上所述,计算机的软件和硬件相互配合完成内存的置换工作。虽然这在一定程度上解决了内存不足的问题,但还是有缺陷。比如物理内存特别小,以至于无法容纳任何一个进程的段,这就没法运行进程,更没法做段的置换工作了。而出现这种问题的本质原因是在目前只有分段的情况下,CPU认为线性地址等于物理地址,而线性地址是由编译器编译出来的,它本身是连续的,所以物理地址也必须要连续才行,但我们可用的物理地址并不连续。因此,为解决这个问题,我们需要解除线性地址与物理地址一一对应的关系,让他们之间重新建立映射——即让线性地址连续而物理地址不连续。在操作系统中实现这种映射的策略就是内存分页机制,相关数据结构就是页表。这样在进行换入换出时就能有更小的粒度

  • 虚存技术想要达到的目标:只将程序所需的一小部分数据放在内存中,同时保证虚存技术的效率

  • 虚存技术的前提:程序具有局部性

    • 时间局部性:一条指令的执行和下次执行,一个数据的访问和下一次访问都集中在一个较短的时期内
    • 空间局部性:当前指令和邻近的指令,当前访问的数据和临近访问的数据都集中在一块较小的区域内

    因此我们在编写程序时本身也要注意程序的局部性,尽量让访问集中。EG:按列遍历,按行遍历。按行遍历访问的的内容就具有局部性就会减少缺页的次数·

  • 基于段式或页式的虚存技术:

    • 在装入程序时,不必将可执行程序的全部内容加载到内存中,而只需要将当前需要执行的页,或者段加载入内存,就可以开始执行程序
    • 程序在执行过程中,如果程序所需的内容还未加载到内存中,则产生缺页或者缺段异常,由操作系统将相应内容换入内存
  • 虚存技术的优势:

    1. 能给每个进程都提供大的用户空间,完全利用CPU更广的寻址空间,给程序提供CPU的寻址最大范围。由于CPU的寻址范围可能很大,而真实物理空间可能没有那么大因此,虚存空间 = 内存 + 外存。在程序需要某些数据时才将其加载入内存
    2. 部分交换:与交换技术相比,虚存技术的换入换出的粒度是页或者段为单位,粒度更小(但通常我们更倾向于使用页)
  • 基于页机制的虚存技术

    • 虚拟页式管理的表项:

image-20230511212008953

1. 驻留位:表示该逻辑页是在内存还是在外存,若该位等于1则表示该页位于内存中,若为0,则表示当前页还在外存中,访问该表项则会引起一次缺页异常,操作系统进行换入换出操作
2. 保护位:表示允许对该页进行的操作,EG:只读,可读可写,可执行等
3. 修改位:表示该页是否被修改过,操作系统回收或者换出该页时,根据此位来决定是否将其写回外存。若没有被修改过则可以省去一次磁盘写操作
  • **缺页异常的处理流程:**操作系统通过产生缺页的地址,在外存中找到是那一页的数据要被访问,然后将其换入到内存

    1. 如果在内存中有空闲的物理页,则分配这一空闲的物理页,然后进行步骤4,否则进行步骤2
    2. 采用某种页面置换算法,选择一个被换出的页q,查看q在内存中是否被修改过,若被修改过,则将其写回外存
    3. 对q的页表项进行修改,将驻留位修改为0
    4. 将引发缺页异常的页装入物理页
    5. 重新运行被中断的指令
  • 后备存储:

    1. 一个虚拟地址空间的页面可以映射到一个磁盘上的一个文件
    2. 代码段:映射到可执行程序的二进制文件
    3. 动态库:映射到动态链接的库文件
    4. 其他段在程序运行中产生的数据会被映射到交换文件(swap file)

各种置换算法

  • 虚拟内存的性能问题:虽然我们通过分页式虚存技术我们可以让程序感到更大,成本更低的内存空间。但这也的空间涉及到缺页机制的处理,需要进行硬盘的读写,因此一个好的置换算法决定了我们与硬盘打交道的频率,当我们能更少的进行硬盘的读写

    虚存技术的效率计算:

    image-20230511215802737

    p:产生缺页的概率 q:对页进行写的概率

    由于对页进行写的操作通常是无法避免的,因此为了保证虚存技术的效率,需要设计一个优秀的置换算法,来尽量降低缺页的概率

  • 页面置换算法主要分为两个大的方面:局部页面置换算法与全局页面置换算法

局部页面置换算法

  • 局部页面置换算法是针对与某个进程来说,对于某个进程发生了缺页中断,需要进行页的置换时采取的算法
  • 局部页面置换算法想要达到的目标:尽可能减少页面的换入与换出次数,希望将未来不再使用,或者短期不在使用的页换出
  • 页面锁定技术:操作系统的关键部分不能被换出,针对那些不能被换出的页提供保护。实现方式:在页表项中增加锁定标志位
  • 最优页面置换算法:当一个缺页中断发生时,对于内存中的每一个页,选择距离下一次访问间隔时间最久的哪一个作为被置换的页
    • 缺陷:只是理想算法,实际根本无法实现,因为操作系统无从知晓一个页面还需要等待多久才会被访问,但其可作其他置换算法的性能评价依据(当我们第二次运行某个程序,我们就可以知晓其页面访问顺序,从而得出最优置换算法),通过看其他算法缺页次数和最优置换算法的缺页次数的接近情况
  • 先进先出算法(FIFO):选择内存中驻留时间最长的页淘汰
    • 实现方式:维护一个驻留时间的链表,某次淘汰链表头部
    • 缺陷:性能较差,调出的页可能是经常需要访问的页,因此可能会造成更多的缺页次数,且还会出现belady现象
    • 优点:实现简单,开销小
  • 最近最久未使用(LRU):当一个缺页发生时,选择最久未使用的页面将其淘汰,他是最优算法的一个近似算法,其依据程序的局部性原理,即在最近一段时间内某些页面被频繁的访问,那么他们在将来一小段时间也将被频繁访问。反之,在近一段时间不经常访问的页,在未来的一段时间也不会被经常访问
    • 实现:LRU栈:每次访问都将访问的页号入栈,同时考察站内是否有相同的页号,若有则将其抽出,当需要进行页面置换术时总是置换栈底的页面,他就是最久未使用的页
    • 优点:性能较好,当程序的局部性较好时缺页次数较少
    • 缺点:开销大,每一次需要考量栈中是否有相同页号
  • 时钟置换算法(clock):时钟置换算法是一种对LRU算法的近似,其通过软硬件结合的方式,在保证一定效率的情况下,开销也比较低。clock算法需要用到页表项中的访问位,当cpu通过寻址读/写过某一页时,cpu会将对应的页表项中的访问位置1。
    • 实现:本质是一个环形链表,在环形链表上寻找访问位为0的页。clock只需要维护一个移动指针,当缺页发生时,其会首先探测指向页访问位是否为0,若为0则该页被作为淘汰页,若为1则将其修改为0,顺时针指向下一个页重复以上步骤,直到找到访问位为0的页。由于这样的寻找淘汰页的方式很像时钟的走动,因此也被形象的称为时钟置换算法
    • 优点:在程序局部性较好时缺页率较小,且相对于LRU算法开销更小
    • 缺点:缺页次数的控制不如LRU,但也非常接近,因此可以接受
  • 二次机会法:二次机会法是对时钟置换算法的一种改良。时钟置换算法在考量需要淘汰的页时借助了访问位,而二次机会法在考量需要淘汰的页需要借助页表项中的访问为和dirty 位(写位),二次机会法想要减少磁盘写的操作,酒尽可能换出的是没有被写过的页,这样在换出时就可以直接释放物理页帧,而不需要将其写回磁盘
    • 实现:需要借助CPU在对某个页写后会将对应的页表项中的写位和访问位都置1的特性(当然若只读过则只修改访问位)。本质上还是环形链表,大体和clock算法相同,只是二次机会法想要换出的目标是访问位和写位同时为0的页。当缺页发生时,会首先探测指针指向的页,若其写位和访问位都为0,则将其作为淘汰页,若访问位为1,写位为0,则将访问位置0,探测下一个页,若写位和访问位都为1,则将写位置0,探测下一个页。综上我们会发现针对访问位和写位同时为1的页其就有两次机会,在第一次被探测时将写位置1,第二次探测时将读位置1,在第三次探测时才真实淘汰掉。通过这样给予被写过的页二次机会的方式,尽量避免一次磁盘的写回操作,提升总体的性能
    • 优点:能够减少磁盘写回的次数,提升性能,且相比LRU来说开销更低
    • 缺点:在缺页次数的方面不如LRU,但其很接近,考虑开销,则其可以被接受
  • 最不常用法(LFU):当缺页发生时,选择访问次数最少的页进行淘汰
    • 实现:对每个页设置访问计数器,当页被访问时,计数器加一,当缺页中断时淘汰数值最小的那个。这样的设计会有一个问题:如果一个页面只在进程开始时被频繁的访问很多次,之后便不再访问,那么那一页就何难被换出去。解决方法:对每一页的计数器赋予时间权重,定期把每页的计数器右移一位
    • 优点:能够有效的控制缺页次数
    • 缺点:实现开销过大
  • belady现象:由计算机科学家Belady发现,在采用FIFO置换算法时,有时会出现分配的物理页面更多反而缺页率提高的异常现象
    • 原因:FIFO算法的置换特征是与进程访问内存的动态特征不相符合的,与置换算法想要达到的目标是相违背的,置换算法想要换出更少使用的页,而FIFO换出的只是在内存中停留时间最久的页。
  • LRU与FIFO算法:本质都是先进先出的思想,只不过LRU算法是对页面访问的时间进行排序,而FIFO是对页面在内存中驻留的时间进行排序。如果每个页在加载到内存后都只访问一次,那么无论是LRU还是其他的置换算法就都退化成为了FIFO算法,因此对于程序的局部性提出了一定的要求。LRU的算法性能较好,但其开销较大。而FIFO算法开销较小,性能较差。因此通常折中选取clock算法,clock算法不必每一次在页面访问时,动态调整页面在链表中的顺序,而仅仅需要硬件完成将访问为或者写位置1的操作。对于内存中那些没有被访问的页,clock算法性能和LRU算法性能接近,但对于曾经被访问过的页,其无法做到和LRU算法那样精确的位置记录

程序局部性的探究

​ 前面的各种算法要向他们能真实的发挥出效用,其都基于一个原理:程序的局部性原理

  • 如果程序局部性原理不成立,那么无论是LRU,还是clock,亦或者LFU,甚至是最优页面置换算法都会退化成为FIFO算法。EG:程序访问的页为单调递增:1,2,3,4,5,6…,那么在物理页有限的情况下,无论采取何种置换算法,每次页的访问必将导致
    缺页中断
  • 如果程序局部性成立,那么如何证明他的存在,如何进行定量的分析?这就是工作集模型需要完成的事情
  • 工作集模型:一个进程在当前时间段正在使用的虚拟页的集合,工作集模型本质上就是窗口大小固定的滑动窗口算法
    • 在一定时间段进程正在使用的虚拟页集合可用W(t, Δ)表示:
      • t:是当前执行的时刻
      • Δ:时间窗口,是一个定值
      • W(t, Δ):在当前时间窗口内,进程访问过页的集合(随着时间的变化,集合内容可能也会发生改变),即工作集的大小
    • image-20230513163009997
    • 通过上图我们发现工作集随着程序的访问会体现出一定的特征,在某些地方局部性很好,在某些地方工作集会迅速扩张或者缩小,那么我们希望根据这样的特征设计出一个基于多个程序的页面置换算法
  • 常驻集:当前时间段实际驻留在内存中的页面集合
    • 工作集是程序在访问内存时的一个固有属性,常驻集取决于系统分配给物理页面的数量,以及所采用的页面置换算法。当工作集中某个所需的页在常住集中不存在则会引发一次缺页中断,因此我们希望能够尽可能的使得在每个时刻工作集与常住集的交集更大。而工作集是固定的,常驻集受操作系统分配给进程物理页的数量以及所采取的局部置换算法有关。

全局页面置换算法

​ 局部置换算法是针对单一的进程而言,且分配给进程的物理页帧是固定的。而事实上,进程对于物理页帧的需求是有阶段性变换的,可能在局部性情况比较好的阶段则不需要太多的物理页帧,在工作集迅速扩张或者缩小的地方也应当相应的动态对进程分配的页帧数进行调整

  • 工作集页置换算法:丢掉工作集时间窗口之外的页,随着时间的执行,即使在没有发生缺页中断的情况下,若有页不属于工作集时间窗口范围内,则将该页换出。这样的算法能够让操作系统有更多的空闲的物理页帧去分配给别的需要降低缺页次数的进程,从总量上减少缺页次数

    image-20230513172055201

  • 基于缺页率的全局置换算法:在进程缺页率较高的情况下分配给进程更多的物理页帧,在进程缺页率很低的情况下,减少一定的物理页帧,使得所有的进程的缺页率保持一个平衡的状态,降低总的缺页次数。具体的:根据缺页的频度来动态的更改工作集时间窗口的大小,并将工作窗口之外的页清空

    • 缺页率:缺页次数 / 内存访问次数
  • 缺页率的影响因素:

    1. 页面置换算法
    2. 分配给进程的物理页帧数
    3. 页面本身的大小
    4. 程序的编写方法
  • 算法整体思路:当缺页发生时,计算上次缺页到这次缺页的时间间隔t,若t > 阈值, 则说明当前缺页率较好可以减少物理页帧的分配,因此清理上次缺页到这次缺页内没有被访问过的页,若t < 阈值,则说明当前缺页率较高需要对这个进程增加物理页帧的分配,因此只将缺的页纳入物理页帧,不做任何清理操作

    image-20230513181424697

抖动问题

​ 如果分配给一个进程的物理页帧数量太少,则会造成很多的缺页中断,则需要频繁的在内存与外存之间进行页的替换,从而使得CPU的利用率降低,大量的时间被浪费在磁盘IO上,使得进程的运行速度变得很慢,对于这种状态称为抖动

  • 产生抖动的原因:随着驻留内存的进程数目增加(或者启动了一个极其消耗内存的进程),能够分配给每个进程的物理页帧数量在不断减少,缺页率在不断上升。此时操作系统会花费大量的时间进行页的置换。因此操作系统需要控制适当的进程数量与进程分配的物理页帧数,使得操作系统在进程的并发数和缺页率之间得到一个平衡

  • 改善抖动问题:平均缺页时间 = 缺页服务时间时能得到较大的并发数已经较好的CPU使用率

    image-20230513183221266

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值