<
  • 主题:
  • + -
  • 清除背景
  • 禁用背景
目 录
  1. 1. 内存管理
    1. 1.1. 程序的装入与链接
      1. 1.1.1. 可执行程序
      2. 1.1.2. 程序的装入
      3. 1.1.3. 程序的链接
    2. 1.2. 内存覆盖与交换技术
      1. 1.2.1. 内存覆盖技术
      2. 1.2.2. 内存交换技术
        1. 1.2.2.1. 交换空间
        2. 1.2.2.2. 进程的换入换出
    3. 1.3. 连续分配管理方式
      1. 1.3.1. 单一连续分配
      2. 1.3.2. 固定分区分配
      3. 1.3.3. 动态分区分配
        1. 1.3.3.1. 动态分区分配的数据结构
        2. 1.3.3.2. 内存分配
        3. 1.3.3.3. 内存回收
        4. 1.3.3.4. 内部碎片与外部碎片
        5. 1.3.3.5. 动态分区分配算法
      4. 1.3.4. 动态重定位分区分配
        1. 1.3.4.1. 紧凑
        2. 1.3.4.2. 动态重定位
    4. 1.4. 离散分配方式
    5. 1.5. 分页存储管理方式
      1. 1.5.1. 页面与物理块
      2. 1.5.2. 页大小
      3. 1.5.3. 分页地址结构
      4. 1.5.4. 页表
      5. 1.5.5. 地址变换
      6. 1.5.6. 快表(TLB)
      7. 1.5.7. 访问内存的有效时间
      8. 1.5.8. 两级和多级页表
        1. 1.5.8.1. 两级页表
        2. 1.5.8.2. 地址变换
        3. 1.5.8.3. 多级页表
      9. 1.5.9. 反置页表
    6. 1.6. 分段存储管理方式
      1. 1.6.1. 分段
      2. 1.6.2. 分段地址
      3. 1.6.3. 段表
      4. 1.6.4. 地址变换机构
      5. 1.6.5. 分页与分段管理对比
    7. 1.7. 段页式存储管理方式
      1. 1.7.1. 基本原理
      2. 1.7.2. 地址变换
      3. 1.7.3. 段页式的优缺点
    8. 1.8. 现代OS内存管理
  2. 2. 虚拟存储器
    1. 2.0.1. 局部性原理
  3. 2.1. 虚拟存储器概述
    1. 2.1.1. 虚拟存储器的特征
    2. 2.1.2. 工作方式
  4. 2.2. 请求分页存储管理方式
    1. 2.2.1. 请求页表
    2. 2.2.2. 缺页中断机构
    3. 2.2.3. 地址变换
    4. 2.2.4. 内存分配策略
    5. 2.2.5. 页面调入策略
      1. 2.2.5.1. 何时调入页面
      2. 2.2.5.2. 从何处调入页面
      3. 2.2.5.3. 页面调入流程
    6. 2.2.6. 缺页率
    7. 2.2.7. 页面置换算法
      1. 2.2.7.1. 最佳(Optimal)置换算法
      2. 2.2.7.2. 先进先出(FIFO)置换算法
      3. 2.2.7.3. 最近最久未使用(LRU)置换算法
      4. 2.2.7.4. 最少使用(LFU)置换算法
      5. 2.2.7.5. 时钟(CLOCK)置换算法
      6. 2.2.7.6. 改进型CLOCK算法
      7. 2.2.7.7. 页面缓冲算法PBA
    8. 2.2.8. 抖动与工作集
      1. 2.2.8.1. 抖动
      2. 2.2.8.2. 工作集与驻留集
      3. 2.2.8.3. 抖动的预防方法
  5. 2.3. 请求分段存储管理方式
    1. 2.3.1. 请求分段的硬件支持
      1. 2.3.1.1. 段表
      2. 2.3.1.2. 缺段中断
      3. 2.3.1.3. 地址变换机构
    2. 2.3.2. 分段的共享
    3. 2.3.3. 分段的保护
    4. 2.3.4. 请求分段的局限性
  6. 2.4. 内存映射文件mmap
    1. 2.4.1. 传统文件读写机制
    2. 2.4.2. 内存映射文件

操作系统(中)

字数:22974 写于:2020-07-30
最新更新:2020-07-30 阅读本文预计花费您66分钟

内存管理

外存管理位于文件管理章节

程序的装入与链接

内存是程序运行的场所,CPU执行的指令和操作的数据都必须位于主存中,操作系统的内存管理首先需要解决程序如何进入内存并建立运行环境的问题

可执行程序

程序从源代码到最终运行,需要经过编译、链接、装入三个阶段:

  • 编译(Compile):由编译程序将用户源程序(.c/.cpp等)编译成目标模块(.obj/.o),生成机器语言代码,但目标程序通常不能直接运行,其中仍包含大量未解析的外部符号(如来自库的函数、全局变量等)和逻辑地址(相对于该模块自身的逻辑地址,无法直接用来寻址)
  • 链接(Link):将一个或多个目标模块与所需库文件组合起来,形成一个完整的可装入模块,该过程需要完成外部符号解析(把对外部符号的引用与其地址关联)和重定位(调整各模块内的相对地址,参考下文装入方式)等工作
  • 装入(Load):操作系统将可执行文件装入内存,为程序分配内存空间,建立运行环境,完成必要的地址转换,使程序能够被CPU执行

程序的装入

程序的装入方式决定了间址时程序中的地址指向内存地址中的哪一块,按地址映射发生的时机和灵活性,分为三种:

  • 绝对装入(Absolute Loading):程序中的地址在编译或汇编时就直接写入主存中的实际物理地址,该地址可由编译器编译时指定,也可由程序员直接赋予,内存装入时按指定地址原样放入内存,不需要重定位。该方式的优点是装入速度快,实现简单,缺点是程序必须事先知道驻留的内存位置,程序只能装入固定位置,内存利用率低,因此只适用于早期单道程序系统
  • 可重定位装入方式(Relocatable Loading,静态重定位):程序编译时采用从0开始的相对地址,装入时由装入程序一次性修改所有地址,转换为实际物理地址,装入完成后地址固定。优点是程序可被装入任意可用内存区域,提高了内存利用灵活性,缺点是一旦装入,程序在运行期间不能移动(地址已固定),且所有地址转换在装入时完成,增加装入开销,用于早期多道批处理系统
  • 动态运行时装入(Dynamic Loading,动态重定位):目标模块仍使用逻辑地址(从0开始),装入时不立即修改地址,而是将程序原样装入内存,真正的地址映射推迟到每条指令执行时,由硬件重定位寄存器(如基址寄存器)动态完成。优点是程序运行期间可以在内存中移动(例如紧凑操作后改变物理位置,只需修改寄存器值);支持虚拟存储器;对换、分页、分段等机制依赖于此。缺点是需专门硬件支持,执行时有微小的地址转换开销。现代操作系统主要使用该装入方式

程序的链接

按链接发生的时间,分为三种:

  • 静态链接(Static Linking):链接发生在程序运行前,在程序运行之前完成全部链接工作,将所有目标模块和库函数合并成一个完整的可执行文件。其优点是可执行文件独立完整,运行时无需额外库文件,缺点是多个进程各自加载相同的库代码,浪费内存和磁盘空间;库更新后需重新链接整个程序
  • 装入时动态链接(Load-time Dynamic Linking):链接发生在装入时,目标模块在装入内存过程中,由系统动态将其与所需库链接,然后一并构成完整映像。其优点是可执行文件内只记录所需库的信息,不包含库代码本身,减小文件体积,多个进程可共享同一份库的纯代码副本,节省内存,缺点是本次运行不需要的库也会被链接。
  • 运行时动态链接(Run-time Dynamic Linking):链接发生在执行过程中,程序开始运行时并不链接所有模块,而是在执行过程中,当某个模块第一次被调用时再完成链接并装入内存。优点是未用到的模块根本不会被装入内存,节省时间和空间,程序可以在运行时决定加载哪些库。缺点是需要操作系统提供动态链接库的支持。现代操作系统普遍采用该方式。

内存覆盖与交换技术

早期计算机主存容量十分有限,经常出现程序无法全部装入内存运行的问题。为提高有限内存的利用率,早期操作系统提出了内存覆盖(Overlay)技术和内存交换(Swapping)技术

内存覆盖技术

覆盖技术(Overlay)的核心思想是将程序划分为多个互斥执行的模块,使其“分时共享”同一块内存区域。如:将某个程序拆分为A(main模块,占用8K)、B(占用8K)、C(占用10K)三个模块,其中模块A为主程序模块,常驻内存的固定区,模块B和模块C互斥分时执行,共享覆盖区内存(内存空间大小为二者较大值10K),当模块B执行完毕后即调入模块C并覆盖模块B的内存空间,以此实现在有限内存中运行较大程序。

覆盖技术需要由程序员手动规划模块的调用关系和内存布局,这导致软件开发极其复杂、极易出错,且程序难以移植。随着操作系统实现了虚拟内存技术,由硬件和系统自动完成内存的换入换出,因此覆盖技术已全面淘汰,只用于早期操作系统

内存交换技术

交换(Swapping)又称为对换技术,其核心思想是将内存中暂时不能运行的进程或暂时不用的程序和数据换出到外存上,再把已经具备运行条件的进程或进程所需数据换入内存。根据进程交换时数据量的不同,交换分为以下两类:

  • 整体交换:以整个进程为单位进行交换,因此又称为进程交换,或者处理机中级调度。
  • 页面(分段)交换:以进程的一个“页面”或“分段”为单位进行交换,因此又称页面交换/分段交换,或者部分交换
交换空间

交换空间(Swap Space):为支持进程交换,操作系统在外存预留的一块专门用于保存被换出进程的存储区域,不同于普通文件系统中的文件区,交换空间管理采用连续分配方式,并针对大块顺序读写进行优化,以减少寻道和碎片带来的开销。现代操作系统中,交换空间一般以独立交换分区(Swap Partition)交换文件(Swap File)的形式存在,主要服务于虚拟内存管理

课本描述:OS把磁盘空间分为文件区和交换区:

  • 文件区:占用大部分磁盘空间,用于存储文件,首要目标是提高空间利用率,因此使用离散分配方式
  • 交换区:占用少量磁盘空间,用于存放从内存换出的进程,首要目标是提高进程换入和换出速度,因此使用连续分配方式,较少考虑外存碎片问题
进程的换入换出

进程的换入换出通常在内核需要执行某个操作但内存不足时进行

  • 进程的换出:通常首选处于阻塞状态睡眠状态的进程。换出时,只换出进程中非共享的程序和数据段,对于共享的程序和数据段,只有还有进程需要就不能换出。换出只涉及进程的数据段和代码段,进程的PCB会留在内存中方便OS管理进程信息,进程被换出后通常处于挂起就绪挂起阻塞状态。
  • 进程的换入:交换进程定时执行换入操纵,它首先检查PCB集合中所有进程的状态,找出处于就绪状态但已换出的进程,当有多个该类进程则考虑其优先级和已经换出到磁盘的时间。然后为其申请内存,将其从外存调入内存,并修改其PCB中的相关信息。

连续分配管理方式

连续分配方式是最早出现的内存分配方式,广泛用于早期操作系统,其特点是OS会为用户进程分配一个连续的内存空间,分为单一连续分配、固定分区分配、动态分区分配、动态可重定位分区分配四种方式

单一连续分配

用于早期单道程序环境,其内存空间被分为系统区用户区两部分,用户区中只有一道用户程序,因此整片连续的用户区空间都属于该程序,不太需要内存保护(部分系统有,但MS-DOS等系统没有)。该分配方式实现简单,无外部碎片,但内存利用率低,有内部碎片(程序实际使用空间小于所分配的空间造成),只能用于单用户、单任务系统。

固定分区分配

为了支持多道程序运行,固定分区分配方式将用户空间划分为若干个固定大小的分区,在每个分区中只装入一道作业,以保障程序之间不会相互干扰。根据分区方法它分为:

  • 分区大小相等:所有内存分区大小相等,其缺点是缺乏灵活性,程序太小会造成内存浪费,程序太大又会导致无法装入。但是很适合用于用一台计算机控制多个相同对象的场合(如:工业上为多台一模一样的机器分配相同的内存)
  • 分区大小不等:将内存空间分为多个大小不等的分区,如:分为多个较小分区,适量中等分区,少量大分区,根据程序大小分配适当的分区。该方式增加了灵活性,可以满足不同大小的进程需求

该分配方式支持多道程序设计,实现简单,无外部碎片,但缺乏灵活性,存在严重的内部碎片

分区使用表:为方便分配与回收内存,OS需要建立一张分区使用表,记录每个分区的起始地址、大小、状态(是否已分配)等信息。

动态分区分配

动态分区分配又称为可变分区分配,这种分配方式不会预先划分内存分区,而是在进程装入内存时,根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要。因此系统分区的大小和数目是可变的。

动态分区分配的数据结构

该分配方式下系统记录空闲分区信息,常用两种数据结构:

  • 空闲分区表:使用一张表记录每个空闲分区的情况,每个表项记录一个空闲分区的区号、起始地址、大小等信息
  • 空闲分区链:每个分区的起始部分和末尾部分分别设置一个指向上一个/下一个分区的指针,将所有分区链接成一个双向链,每个分区起始部分还会记录分区大小等信息。
内存分配

按分区分配算法(参考下文)找到分区后,若分区大小大于需求,则从该分区中划出所需部分,剩余部分仍作为空闲分区返回给OS并保留在链/表中(外部碎片产生的原因)

内存回收

进程释放内存时,系统需要将回收区插入空闲链/表。此时有四种合并情况:

  • 回收区与前一个空闲分区相邻:回收区与前一空闲分区合并,只需要修改前一分区表项中的空间大小,不需要为回收分区建立新表项
  • 回收区与后一个空闲分区相邻:回收区与后一空闲分区合并,使用回收区首地址作为合并后空闲区的首地址,大小为二者之和
  • 回收区与前、后两个空闲分区都相邻:将三个分区合并,合并后的分区首地址是前分区的首地址,大小为三者之和,删除后一个分区的表项
  • 回收区不邻接任何空闲分区:单独建立新表项,并按首地址插入到空闲表/链中的适当位置,大小和首地址为回收区的大小和首地址
内部碎片与外部碎片
  • 内部碎片:指已分配给进程的内存空间中,因分配单元大于实际需求而未被利用的部分。这些空间已经属于某个进程,其他进程无法使用,因此造成内存浪费。如:页大小为 4KB,某进程实际需要 10KB,系统为其分配 3 页(12KB),则最后一页剩余的 2KB 就属于内部碎片。该问题可通过减小分配粒度缓解。
  • 外部碎片:指内存中存在许多零散、互不连续的空闲区域,这些零散的空闲空间虽然总量足够,但由于没有足够大的连续空间而无法满足进程使用的空闲空间。该问题可采用紧凑(拼接)技术、非连续分配缓解。
动态分区分配算法

当有多个空闲分区都能满足分配需求时,需要通过动态分区分配算法决定从哪个空闲分区中切割内存

  • 首次适应算法(First Fit,FF):该算法要求空闲空间按地址递增排列(于表中或链表中),然后按照内存地址从低到高扫描空闲分区,找到第一个满足要求的分区后立即分配。其优点是查找速度较快,无需遍历整个空闲分区表,高地址空间通常保留较大的连续空闲区。缺点是低地址区域容易形成大量小碎片;长时间运行后,低地址部分空闲分区越来越零散。
  • 循环首次适应算法(Next Fit,NF):这是对首次适应算法的改进,从上次分配结束的位置开始,向后查找第一个满足要求的空闲分区,而不是重新从内存起始地址开始扫描。该算法减少了低地址区域频繁搜索的问题,空闲分区利用更加均衡。但搜索时可能跳过前面更合适的空闲分区,且缺乏大空闲分区,可能导致大作业无法装入
  • 最佳适应算法(Best Fit,BF):该算法要求空闲分区按照容量从小到大的顺序排列,其核心思想是找到能满足要求的最小的空闲分区后立即分配(即找到的第一个满足要求的空闲空间),其优点是每次分配后剩余空间最少,理论上内存利用率较高。但查找时间较长,容易产生大量极小空闲分区,长期运行后外部碎片较严重
  • 最坏适应算法(Worst Fit,WF):其策略与最佳适应算法相反,该算法通常按空闲分区大小递减组织空闲分区表,每次选择最大的空闲分区进行分配,其优点是每次分配后剩余空间仍然较大,不容易立即产生无法利用的小碎片。缺点是空闲分区会不断被切割,当大进程到来时可能没有足够大的连续空间

动态重定位分区分配

动态重定位分区分配是在动态分区分配的基础上添加了紧凑和动态重定位技术,以消除外部碎片。由于动态分区分配采用连续分配方式,随着进程不断创建、结束和释放内存,会产生大量外部碎片。这些碎片(又称零头)分散在各正在使用内存空间之间,需要将它们合并为连续空间后才能通过连续分配方式分配。

紧凑

紧凑(Compaction)(也称拼接或压缩)是动态分区存储管理中用于消除外部碎片的一种内存整理技术,其基本思想是对内存中的已分配分区进行移动,使它们相邻接,从而使原本分散的空闲分区合并成一个连续的大空闲区,从而满足较大内存申请的需求。

动态重定位

紧凑后的用户程序在内存中位置发生了改变,因此也需要对程序和数据地址加以修改变换,以免CPU寻址到错误的内存地址。动态重定位是指程序执行过程中,CPU访问内存时由硬件将逻辑地址动态转换为物理地址,以此实现进程移动后,只需修改对应的地址映射信息,无需修改程序中的地址引用即可正确访问数据,动态重定位必须依赖以下硬件机制:

  • 动态重定位寄存器:在程序运行时通过重定位寄存器(基址寄存器)将逻辑地址转换为新的物理地址,从而程序无需关心物理地址变化
  • 地址变换机构:PU在执行指令时,通过“逻辑地址 + 重定位寄存器中的基址”实时计算物理地址,保证紧凑后程序仍能正确运行

注意,若采用静态重定位,程序装入后地址已固定,进程运行过程中不能随意移动,因此无法实施紧凑技术

离散分配方式

连续分配方式要求OS需要为用户进程分配连续的内存空间,这会导致大的空闲内存空间不断被分割,剩余部分形成许多零零散散的小空闲区,虽然可以通过紧凑技术拼接这些碎片,但需要付出较大系统开销。

离散分配方式允许将进程离散地装入不相邻的分区中,可以有效解决外部碎片问题,并能提高内存利用率,离散分配方式主要分为三种:分页存储管理方式、分段存储管理方式、段页式存储管理方式

分页存储管理方式

页面与物理块

分页存储管理方式将用户程序的逻辑地址空间分为若干大小固定的区域,称为页(Page)页面,每个页面的编号称为页号,从0开始,表示为:第0页,第1页等。相应地将物理内存空间划分为与页面大小完全相同的块,称为页框(Frame)物理块(部分场景下又称页帧、内存块、物理页等),页框的编号称为块号,也是从0开始,表示为:0#块、1#块等。

通过页到物理块的映射,可将用户程序的任一页装入任一空闲页框中,实现离散分配。同时,多个逻辑页(甚至来自不同进程)还可以映射到同一个物理页框,实现共享内存。

页大小

页面大小通常由硬件(CPU架构)决定,通常为2的幂,操作系统可在一定范围内进行选择,如:x86、x86_64、ARM64架构的标准页大小均为 4KB,但它们都支持大页 2MB 和巨页 1GB,用于提升 TLB 覆盖范围和数据库等大内存应用的性能。装入内存时,进程的最后一页通常装不满一个物理块,最终形成不可利用的碎片,称为页内碎片

分页地址结构

以32位地址长度为例

loading

它包含两部分:

  • 页号:图示中为高12-31位,意味着最多允许有1M页。即如果长 M 位,表示地址空间最多允许有 2 M
  • 页内地址:即页内偏移量,现代计算机几乎都是按字节编址的,CPU能达到字节级寻址颗粒度,每个页内地址对应着 1 Byte存储空间。图示中页大小为 4KB,因此0-11位为页内地址,即长 K 位,表示一个页面大小为 2 KBytes

假设给定一个逻辑地址A,则有:

  • 页号 = 逻辑地址 / 页面大小(取整)
  • 页内偏移量 = 逻辑地址 % 页面大小
    假设 A = 2170B,系统页面大小为 1KB,则有: 页号 = 2170/1027 = 2 页内地址 = 2170%1027 = 122

页表

为了保证进程能在内存中找到每个页面所对应的物理块,OS会为每个进程建立一张页表(通常位于进程控制块PCB中):

  • 每个页面对应一个页表项
  • 每个页表项由页号(可隐含)、块号组成,有的实现还包含一些存取控制字段
  • 页表负责实现从页号到物理块号的地址映射
  • 现代操作系统中,一个进程可能包含多个页表
loading
页表项中的页号通常是隐含的,假设某个系统物理内存大小为4GB,页面大小为4KB,页表项不包含存取控制字段,则每个页表项占用字节数为: 4GB内存会被分为:232/212=220个物理块 内存块号为:0~(220-1) 内存块号需要 20 bit 表示,由于存储空间按字节分配,因此至少需要 3Bytes空间 由于页号隐含,页表项只包含内存块号,因此每个页表项占用3字节 在找出页表项时,通过页表存储起始地址+3*i即可找出第i号页表项的存放地址

地址变换

地址变换机构在现代计算机中通常由 CPU 内的内存管理单元MMU(Memory Management Unit)实现,其主要任务是将用户地址空间中的逻辑地址(或虚拟地址)转换为内存空间中的物理地址。在分页存储管理中,由于页内地址与物理块内地址是一一对应的,因此地址转换只需要完成页号到物理块号的转换,这一映射关系主要由页表维护,MMU 通过查找页表(或命中快表 TLB)完成地址转换

页表通常驻留在内存中,当进程被调度运行时,PCB 中保存的页表基址和页表长度等内存管理信息会被装入 CPU 内 MMU 的页表寄存器PTR(Page-Table Register)中,完成该进程的分页管理环境配置,当进程要访问某个逻辑地址时,按以下步骤执行:

  • 地址变换机构从逻辑地址中分离出页号P和页内偏移量W
  • 将页号和页表长度进行比较,检查是否越界,如果越界,发出地址越界中断
  • 若不越界,则通过 页表起始地址+页号*页表项长度方式计算出页表项地址,取出物理块号
  • 最终物理地址 = 物理块号 * 页面大小 + 页内偏移量W
现代CPU中页表项通常是固定的(如:一个页表有512项),因此页表寄存器PTR在现代CPU中的实现只有一个用于保存页表基址的寄存器(如:x86中的CR3),不需要专门存储页表长度的寄存器

快表(TLB)

由于页表是存放在内存中的,这使CPU在每存取一个数据时,都要两次访问内存。第一次是访问内存中的页表,第二次才是从该物理地址中读出/写入数据,两次访问主存的操作极大降低了计算机处理速度。

为提高地址变换速度,通常在CPU地址变换机构MMU(内存管理单元)中增设具有并行查询能力的高速缓冲寄存器,称为联想寄存器(Associative Memory)或快表TLB(Translation Lookaside Buffer),用于存放最近使用的页表项,当CPU给出逻辑地址后,具有快表的地址变换机构会按照以下方式执行:

  • 从逻辑地址中分离出页号P和页内偏移量W
  • 首先在TLB中查找是否有匹配的页号,如果命中,则直接从快表中读出物理块号;如果未命中,则查询内存中的页表,并在找出物理块号后同步更新该页表项到快表
  • 构造出物理地址

访问内存的有效时间

内存的有效访问时间(Effective Access Time, EAT):指从进程发出指定逻辑地址的访问请求,经过地址变换,到在内存中找到对应的实际物理地址单元并取出数据,所需要花费的总时间

假设访问一次内存的时间为t,则:

  • 无快表的分页存储管理方式:EAT = 2t
  • 有快表的分页存储管理方式:假设 λ 为查找快表时间,a 为命中率,则EAT=命中时的快表查询时间 + 不命中时查询时间(查块表+一次访存) + 实际存取数据时间,即:
    EAT = a × λ + ( t + λ )( 1 - a ) + t = 2t + λ - t × a

两级和多级页表

现代计算机系统普遍支持庞大的逻辑地址空间,这直接导致页表规模急剧膨胀,若采用单级页表,其所需的连续物理内存空间将难以得到保证,同时过大的页表也会导致地址转换效率降低。多级页表的核心思想是将原本庞大的页表进行分页管理,通过离散方式存放页表。

两级页表

为离散存放的页表再建立一张页表,称为外层页表(Outer Page Table),又称页目录表、顶层页表,它是位于最顶层的页表,其页表项指向存放次级页表内容的物理页框号。

以 32 位逻辑地址空间为例,假设页面大小为 4KB,每个页表项占用 4B 1. 如果使用单级页表,则有: 页面大小为 4KB,页内地址占用12位地址 页号拥有20位地址,系统支持最多 2 20个页面(1048576个) 整个页表需要占用:2 20 x 4 B = 4MB 如果系统同时运行着数百个进程,每个进程都拥有自己的页表,它们都需要数MB的连续存储空间(即便该进程本身总共也只需要数MB存储空间) 2. 如果使用二级页表,则有: 每个物理块能存储:4KB / 4B = 1024 个页表项 由此,可将整个大页表(最多含1048576个页表项)拆分为多个小页表,共可以拆出来1024个小页表,它们可以离散存储,每个小页表包含1024个页表项,占用一个物理块 将这些小页表编号:0#页表-1023#页表
loading
地址变换

多级页表相应地要从地址中拆分几位用来表示外层页表的页号,以上述32 位逻辑地址空间的两级页表为例:

loading

逻辑地址被分为三部分:

  • 页内偏移量:页大小为4KB,因此占用低12位
  • 二级页号(外层页内地址):由于每个小页表包含1024个页表项,因此占用地址10位
  • 一级页号(外层页号):占用剩余地址位数

为方便地址变换,从逻辑地址解析出物理地址,需要在地址变换机构中增设一个外层页表寄存器,存放外层页表的起始地址,按下述步骤求出物理地址:

  • 根据一级页号查外层页表,得二级页表的物理块号
  • 根据二级页号查二级页表,取出物理块号b
  • 物理地址 = 物理块号 * 页面大小 + 页内偏移量
多级页表

对于64位页大小为4KB的机器,如果采用两级页表,则页内偏移量占用12位地址,假设每个物理块中的小页表依旧存储1024个页表项,占用10位作为二级页号,则外层页号剩余42位,外层页表最多拥有4096G个页表项,需要占用16384G连续内存空间,使用二级页表显然也不够分配内存空间。因此通常采用多级页表,即对外层页表再进行分页。

即便如此,对于64位计算机,由于其能支持264=1844744TB规模的物理空间,而实际应用又没有此必要,因此大多数OS将可直接寻址的内存空间减少为48位长度(支持256TB内存),使用4级页表,部分OS可配置拓展为57位(支持128PB内存),并使用5级页表

反置页表

(已弃用,仅了解)

传统页表以页号为索引,记录的是页号对应的物理页框号,随着现代计算机逻辑地址空间增大,传统页表占用连续内存过多,且每个进程需独立维护一张页表,开销巨大。

反置页表的核心是为每一个物理页框设置一个页表项,页表项按照物理页框号排序,内容是:

  • 该物理块对应的页号
  • 该物理块所属进程PID
  • 保护位/标志位等信息

这样,整个系统只有一张页表,所有进程共用,页表大小与物理内存大小成正比(页表项数量 = 物理内存大小 / 页面大小),与进程数量无关。但由于反置页表无法直接索引,需通过哈希表查找虚拟地址对应的物理页框,现代系统物理内存巨大,反置页表自身也及其巨大,因此现代操作系统不再使用反置页表

分段存储管理方式

引入分段管理主要为了满足用户在编程和使用上的多方面需求:

  • 方便编程:用户更希望按逻辑模块(主程序、子程序、数据区等)来组织地址空间,无需人工拼接一个连续线性空间
  • 信息共享:分页系统中的页只是存放信息的物理单位,一个共享过程往往需要占用数十个页,不方便管理。因此对于需要共享的模块化程序和数据,段才是逻辑上的共享单位。
  • 信息保护:对逻辑段设置不同保护权限(读、写、执行),比按页保护更自然
  • 动态增长:某些数据段(如栈)在运行期长度可变,分段系统能灵活支持段的动态伸缩
  • 动态链接:分段为动态链接提供了基础,链入的往往是整个段,运行时才链接,节省内存开销

分段

分段存储管理方式中,作业的地址空间按照逻辑关系划分为若干,如:主程序段 MAIN、子程序段 X、数据段 D、栈段 S 等,这些段有以下特点:

  • 每个段有各自的段号
  • 各段都从0开始编址
  • 各段长度并不相等,且段长可变

分段地址

分段系统的逻辑地址由段号(段名)段内地址(段内偏移量)组成:

  • 段号位数决定了每个进程持有内存最多可以分为几个段
  • 段内地址位数决定了每个段的最大长度
loading

该例子中,段号占用16位,最多有216=64K个段,段内地址占16位,每个段最大长度是64KB

段表

段表是系统为每个进程建立的一张段映射表,用来记录各个段在内存中的物理位置,每个段表项包含段基址段长,段号是隐含的,不占用段表存储空间

loading

地址变换机构

段表通常存放在内存中,当进程被调度运行时,PCB 中保存的内存管理信息会被装入 CPU 内 MMU(内存管理单元) 中的寄存器中,以完成该进程的内存管理环境配置,其中段表基址(指明段表在内存的起始地址)会被装入段表基址寄存器(STBR)段表长度(指明段表长度,即段表项数量)会被装入段表长度寄存器(STLR)。随后,当 CPU 执行指令访问内存时:

  • CPU 产生逻辑地址 (段号 s,段内偏移 d)
  • 检查段号是否合法:MMU 将段号 s 与 STLR 比较;若 s ≥ STLR,则发生段号越界中断
  • 查找段表:MMU 根据 STBR 找到当前进程段表起始地址,以 s 为索引读取第 s 个段表项,获得该段的段基址 B 和段长 L(以及访问权限等信息)
  • 检查段内偏移是否合法:若 d ≥ L,则发生段内地址越界中断
  • 形成物理地址:物理地址 = B + d,CPU 再依据该物理地址访问主存
  • 类似于分页系统,可在MMU内设置快表保存近期使用的段表项,减少访存次数

分页与分段管理对比

  • 页是信息的物理单位。分页的主要目的是为了实现离散分配,提高内存利用率。分页是系统行为,对用户是不可见的。
  • 段是信息的逻辑单位,分段的主要目的是更好地满足用户需求,分段对用户是可见的
  • 页的大小固定且由系统决定。段的长度却不固定,决定于用户编写的程序。
  • 分页的用户进程地址空间是一维的,只有连续的页号,页内地址由硬件处理
  • 分段的用户进程地址空间是二维的,其地址既要给出段号,也要给出段内地址
  • 分段比分页更容易实现信息的共享和保护,可重入代码(Reentrant Code)又称为纯代码(Pure Code),是一种允许多个进程同时访问的代码,为使各进程所执行的代码完全相同,可重入代码不允许任何进程对它进行修改,可修改的代码是不能共享的

段页式存储管理方式

分页存储管理使用页面作为内存分配基本单位,提高了内存利用率,分段存储管理符合程序逻辑结构,段页式存储管理结合了两者的优点:程序按逻辑划分为多个段,每个段再划分为固定大小的页,既满足程序的逻辑组织,又便于离散分配内存。

基本原理

段页式存储管理(Segmentation with Paging)是分段存储管理和分页存储管理相结合的内存管理技术,即先将用户程序分成若干个段,再把每个段分成若干个页。在段页式系统中,其地址结构由段号、段内页号及页内地址三部分所组成:

loading
+ 段号的位数决定了每个进程最多可以分几个段,上述例子中,段号占16位,因此在该系统中,最多有216=64K个段 + 页号位数决定了每个段最大有多少页,上述例子中,页号占4位,因此每个段最多有16页 + 页内偏移量决定了页面大小,上述例子中,页内偏移量占12位,因此每个页面\每个内存块大小为212=4096=4KB + 分段对用户可见,程序员编程时需显式给出段号和段内地址;分页对用户不可见,硬件自动完成分页。因此段页式管理的地址结构是二维的

地址变换

段页式系统中需要同时配置段表和页表:

  • 每个进程拥有一张段表,段表项不再是段内存起始地址和长度,而是页表的起始地址和长度
  • 每个段各自维护一张页表,页表项记录该段中逻辑页号与物理块号(页框号)的对应关系

当进程被调度运行时,PCB 中保存的段表信息(段表始址和段表长TL)会装入 CPU 内 MMU 的段表寄存器,完成当前进程内存管理环境的配置,随后地址变换过程如下:

  • CPU 产生逻辑地址(段号 s,页号 p,页内偏移 d),随后 MMU 从逻辑地址中提取段号S、页号P、页内偏移量d
  • MMU 比较段号S和段长TL,如果S ≥ TL,则产生越界中断
  • 如果没有段号越界,根据段表始址 + S × 段表项长度找到对应段表项,从中获取该段的页表始址和页表长度
  • MMU 比较页号 p 和 页表长度,若 P 大于等于页表长度,则产生越界中断
  • 如果没有页号越界,根据页表始址和页号P找到对应页表项,读出该页所在的物理块号b
  • 形成物理地址:物理地址 = 物理块号b × 块大小 + 页内偏移量d
  • 段页式系统为了获取一条指令或数据,需要访问三次主存,第一次是访问段表,第二次是访问页表,第三次才是读取指令或数据
  • 为提高访问速度,依旧可以设置快表,快表的关键字由(段号,页号)组成,值为对应的物理块号和保护码,若快表命中,只需1次访存

段页式的优缺点

段页式兼具分段和分页的优点,既可以按程序逻辑组织代码、数据、栈等不同区域,并支持按段进行共享和保护,也兼具了页面可离散存放,提高内存利用率的优点。但缺点是需要同时维护段表和页表,系统开销较大,地址变换需要先查段表,再查页表,过程比单纯分页更复杂

现代OS内存管理

历史上,8086 等早期处理器采用硬件分段机制,CPU 通过 CS、DS、SS 等段寄存器参与地址形成;80386 在此基础上进一步发展出完善的分段和分页机制,并支持段页式管理。

随着分页技术和虚拟内存的发展,现代 64 位操作系统已基本不再依赖硬件分段进行内存管理,而是保留代码段、数据段、只读数据段、堆、栈等逻辑组织方式,由编译器、链接器和操作系统共同维护其布局。以 C/C++ 为例,const 等只读数据通常由编译器和链接器放入 .rodata段,但操作系统并不识别“.rodata 段”这一概念,而是将对应页面映射为只读属性(Read Only),MMU 根据页表权限阻止写入;而在 JavaScript、Python、Shell 等解释型语言中,通常不存在 .rodata 段,const、readonly 等语义由语言运行时(解释器或虚拟机)负责实现,操作系统底层仍仅按分页机制管理内存,并统一采用 MMU、TLB 和多级页表完成地址转换。

虚拟存储器

常规存储管理方式具有以下缺陷:

  • 一次性:程序必须一次性全部装入内存才能运行,导致大作业无法在小内存中运行
  • 驻留性:程序运行期间始终驻留内存,即使大量代码暂时不会执行,也不释放

局部性原理

局部性原理指程序执行时存在局部性现象,即在一个较短时间内,程序执行仅限于某个部分,其访问的存储空间也限于某个区域。表现在:

  • 时间局部性:一条指令或数据被访问后,不久可能再次被访问(常见于循环、子程序)
  • 空间局部性:一旦访问了某个单元,其附近的单元也可能很快被访问(因为程序通常顺序执行,且访问数组)

虚拟存储器概述

虚拟存储器指具有 请求调入 和 置换 功能,能从逻辑上对内存容量加以扩充的一种存储器系统。

虚拟存储器的逻辑容量取决于外存和内存容量之和,其实现都建立在内存离散分配基础上,运行速度接近于内存速度。虚拟存储器的实现主要有请求分页、请求分段、请求段页式三种,现代操作系统几乎全部采用请求分页(Demand Paging)

虚拟存储器的特征

  • 多次性:程序无需一次装入,而是允许被分成多次调入内存运行
  • 对换性:作业所含程序和数据,无须在作业运行时一直常驻内存,而是可在内存外存之间换入换出
  • 虚拟性:能从逻辑上扩充内存容量,使用户看到的内存容量远大于实际内存容量。虚拟性是以多次性和对换性为基础的,只有支持多次调入,数据换入换出才能实现虚拟存储器

工作方式

基于局部性原理,应用程序在运行之前没必要装载全部内容到内存,而仅须装载那些当前要运行需要的少数页面或段便可运行,其余部分暂留在盘上。程序在运行时,但如果程序所要访问的页(段)尚未调入内存(称为缺页或缺段),便发出缺页(段)中断请求,此时 OS将利用请求调页(段)功能将它们调入内存,以继续执行程序。如果此时内存已满,无法再装入新的页(段),OS还须再利用页(段)的置换功能,将内存中暂时不用的页(段)调至盘上,腾出足够的内存空间后,再将要访问的页(段)调入内存,使程序继续执行下去。这样,便可使一个大的用户程序在较小的内存空间中运行,也可在内存中同时装入更多的进程,使它们并发执行。

请求分页存储管理方式

分页请求系统是在分页系统的基础上增加了请求调页功能页面置换功能所形成的页式虚拟存储系统,它允许用户程序只装入少数页面的程序和数据即可启动运行,之后再通过调页功能及页面置换功能陆续地把即将运行的页面调入内存,同时把暂不运行的页面换出到外存上,完成虚拟内存拓展,置换时以页面为单位。

为了能实现请求调页和页面置换功能,系统必须提供必要的硬件支持和实现请求分页的软件。需要的硬件支持包括请求页表机制、缺页中断机构、地址变换机构;所需的软件包括用于实现请求调页的软件和实现页面置换的软件。

请求页表

请求页表实际上只是在纯分页的页表基础上增加了若干字段:

loading
  • 状态位(存在位)P:只占用一位,因此又称位字,用于指示该页是否已调入内存,以区分应用程序的一部分调入内存,还有一部分仍在外存磁盘的情况
  • 访问字段A:用于记录本页在一段时间内被访问的次数,或记录本页最近已有多长时间未被访问,提供给置换算法(程序)在选择换出页面时参考
  • 修改位M:标识该页在调入内存后是否被修改过。在置换该页时,如果该页在内存中已经被修改,则需要写回硬盘,如果未被修改,则无需写回
  • 外存地址:用于指出该页在外存上的地址,通常是物理块号,供调入该页时参考

缺页中断机构

在请求分页系统中,当访问的页面不在内存(P=0)时,便产生缺页中断。缺页中断属于内部中断(异常,但不是错误),同样需要经历CPU现场保护、分析中断原因、转入中断处理程序、恢复CPU现场等步骤,但它有一些特殊点:

  • 一般的中断通常在CPU执行完一条指令后才会检查并处理,但缺页中断在指令执行期间产生并立即处理,处理后会重新执行引发中断的指令
  • 一条指令执行期间可能产生多次 缺页中断,如:执行指令copy A to B,假设指令和数据本身需要跨两个页面,则可能产生6次缺页中断

地址变换

请求分页系统中的地址变换是在分页系统地址变换的基础上,增加了实现虚拟存储器系统的换出/换入和缺页产生和处理功能,其工作流程导致为:

  • CPU需要访问某页
  • MMU检查页号是否大于页表长度,是则产生越界中断
  • 无越界则查询快表,如果命中,则修改访问字段A(用于表示该页近期已被访问过,用于给置换算法提供参考)和修改位M(执行写指令时修改该字段,方便页被置换时决定是否需要写回硬盘),随后提取物理块号计算出物理地址。注意,一个页的页表项在快表中意味着该页一定在物理内存中,而不会被换出到硬盘;同样,某个页被换出到硬盘时一定会修改快表,来让对应页表项失效(TLB一致性)
  • 快表未命中,则访问内存中的页表,检查页表项的状态位P,检查该页是否已被调入内存:
    • 页在内存中,则修改访问字段和修改位字段,然后提取物理块号
    • 页在外存,发出缺页中断,然后从外存找到缺页,然后尝试调入内存:
      • 如果内存未满,直接读入缺页
      • 如果内存已满,则选择内存中的一个页换出。换出前检查该页的修改位M。如果该页在调入内存后已经修改过,则需要将该页写回外存。如果未修改,则直接读入缺页进行覆盖
    • 缺页被读入内存后,缺页中断执行完毕,修改页表,记录该页所处物理块号
  • 由于CPU是从内存页表获取的页表项,因此需要将该页表项更新到快表,随后修改”访问”字段和”修改位”字段,CPU获得物理块号,计算出物理地址 = 物理块号 * 页面大小 + 页内偏移量W,地址转换完毕

    内存分配策略

    请求分页系统中,内存分配策略分为固定分配和可变分配,页面置换则分为全局置换和局部置换,由此组合出以下三种策略:
  • 固定分配局部置换:系统为每个进程分配一定数量的物理块,在整个运行期间都不改变。若进程在运行中发生缺页,则只能从该进程在内存中的页面中选出一页换出,然后再调入需要的页面。该策略缺点是难以提前确定应该为进程分配多少物理块,在采用固定分配策略时,可采用平均分配算法(将物理块平均分给进程,由于进程大小不一因此会造成不公平)、按比例分配算法(根据进程大小按比例分配物理块)、考虑优先权分配算法(一部分按比例分配给各进程,另一部分按进程优先级进行分配)。
  • 可变分配全局置换:进程分配到的物理块数可以动态变化,当进程缺页需要调入一个页面时,OS会尝试从空闲物理块中取出一块分配给该进程,若已无空闲物理块,则可以从全局所有进程的全部物理块中选择一块换出。该策略缺点是被选中的进程拥有的物理块会减少,缺页率会增加
  • 可变分配局部置换:进程分配到的物理块数可以动态变化,当进程缺页时,只能从该进程内存页中选择一页换出,因此不会影响其他进程。如果该进程频繁缺页,则系统会为该进程分配若干附加物理块,直到缺页率减少到适当程度;反之,如果进程缺页率很低,则会适当减少分配给该进程的物理块数。

页面调入策略

为了使进程能正常执行,必须事先将要执行的那部分程序和数据调入内存,其核心问题是:何时调入,何处调入,如何调入

何时调入页面
  • 预调页策略(运行前调入):根据局部性原理,一次调入若干个相邻的页面可能比一次调入一个页面更高效。但如果提前调入大量页面而大多数又不会被访问,则又是低效的。因此可以预测可能访问到的页面,将它们预先调入内存,但目前预测成功率只有50%左右。故这种策略主要用于进程的首次调入,由程序员指出应该先调入哪些部分。
  • 请求调页策略(运行时调入):进程在运行期间发现缺页时才将所缺页面调入内存。由这种策略调入的页面一定会被访问到,但由于每次只能调入一页,而每次调页都要磁盘I/O操作,因此I/O开销较大。
从何处调入页面

请求分页系统的外存分为两部分:

  • 文件区:存储空间离散分配,读写速度较慢,存储空间大
  • 交换区(Swap Space):存储空间连续分配,读写速度比文件区快,但存储空间一般较小

当发生缺页请求时,系统调入缺页的来源有以下三种情况:

  1. 系统拥有足够的对换区空间:页面的调入、调出都是在内存与对换区之间进行,这样可以保证页面的调入、调出速度很快。在进程运行前,需将进程相关的数据从文件区复制到对换区。
  2. 系统缺少足够的对换区空间:凡是不会被修改的数据都直接从文件区调入,由于这些页面不会被修改,因此换出时不必写回磁盘,下次需要时再从文件区调入即可。对于可能被修改的部分,换出时需写回磁盘对换区,下次需要时再从对换区调入。
  3. UNIX方式:运行之前进程有关的数据全部放在文件区,故未使用过的页面,都可从文件区调入。若被使用过的页面需要换出,则写回对换区,下次需要时从对换区调入。
页面调入流程
  • 当程序所要访问页面未在内存中时,快表一定未命中,CPU通过页表查询到该页页表项的存在位为0,便发出缺页中断,中断处理程序首先保留CPU环境,然后转入缺页中断处理程序,该程序通过页表查找到所需页在外存的物理块后,尝试将其调入内存
  • 如果内存能容纳新页,则启动磁盘 I/O,将所缺页面从外存调入内存空闲页框,修改页表(修改页框号;将存在位置为1)。缺页中断处理结束,恢复进程执行,CPU会重新执行原指令,此次访问该页,快表仍未命中,通过页表查询到物理块号计算出物理地址,并将该页表项更新到快表
  • 如果内存已满,则需要通过页面置换算法,从内存选择一页换出,如果该页已被修改过,则还需将其写回磁盘,如果未被修改,可以直接释放该页。随后修改淘汰页面的页表项(将存在位置为0,清除页框号等信息),并将淘汰页的页表项同步到快表(TLB一致性处理,防止TLB中的旧页表项映射已经被释放的物理页框)。之后从外存调入所需页到本释放的页框,修改新调入页的页表项(修改页框号;将存在位置为1),之后CPU重新执行指令并访问该页时,该页表项会被更新到快表中

缺页率

假设进程运行过程中,访问页面成功(页面在内存中)次数为S,访问页面缺页(页面需要从外存调入)次数为F,则缺页率为:

f = FS+F

页面置换算法

进程运行时,其需要的页面不在内存而需要将其调入内存时,如果内存已经没有空闲空间,则需要从内存中调出一页数据或程序,选择将哪一页数据调出则将由页面置换算法(Page-Replacement Algorithms)决定。

最佳(Optimal)置换算法

最佳置换算法(OPT,Optimal):置换时淘汰的页面选择以后永远不用,或者在最长时间内不再被访问的页面。该算法可以保证最低缺页率,但实际上,进程无法得知哪一页是未来最长时间内不再被访问的,因此该算法是无法实现的,通常用作评价其他算法的标尺。

例:假设系统为某进程分配了三个内存块,现提前得知进程会按照以下顺序访问页面: 7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1 页面调入流程为: 1.进程运行时,先将7,0,1页面装入内存 2.随后进程需要访问页面2,产生缺页中断,当前在内存中的三个页面,页面0将作为第5个访问,页面1是第14个被访问页面,页面7是第18个被访问的页面,因此淘汰页面7,置换入页面2 3.进程访问页面0,该页面已在内存 4.进程访问页面3,产生缺页中断,当前在内存的页面201,淘汰页面1,置换入页面3 5.以此类推
loading
整个过程缺页中断发生了9次,页面置换发生了6次,缺页时未必发生置换,前三次缺页,因为内存有空闲块,不需要进行页面置换
先进先出(FIFO)置换算法

先进先出置换算法(FIFO):每次选择淘汰的页面是最早进入内存的页面。该算法实现简单,只需要把调入内存的页面根据调入的先后顺序排成一个队列,需要换出页面时选择队头页面即可。

但最早进入内存的页面并不一定是适合淘汰的页面,它可能在之后依旧需要频繁访问,因此FIFO算法性能差,且会产生Belady异常,上下文介绍的算法中,只有FIFO算法会产生该异常

例:假设系统为某进程分配了三个内存块,并考虑到有以下页面号引用串: 3, 2, 1, 0, 3, 2, 4, 3, 2, 1, 0, 4
loading
给进程分配四个内存块,且页面引用不变
loading
分配了3个内存块时,缺页次数为9次;分配了4个内存块时,缺页次数反而增加到了10次 Belady异常:当为进程分配的物理块数数量大时,缺页次数不减反增的异常现象
最近最久未使用(LRU)置换算法

最近最久未使用LRU(Least Recently Used)置换算法:每次淘汰的页面是最近最久未使用的页面,实现方法是在每个页面的页表项中,用访问字段记录该页面自上次被访问以来所经历的时间t。当需要淘汰一个页面时,选择现有页面中t值最大的页面,即最近最久未使用的页面。

该算法性能好,但开销大,需要专门的硬件支持,为记录了各页面有多长时间未被访问,以及快速找出哪个页面是最久未使用的页面,通常使用寄存器或栈来记录最后一个被访问的页面,如:使用栈,把最近访问页面移到栈顶,淘汰栈底

例:假设系统为某进程分配了四个内存块,并考虑到有以下页面号引用串: 1, 8, 1,7,8, 2,7, 2, 1,8, 3, 8, 2, 1, 3,1,7,1, 3,7
loading
最少使用(LFU)置换算法

最少使用LFU(Least Frequently Used)置换算法:每次淘汰的页面是近期最少访问的页面,该算法同样需要特殊硬件支持,需要在内存中为每个页面设置一个移位寄存器,记录该页面被访问的频率。由于内存有较高访问速度,如:1ms访问某页面成千上万次,而记录次数只能做到一定时间间隔记录一次,因此该算法并没有办法真正反映页面使用情况

时钟(CLOCK)置换算法

LRU算法性能好,是最接近OPT算法性能的,但是实现起来需要专门的硬件支持,算法开销大,因此大多采用LRU的近似算法,其中时钟置换算法就是一种性能和开销较均衡的算法。

时钟(CLOCK)置换算法又称最近未用(NRU)算法(Not Recently Used),其实现方法是为每个页面设置一个访问位,再将内存中的页面都通过链接指针链接成一个循环队列。当某页被访问时,其访问位置为1。置换算法在选择淘汰一个页面时,只需检查页的访问位。如果是0,就选择该页换出;如果是1,则将它置为0,暂不换出,继续检查下一个页面,若第一轮扫描中所有页面都是1,则将这些页面的访问位依次置为0后,再返回队首进行第二轮扫描(第二轮扫描中一定会有访问位为0的页面,因此简单的CLOCK算法选择一个淘汰页面最多会经过两轮扫描)

改进型CLOCK算法

将一个页面换出时,如果该页已被修改过,需要将其写回磁盘,置换代价太大,因此置换一个未被修改过的页将有更好的性能。简单的时钟置换算法仅考虑到一个页面最近是否被访问过,改进型CLOCK算法除了考虑页面最近使用情况,还考虑置换代价。

改进型CLOCK算法淘汰页面时,同时参考访问位A修改位M

  • (A=0,M=0):表示该页最近既未被访问,又未被修改,是最佳淘汰页
  • (A=0,M=1):表示该页最近未被访问,但已被修改,不是很好的淘汰页
  • (A=1,M=0):表示该页最近被访问过,但未被修改,可能会被再次访问
  • (A=1,M=1):表示该页最近被访问过,且已被修改,可能再次被访问

进行页面置换时,按照以下步骤执行:

  • 从指针所指当前位置开始,扫描循环队列,寻找(A=0,M=0)的页面,将所遇到的第一个页面选中为淘汰页,第一轮扫描不修改访问位A
  • 如果第一轮扫描未找到(A=0,M=0)的页面,则开始第二轮扫描,寻找(A=0,M=1)的页面,将所遇到的第一个页面选中为淘汰页,且本轮扫描将所有扫过页面的访问位A置0
  • 如果第二轮扫描也失败,则将指针返回开始位置,并将所有访问位置0。然后重复第一步,如果仍然失败(即找不到A=0,M=0的页面),则重复第二步,此时必能找到(A=0,M=1)的页面

该算法可以减少磁盘I/O操作,但可能需要多轮扫描,选择一个淘汰页面最多可能进行四轮扫描

页面缓冲算法PBA

FIFO、LRU、CLOCK等算法主要解决当需要页面置换时,应选择哪个页面淘汰,在实际系统中,页面换入换出的处理过程也会影响性能,尤其是被修改过的页面(脏页,Dirty Page)需要写回磁盘时。

页面缓冲算法PBA(Page Buffering Algorithm)并不改变页面置换算法选择淘汰页的方式,而是在页面被淘汰后的处理阶段进行优化,其核心思想是将被淘汰的页面暂时保存在页面缓冲区中,不立即执行磁盘写回,而是在后台积累一定数量(如:64页)后统一写回,从而减少I/O次数,提高系统吞吐量。具体实现时,PBA维护:

  • 空闲页面链表(Free Page List):由系统内核管理的空闲物理块,当有一个未被修改的页面要换出时,由于不需要将其写回磁盘,因此直接挂在空闲链表末尾即可,以作为可直接覆盖的空闲页。而当某进程重新需要读入该页时,也可直接从空闲链表中取下,免除磁盘读入操作。
  • 修改页面链表(Modified Page List):该链表由已修改的页面形成,当进程需要将一个已修改的页面换出时,系统并不立即将其换出到外存,而是挂在修改页面链表末尾,等待链表到达一定长度后再统一写入外存。该方法并不会立即释放物理块空间(系统内存告急时也可以立即释放),而是先释放逻辑上的占用,再异步处理I/O,一方面减少缺页处理阻塞时间,一方面减少了I/O读写频率。

抖动与工作集

抖动

刚刚换出的页面马上又要换入内存,刚刚换入的页面马上又要换出外存,这种频繁的页面调度行为称为抖动,或颠簸。进程抖动会导致系统需要频繁在内存与外存之间换入换出,导致 CPU 大部分时间等待 I/O,实际有效运算时间极低,系统吞吐量急剧下降。产生抖动的主要原因是分配给进程的物理块数 < 进程当前所需的最小页面数(工作集),或者说进程频繁访问的页面数目高于可用的物理块数

工作集与驻留集
  • 驻留集:指给进程分配的内存块的集合,它反映进程“目前拥有”多少内存
  • 工作集:指在某段时间间隔Δ内,进程实际访问页面的集合,记作W(t, Δ),它反映进程当前“真正需要”多少内存
  • 若驻留集 < 工作集 ,频繁缺页可能引发抖动
  • 若驻留集 ≫ 工作集,则浪费内存
抖动的预防方法

在多道程序环境下,为保证系统有较大吞吐量,必须防止抖动的发生,以下是几种常用预防抖动的方法:

  • 采用局部置换策略:当进程发生缺页时,只能在分配给自己的内存空间内置换,不允许从其他进程获取物理块。这样,即便进程发生了抖动,也不会影响其他进程
  • 把工作集算法融入到处理机调度中,系统持续监测每个进程的工作集,保证其驻留集大小 ≥ 工作集大小
  • 根据L=S准则调节多道程序度,其中L是缺页间之间的平均时间,S是置换一个页面所需时间。如果L远比S大,说明很少发生缺页,磁盘能力尚未充分得到利用;如果L比S小,则说明频繁发生缺页,缺页速度超过了磁盘处理能力;当L和S接近时,磁盘和处理机都可以达到最大利用率
  • 挂起部分进程:当检测到抖动时,选择若干进程全部挂起,将其页写回外存释放内存,将释放的块分给其他进程,待系统恢复再唤醒

请求分段存储管理方式

请求分段式虚拟存储器系统是在分段基础上建立的,将分段存储管理与虚拟存储技术结合的内存管理系统,它以分段为单位进行换入、换出。其实现原理以及所需要的硬件支持与请求分页系统十分相似的。其核心思想是:将程序按照逻辑功能划分为多个段,程序运行时不必一次将所有段装入内存,而是只装入当前需要访问的段,其余段保存在外存中。当访问未装入内存的段时,产生缺段中断,由系统将该段调入内存

请求分段的硬件支持

请求分段系统需要增加以下硬件和数据结构

段表
loading

在段表项中,除了段名(号)、段长、段在内存中的起始地址(段基址)外,还增加了以下字段:

  • 存取方式:用于对段实施保护,可以是只执行、只读和允许读/写。
  • 访问字段A:用于记录该段被访问的频繁程度。提供给置换算法选择换出页面时参考
  • 修改位M:用于表示该页在进入内存后是否已被修改过,供置换页面时参考
  • 存在位P:用于指示本段是否已调入内存,供程序访问时参考。
  • 增补位:用于表示本段在运行过程中是否做过动态增长。
  • 外存始址:指示本段在外存中的起始地址,即起始盘块号。
缺段中断

访问某段时,当发现进程所要访问的段尚未调入内存,便产生缺段中断信号,工作流程为:

  • CPU根据逻辑地址得到段号和段内地址
  • 查询段表
  • 若存在位为1,说明该段已在内存,完成地址转换
  • 若存在位为0,产生缺段中断,与缺页中断机构类似,缺段中断机构同样在执行执行期间产生和处理中断,一条指令执行期间可能产生多次中断
  • 操作系统将对应段从外存调入内存
  • 更新段表
  • 重新执行指令
地址变换机构

系统需将逻辑地址 (段号, 段内偏移) 变换为物理地址:

  • 从段表寄存器找到段表起始地址。
  • 用段号 S 索引段表,找到对应段表项。
  • 地址越界检查:若 段内偏移 W ≥ 段长,则产生越界中断。
  • 保护检查:检查当前操作是否与存取方式字段匹配。
  • 存在检查:若存在位 P = 0,产生缺段中断,等待调入。
  • 若在内存,计算物理地址:物理地址 = 段基址 + 段内偏移 W

分段的共享

为了实现分段共享,可在系统中配置一张共享段表,所有共享段都在共享段表中占有一个表项。表项记录了共享段的段号、段长、内存始址、状态(存在)位、外存始址以及共享计数等信息,关键字段为:

  • 共享进程计数count:记录有多少进程正在共享该分段,当某进程不再需要而释放它时,系统不立即回收该段所占内存区,而是检查count是否为0,若不是0,则表示还有进程需要它;当count为0,才由系统回收该段所占内存区。
  • 存取控制字段:对于一个共享段,应为不同的进程赋予不同的存取权限。例如,对于文件所有者,通常允许他读和写:而对其它进程,则可能只允许读,甚至只允许执行。
  • 段号:对于一个共享段,在不同的进程中可以具有不同的段号,每个进程可用自己进程的段号去访问该共享段

分段的保护

在分段系统中,由于每个分段在逻辑上是相对独立的,因此可以方便实现保护,分段保护主要有三种措施:

  • 越界检查:地址变换时,检查段内偏移是否超过段长
  • 存取控制检查:设置段的读、写、执行权限,检查操作的合法性。
  • 环保护机构:把程序按访问权限分层,OS内核处于0号环内,重要的程序和系统服务位于居中环,普通应用程序位于外环。低特权级环不能随意访问高特权级环的数据或调用其程序:
    • 一个程序可以访问驻留在相同环或较低特权环(外环)中的数据;
    • 一个程序可以调用驻留在相同环或较高特权环(内环)中的服务。
      loading

请求分段的局限性

请求分段虽然符合程序逻辑,但存在:段大小不固定,容易产生外部碎片;段置换需要处理较大连续空间;硬件实现复杂等问题,因此现代操作系统通常采用分页负责虚拟内存管理,分段主要用于逻辑保护和权限控制

内存映射文件mmap

传统文件读写机制

传统文件访问方式中,文件存储在磁盘等外存设备中,进程不能直接访问文件内容,必须通过系统调用完成数据传输,以读操作为例,该过程需要经历:

  • 文件位于磁盘,用户进程调用系统调用read(),随后OS检查所访问数据是否已经被缓存到内核缓冲区的页缓存(Page Cache)中
    • 如果缓存命中,则不需要从磁盘中读取
    • 如果Page Cache未命中,CPU从用户态切换到内核态,通过文件系统找到文件数据所在磁盘块,将文件读入内核缓冲区(页缓存 Page Cache)
  • 随后OS将数据从内核空间复制到用户空间缓冲区
  • CPU从内核态切换回用户态,返回用户进程继续执行(该切换是在进程内部进行的在内核态和用户态之间的切换,并非进程切换)

传统的文件读写机制在缓存未命中的情况下,主要开销包括:两次用户态/内核态之间的切换,两次数据搬移(从磁盘到内核缓冲区,从内核缓冲区到用户进程空间),对于大量随机访问文件,频繁调用 read()/write() 效率较低

内存映射文件

内存映射文件mmap(Memory-Mapped Files)的核心思想是将文件映射到进程虚拟地址空间,使文件数据可以像访问内存一样访问。其本质是利用虚拟存储和请求分页机制,将文件作为虚拟页按需调入内存。相比传统read/write,mmap减少数据复制,提高随机访问效率,并广泛应用于大文件处理、共享内存、动态库加载和程序装载

内存映射文件的核心系统调用是mmap(),核心流程包括:

  • 执行系统调用mmap(),随后磁盘文件会被映射到进程虚拟地址空间,操作系统会在进程页表中建立映射关系,虚拟地址对应文件中的某些磁盘块,此时文件内容并没有立即全部加载到内存
  • 当进程需要访问某个文件时,实际访问的是其某个数据块的虚拟地址,随后MMU查询页表,发现对应页不在内存,便产生缺页异常
  • OS执行缺页中断,从文件读取对应页,建立页表映射
  • OS重新执行指令,即可从内存读取文件

与传统read/write相比,mmap通过文件映射方式,可以有效减少数据搬移,且能按页调入数据,支持映射大文件,支持多个进程映射同一个文件,效率较高,因此被广泛用于大文件随机访问,进程间共享内存通信,动态链接库加载。但mmap并不能完全替代 read/write,它有以下缺点:

  • mmap建立映射需要系统调用,有初始化开销
  • 大量随机访问可能产生大量缺页异常
  • 小文件顺序读取时,read()可能更简单且性能足够
上一篇:操作系统(下)
下一篇:操作系统(上)
z z z z z