虚拟存储器][十分核心]

Posted zy691357966

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了虚拟存储器][十分核心]相关的知识,希望对你有一定的参考价值。

9.虚拟存储器

为了更加有效地管理存储器且少出错,现代系统提供了对主存的抽象概念,叫做虚拟存储器(VM)

  • 虚拟存储器是硬件异常,硬件地址翻译,主存,磁盘文件和内核软件的完美交互。
  • 为每个进程提供一个大的一致的私有的地址空间。
  • 提供了3个重要能力。

    • 将主存看成磁盘地址空间的高速缓存
      • 只保留了活动区域,并根据需要在磁盘和主存间来回传送数据,高效使用主存。
    • 为每个进程提供一致的地址空间

      • 简化存储器管理
    • 保护了每个进程的地址空间不被其他进程破坏。

  • 程序员为什么要理解它?

    • 虚拟存储器是中心的。
      • 遍布在计算机系统所有层次,硬件异常,汇编器,连接器,加载器,共享对象,文件和进程中扮演重要角色。
    • 虚拟存储器是强大的。

      • 可以创建和销毁存储器片(chunk)
      • 将存储器片映射到磁盘文件的某个部分。
      • 其他进程共享存储器。
      • 例子
        • 能读写存储器位置来修改磁盘文件内容。
        • 加载文件到存储器不需要显式的拷贝。
    • 虚拟存储器是危险的

      • 引用变量,间接引用指正,调用malloc动态分配程序,就会和虚拟存储器交互。
      • 如果使用不当,将遇到复杂危险的与存储器有关的错误。
      • 例子
        • 一个带有错误指针的程序可以立即崩溃于段错误或者保护错误
        • 运行完成,却不产生正确结果。
  • 本章从两个角度分析。

    • 虚拟存储器如何工作。
    • 应用程序如何使用和管理虚拟存储器

9.1 物理与虚拟寻址

  • 物理地址(Physical Address,PA):计算机系统的主存被组织为M个连续的字节大小的单元组成的数组。每个字节的地址叫物理地址.

  • CPU访问存储器的最自然的方式使用物理地址,这种方式称为物理寻址
    - 早期的PC,数字信号处理器,嵌入式微控制器以及Cray超级计算机使用物理寻址

  • 现代处理器使用的是虚拟寻址(virtual addressing)的寻址形式。

    技术分享

    • CPU通过生成一个虚拟地址(Virtual address,VA)来访问主存。

      • 虚拟地址转换为物理地址叫做地址翻译(address translation)
    • 地址翻译也需要CPU硬件和操作系统之间的紧密结合。

      • CPU芯片上有叫做存储器管理单元(Memory Management Unit,MMU)的专用硬件。
        • 利用存储在主存中的查询表来动态翻译虚拟地址。
        • 查询表由操作系统管理。

9.2 地址空间

地址空间(address space)是一个非负整数地址的有序集合。

  • 如果地址空间中整数是连续的,我们说它是线性地址空间(linear address space)

    • 为了简化讨论,我们总是假设使用线性地址空间。
  • 在一个带虚拟存储器的系统中,CPU从一个有N=2^n个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间(virtual address space)

  • 一个地址空间大小是由表示最大地址所需要的位数来描述的。

    • N=2^n个地址的虚拟地址空间叫做n位地址空间。
    • 现在操作系统支持32位64位
  • 一个系统还有物理地址空间,它与系统中物理存储器的M=2^m(假设为2的幂)个字节相对应。

地址空间的概念很重要,因为它区分了数据对象(字节)和 它们的属性(地址)

  • 每个字节(数据对象)一般有多个 独立的地址(属性)。每个地址都选自不同的地址空间。
    • 比如一般来说。
      • 字节 有一个在虚拟地址空间虚拟地址
      • 还有一个在物理地址空间物理地址
      • 两个地址都能访问到这个字节
    • 类似现实世界的门牌号, 和经纬度

9.3 虚拟存储器作为缓存的工具

感悟

在讲述这一小章之前,必须交代一下我对虚拟存储器概念的存疑
原本我以为虚拟存储器=虚拟内存
以下是虚拟内存的定义

虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换

而在下面的定义我们可以看到CSAPP中认为虚拟存储器是存放在磁盘上的。
在此,我们姑且当做两者是不同的东西,以后有更深刻的理解,再思考。

虚拟存储器(VM)被组织为一个存放在磁盘上的N个连续字节大小的单元组成的数组。

  • 每个字节都有一个唯一的虚拟地址,这个虚拟地址作为到数组的索引。
  • 磁盘上数组的内容被缓存到主存中。

    • 同存储器层次结构其他缓存一样,磁盘上的数据被分割称
      • 这些作为磁盘和主存之间的传输单元。
      • 虚拟页(Virtual Page,VP)就是这个
        • 每个虚拟页大小为P=2^p字节。
    • 物理存储器被分割为物理页,大小也为P字节
      • 也被称为页帧(page frame)
  • 任何时候,虚拟页的集合都被分为3个不相交的子集

    • 未分配的:VM系统还未分配(或者创建)的页。未分配的没有任何数据与之相关联。
      • 不占用磁盘空间
      • 通过malloc来分配
    • 缓存的:当前缓存在物理存储器的已分配页。
    • 未缓存的:没有缓存在物理页面存储器中的已分配页。

    技术分享

9.3.1 DRAM缓存的组织结构

DRAM表示虚拟存储器系统的缓存,在主存中缓存虚拟页,有两个特点。

  • DRAM缓存不命中处罚十分严重。
    • 因为磁盘DRAM慢100000多倍。
  • 访问一字节开销
    • :从一个磁盘的一个扇区读取第一个字节的时间开销要比从该扇区中读连续的字节慢大约100000倍

DRAM缓存的组织结构由这种巨大的不命中开销驱动。因此有以下特点。
(有些地方不是特别懂,之后看完第六章应该会好点)

  • 虚拟页往往很大。

    • 4KB~2MB
    • 访问一字节开销的原因才要这么大。
  • DRAM缓存是全相联

    • 也就是: 任何虚拟页都能放在任何物理页中。
    • 原因在于大的不命中惩罚
  • 更精密的替换算法

    • 替换错了虚拟页的惩罚很高。
  • DRAM缓存总是写回

    • 因为对磁盘的访问时间很长
    • 而不用直写

9.3.2 页表

判断命中替换又多种软硬件联合提供。

  • 操作系统软件,MMU中的地址翻译硬件和页表(page table)

    • 页表是存放在物理存储器的数据结构。

      • 页表将虚拟页映射到物理页。
      • 地址翻译硬件将虚拟地址转换为物理地址都会读取页表
    • 操作系统负责维护页表的内容,以及磁盘及DRAM之间来回传送页。

技术分享

  • 页表就是一个页表条目(Page Table Entry,PTE)的数组.
    • 虚拟地址空间 中每个页在页表的固定偏移量处都有一个PTE.
    • 每个PTE由一个有效位n位地址字段
      • 有效位表明虚拟页是否被缓存。
        • 如果有效位存在,那么地址字段指向对应的物理存储器。
        • 如果有效位不存在。
          • 地址字段要么为NULL
          • 要么指向虚拟页在磁盘所在的位置。

9.3.3 页命中

技术分享

  • 一个页命中的过程
  • 一个虚拟地址转换为物理地址的过程。

9.3.4 缺页

在虚拟存储器的习惯说法中,DRAM缓存不命中称为缺页

处理过程如下:

  • 读取虚拟地址所指向的PT
  • 读取PTE有效位,发现未被缓存,触发缺页异常
  • 调用缺页异常处理程序

    • 选择牺牲页。
    • 如果牺牲页发生了改变,将其拷贝回磁盘(因为是写回)
    • 需要读取的页代替了牺牲页的位置。
    • 结果:牺牲也不被缓存,需要读取的页被缓存。
  • 中断结束,重新执行最开始的指令。

  • DRAM中读取成功。

虚拟存储器是20世纪60年代发明的,因此即使与SRAM缓存使用了不同的术语。

  • 被称为
  • 磁盘DRAM之间传送的活动叫做交换(swapping)或者页面调度(paging)
  • 不命中发生时,才换入页面,这种策略叫做按需页面调度(demand paging)
    • 现代系统基本都是用这个。

9.3.5 分配页面

比如某个页面所指向地址为NULL,将这个地址指向磁盘某处,那么这就叫分配页面

此时虚拟页未分配状态 变为 未缓存

9.3.6 又是局部性拯救了我们

虚拟存储器工作的相当好,主要归功于老朋友局部性(locality)

尽管从头到尾的活动页面数量大于物理存储器大小。

但是在局部内,程序往往在一个较小的活动页面集合工作

  • 这个集合叫做工作集(working set)或者叫常驻集(resident set)

    • 初始载入开销比较大。
  • 程序有良好的时间局部性虚拟存储器都工作的相当好。

  • 如果程序实在很烂,或者物理空间很小,工作集大于物理存储器大小。这种状态叫颠簸(thrashing).
    • 这时,页面不断换进换出。性能十分低。

统计缺页次数
可以利用Unix的getrusage函数检测缺页数量。

9.4 虚拟存储器作为存储器的管理工具

实际上,操作系统为每个进程提供一个独立的页表

技术分享

因此,VM简化了链接加载,代码数据共享,以及应用程序的存储器分配。

  • 简化链接

    • 独立的空间地址意味着每个进程的存储器映像使用相同的格式。

      • 文本节总是从0x08048000(32位)处或0x400000(64位)处开始。
      • 然后是数据,bss节,栈。
    • 一致性极大简化了链接器的设计和实现。

  • 简化加载

    • 加载器可以从不实际拷贝任何数据从磁盘到存储器。
    • 基本都是虚拟存储系统完成。

    将一组连续的虚拟页映射到任意一个文件中的任意位置的表示法称作存储器映射。Unix提供一个称为mmap的系统调用,允许程序自己做存储器映射。在9.8详细讲解。

  • 简化共享

    • 独立地址空间为操作系统提供了一个管理用户进程和操作系统自身之间的一致共享机制.
    • 例子
      • 操作相同的操作系统内核代码
      • C标准库的printf.
    • 因此操作系统需要将不同进程的适当的虚拟页映射到相同的物理页面。
      • 多个进程共享这部分代码的一个拷贝。
      • 而不是每个进程都要加载单独的内核和C标准库的拷贝。
  • 简化存储器分配.

    • 虚拟页连续(虚拟页还是单独的),物理页可以不连续。使得分配更加容易。
  • 9.5 虚拟存储器作为存储器保护的工具

    任何现代操作系统必须为操作系统提供手段来控制存储器系统的访问。

    • 不应该允许用户进程修改它的只读文本段。
    • 不允许它读或修改任何内核的代码和数据结构
    • 不允许读写其他进程的私有存储器。
    • 不允许修改共享的虚拟页,除非所有共享者显示允许这么做(通过调用明确的进程间通信

    方式:在PTE上添加一些格外的许可位来控制访问。

    技术分享

    • SUP:是否只有在内核模式下才能访问?
    • READ:读权限。
    • WRITE:写权限。

    如果指令违反了许可条件,触发一般保护性异常,然后交给异常处理程序Shell一般会报告为段错误(segmentaion fault)

    9.6 地址翻译

    认识到硬件在支持虚拟存储器中的角色

    以下是接下来可能要用到的符号,作参考。

    技术分享

    • 形式上来说,地址翻译是一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS)元素之间的映射,

      技术分享

    • 以下展示了MMU(Memory Management Unit,存储器管理单元)如何利用页表实现这样的功能

      技术分享

      • 页表基址寄存器(Page Table Base Register,PTBR)指向当前页表。
      • n位的虚拟地址包含两个部分

        • 一个p位的虚拟页面偏移(Virtual Page Offset,VPO)
        • 一个n-p位的虚拟页号(Virtual Page Number,VPN)
          • MMU利用VPN选取适当的PTE(页面条目,Page Tabe Entry,PTE)
      • 页面条目 (PTE)中物理页号(PPN)和虚拟地址中的VPO串联起啦,即是物理地址

        • PPOVPO是相同的
        • 不要忘记VPN,PPN都是块,都是首地址而已,所以需要偏移地址PPO,VPO

    技术分享

    图(a)展示页面命中,CPU硬件执行过程

    • 第一步:处理器生成虚拟地址,把它传送给MMU
    • 第二步: MMU生成PTE地址(PTEA),并从高速缓存/主存请求中得到它。
    • 第三步: 高速缓存/主存向MMU返回PTE
    • 第四步: MMU构造物理地址(PA),并把它传送给高速缓存/主存
    • 第五步: 高速缓存/主存返回所请求的数据字给处理器。

    页面命中完全由硬件处理,与之不同的是,处理缺页需要 硬件和操作系统内核协作完成。

    • 第一到三步: 与命中时的一样
    • 第四步:PTE有效位是零,所以MMU触发异常,传递CPU中的控制到操作系统内核中的 缺页异常处理程序
    • 第五步:缺页异常处理程序确定出物理存储页中的牺牲页,如果这个页面已经被修改,则把它换出到磁盘。
    • 第六步:缺页异常处理程序调入新的页面,并更新存储器中的PTE
    • 第七部:缺页异常处理程序返回到原来的进程,再次执行导致缺页的指令,之后就是页面命中一样的步骤。

    9.6.1 结合高速缓存和虚拟存储器(PA->内存)

    在任何使用虚拟存储器又使用SRAM高速缓存的系统中,都存在应该使用虚拟地址 还是 使用 物理地址 来访问SRAM高速缓存的问题。

    使用虚拟地址的优点,就是类似于使用虚拟存储器的优点,更好的利用空间。但是设计更复杂。两者的使用需要权衡。

    大多数系统是选择物理寻址

    • 使用物理寻址,多个进程同时在高速缓存中有存储块和共享来自相同虚拟页面的块称为简单的事。

      • 而且还无需处理保护问题,因为 访问权限的检查在地址翻译中(PTE)的一部分。
    • 以下是一个例子(将PTE进行高速缓存)。

    技术分享

    9.6.2 利用TLB加速地址翻译(VA->PA)

     
    每次CPU产生一个虚拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为 物理地址

    • 在最糟糕的情况下,会从内存中取数据,代价是几十几百个周期
    • 如果PTE碰巧缓存在L1中,那么开销就下降到一到两个周期

    许多系统都试图消除这样的开销,他们在MMU中包含了一个关于PTE的小缓存,称为翻译后备缓冲器(Translation Lookaside Buffer,TLB)

    • TLB是一个小的,虚拟寻址的缓存

      • 每一行都保存着一个由单个PTE组成的块。
      • TLB通常用于高度的相连性
      • 如图所示

        技术分享

        • 用于组选择和行匹配的索引标记字段是从虚拟地址中的虚拟页号中提取出来的。
        • 如果TLB有T=2^t个组
          • 那么TLB索引(TLBI)是由VPN的t个最低位组成。(对应于VPO)
          • TLB标记(TLBT)是由VPN中剩余位组成(对应于VPN)
    • 下图展示了TLB命中步骤

      • 关键点:所有的地址翻译步骤都是在芯片上的MMU中执行的,因此非常快

      技术分享

      • TLB命中

        • 第一步:CPU产生虚拟地址。
        • 第二步和第三部:MMUTLB取出对应的PTE
        • 第四步:MMU将这个虚拟地址翻译成一个物理地址,发送到高速缓存/主存
        • 第五步:高速缓存/主存所请求的数据字返回给CPU
      • TLB不命中的时候,MMU必须从L1缓存或内存中取出相应的PTE,并进行类似缺页处理过程

    9.6.3 多级页表

    如果我们有一个32位地址空间,4KB大小的页面(p=2^12)和一个4BPTE,即使应用所引用的只是虚拟地址空间中很小的一部分,也总是需要一个4MB的页表驻留在存储器中。

    所以多级页表的诞生用于解决在很少使用时有一个很大的页表常驻于内存

    计算方式,最多可能要2^32/4KB=1MB 个页面,每个页面需要4B的PTE 所以需要4MB大小的页表。
    思考虚拟地址是31~p,p-1~0即VPN,VPO。
    VPN即可表示页面个数(上文中的1MB),VPO即页面大小(上文中的4KB),显然知道两者相乘为2^32 次方、

    用来压缩页表的常用方式是使用层次结构的页表。

    页表本身一个优点就是用来解决 内存不够装载程序所用内存的情况,进行动态分配。那么当我们发现内存装载那么大的页表也是负担的时候,显然也可以用类似页表的形式来解决,这就是多级页表。

    技术分享

    以下用上图的 两层 作为例子。

    • 总共有9KB个页面,PTE为4个字节。

      • 2KB个页面分配给代码和数据。
      • 接下来6KB个页面未分配
      • 再接下来1023个页面也未分配
      • 接下一个页面分配给用户栈
    • 一级页表中的每个PTE负责映射虚拟地址空间中一个4MB大小的片(chunk).

      • 每一个都是由1024个连续的页面组成。
      • 4MB=1024个页面*PTE大小4字节
    • 如果片i中每个页面都没有分配,那么一级PTE i就为空。

      • 例如图中的PTE 2~PTE 7
      • 但是如果片i中有一个被分配了,那么PTE i就不能为空。
        • 是不是觉得这样很浪费啊~所以说,三级四级页表的原由也是如此。
        • 而且后文会发现,页表级数即使很大,复杂度也不会怎么变化。
    • 这种方法从两个方面减少了存储器要求。

      • 如果一级页表PTE为空,那么相应的二级页表就根本不会存在。
        • 一种巨大的潜在节约,大部分时候内存都是未分配的。
      • 只有一级页表才需要总是在主存中。
        • 虚拟存储器系统可以在需要时创建,页面调入,调出二级页面,减少主存压力。

      技术分享

    k级页表层次结构的地址翻译。

    • 虚拟地址被分为kVPN和一个VPO。每个VPN i都是i-1级页表到i级页表的索引。
    • PPN存于k级页表。
    • PPO依旧与VPO相同。

    此时TLB能发挥作用,因为层次更细,更利于缓存。使得多级页表的地址翻译不比单级页表慢很多。

    9.6.4 综合:端到端的地址翻译

    在这一节里,我们通过一个具体的端到端的地址翻译示例,来综合一下我们学过的内容。

    一个在有一个TLBL1 d-cache的小系统上。作出如下假设:

    • 存储器都是按字节寻址的。(?)
    • 存储器访问是针对一字节的字的。(?)
    • 虚拟地址14位长(n=14)
    • 物理地址12位长(m=12)
    • 页面大小64字节(P=2^6)
    • TLB四路组相连的,总共有16个条目(?)
    • L1 d-cache是物理寻址,高速缓存,直接映射(E=1)的,行大小为4字节,而总共有16个组。(?)

    存储结构快照

    技术分享

    技术分享

    技术分享

    • TLB: TLB利用VPN的位进行缓存。
    • 页表: 这个页表是一个单级设计。一个有256个,但是这里只列出16个。
    • 高速缓存:直接映射的缓存通过物理地址的字段来寻址。
      • 因为是直接映射,通过索引就能直接找到。且E=1
      • 直接能判定是否命中。

    9.7 案例研究: Intel Core i7/Linux 存储器系统

    技术分享

    • 处理器包(processor package)
      • 四个核
        • 层次结构的TLB
          • 虚拟寻址
          • 四路组相连
          • Linux 一页4kb
        • 层次结构的数据和指令高速缓存。
          • 物理寻址
          • L1L2 八路组相连
          • L3 十六路组相连
          • 大小64字节。
        • 快速的点到点链接。
          • 基于Intel QuickPath技术。
          • 为了让核与其他核和外部I/O桥直接通信。
      • L3高速缓存
      • DDR3存储器控制器

    9.7.1 Core i7地址翻译

    技术分享

    上图完整总结了Core i7地址翻译过程,从虚拟地址到找到数据传入CPU。

    • Core i7采用四级页表层次结构。
      • CR3 控制寄存器指向第一级页表(L1)的起始位置
        • CR3也是每个进程上下文的一部分。
        • 上下文切换的时候,CR3也要被重置。

    一级,二级,三级页表PTE的格式:
    技术分享

    • P=1时 地址字段包含了一个40位物理页号(PPN),指向适当的页表开始处。

    • 强加了一个要求,要求物理页4kb对齐。

      • 因为 PPO12位 = 4kb
      • PPO的大小就跟物理页的大小有关。

    四级页表的PTE格式:

    技术分享

    • PTE有三个权限位,控制对页的访问

      • R/W位确定页的内容是可以 读写还是 只读
      • U/S位确定用户模式是否能够访问,从而保护操作系统内核代码不被用户程序访问。
      • XD (禁止执行) 位是在64位系统引入,禁止某些存储器页取指令。
        • 这是一个重要的新特性,限制只能执行只读文本段,降低缓冲区溢出的风险。
    • MMU翻译虚拟地址时,还会更新两个内核缺页处理程序会用到的位。

      • A

        • 每次访问一个页,MMU都会设置A位,称为引用位(reference bit).
        • 可以利用这个引用位来实现它的页替换算法。
      • D

        • 每次对一个页进行了 就会设置D位,又称脏位(dirty bit).
        • 脏位告诉内核在拷贝替换页前是否要写回
      • 内核通过调用一条特殊的内核模式指令来清除引用位脏位

    四级页表如何将VPN 翻译成物理地址
    技术分享

    • 每个VPN被用作页表的偏移量。
    • CR3寄存器包含L1页的物理地址

    优化地址翻译
    在对地址翻译中,我们顺序执行这两个过程

    • MMU将虚拟地址翻译成物理地址。
    • 物理地址传送到L1高速缓存。


    然而实际的硬件实现使用了一个灵巧的技巧,允许这两个步骤并行。加速了对高速缓存的访问


    例如:页面大小为4KBCore i7上的虚拟地址有12位的VPO,且PPO=VPO.

    而且物理地址的缓存,也是6位索引+6位偏移,刚好是VPO的12位。这不是巧合

    • 一方面通过VPNPPN
    • 另一方面直接通过PPO对高速缓存进行组选择。
    • 等找到VPN后就能立即进行关键字匹配。

    9.7.2 Linux 虚拟存储系统

    目标:对Linux的虚拟存储系统做一个描述,大致了解操作系统如何组织虚拟存储器,如何处理缺页

    技术分享

    内核虚拟存储器

    • 内核虚拟存储器包含内核中的代码和数据。

      • 内核虚拟存储器的某些区域被映射到所有进程共享的物理页面

        • 如:内核代码,全局数据结构。
      • Linux也将一组连续的虚拟页面(大小等同于系统DRAM总量)映射到相应的一组物理页面。(这句话啥意思???????????????????????????????)

    • 内核虚拟存储器包含每个进程不相同的数据。

      • 页表,内核在进程上下文中时使用的栈,等等。

    Linux 虚拟存储器区域

    Linux将虚拟存储器组织成一些区域(也叫做)的集合。

    • 一个区域就是已经存在着的(已分配的) 虚拟存储器的连续,这些片/页已某种形式相关联。

      • 代码段,数据段,堆,共享库段,用户栈。
      • 所有存在的虚拟页都保存在某个区域。

        • 区域的概念很重要
        • 允许虚拟地址空间有间隙。

    一个进程中虚拟存储器的内核数据结构。

    技术分享

    内核为系统中每个进程维护了一个单独的任务结构任务结构中的元素包含或指向内核运行该进程所需要的全部信息。

    • task_struct
      • mm_struct
        • 描述了虚拟存储器的当前状态。
        • pgd
          • 指向第一级页表的基址。
          • 当进程运行时,内核将pgd存放在CR3控制寄存器
        • mmap
          • vm_area_structs(区域结构)
            • 每个vm_area_structs都描述了当前虚拟地址空间的一个区域(area).
            • vm_start:指向这个区域的起始处。
            • vm_end:指向这个区域的结束处。
            • vm_port:描述这个区域内包含的所有页的读写许可权限。
            • vm_flags:描述这个区域页面是否与其他进程共享,还是私有。
              • 还有一些其他事情
            • vm_next: 指向链表的下一个区域。

    Linux 缺页异常处理

    MMU在试图翻译虚拟地址A时,触发缺页。这个异常导致控制转移到缺页处理程序,执行一下步骤。

    技术分享

    • 虚拟地址A是合法的吗?

      • A在某个区域结构定义的区域内吗?
      • 解决方法:
        • 缺页处理程序搜索区域结构链表。
        • 把A和每个区域的vm_startvm_end做比较。
          • 通过某种 树的数据结构算法查找
      • 如果不合法,触发段错误。
    • 试图访问的存储器是否合法?

      • 即是否有读,写,执行这个页面的权限?
      • 如果不合法,触发保护异常,终止进程。
    • 一切正常的话

      • 选择牺牲页,替换,重新执行指令

    9.8 存储器映射

    存储器映射: Linux通过将一个虚拟存储器区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容,这个过程叫做存储器映射

    虚拟存储器区域可以映射到以下两种类型文件。

    • Unix文件系统中的普通文件:一个区域可以映射到一个普通磁盘文件的连续部分。

      • 例如,一个可执行文件。
      • 文件区(section)被分成大小的片,每一片包含一个虚拟页面的初始化内容。
      • 仅仅是初始化虚拟页面此时还并未进入物理存储器
        • 直到CPU第一次引用这个页面。
    • 匿名文件 : 一个区域 可以映射到一个匿名文件

      • 匿名文件由内核创建,包含的全是二进制零。
      • CPU第一次引用这样区域(匿名文件)的虚拟页面时。

        • 将存储器中牺牲页面全部用二进制零覆盖。
        • 并将虚拟页面标记为驻留在存储器中。
        • 注意: 实际上,虚拟页面并没有跟存储器进行数据传送。
          • 反正是送零过去,不如我自己用零赋值,这样子更快。
      • 又叫请求二进制零的页(demand-zero page)

    交换文件交换空间。(win下叫做paging file)

    • 一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件(swap file)之间换来换去。交换文件也叫交换空间或者交换区域

    • 需要意识到,在任何时刻,交换空间都限制着当前运行着的进程分配的虚拟页面总数。

    • 这一段不太明白。

    9.8.1 再看共享对象

    共享对象的由来

    • 许多进程有同样的只读文本区域
      • printf
      • 运行Uinx shelltcsh
      • 如果每个进程都加载进内存一次,极其浪费。
    • 存储器映射提供一种机制,来共享对象

    一个对象被映射到虚拟存储器的一个区域,一定属于以下两种。

    • 共有对象
      • 一个进程将一个共有对象映射到它的虚拟地址空间的一个区域。
        • 进程对这个区域的写操作,对于那些也把这个共享对象映射它的虚拟存储器的进程可见的。
        • 这些变化也会反映到磁盘上的原始对象。
      • 映射到的虚拟存储器那个区域叫做共享区域
    • 私有对象
      • 对一个映射到私有对象的区域做出的改变,对于其他进程不可见.
      • 并且进行的写操作不会反映到磁盘上。
      • 映射到的虚拟存储器那个区域叫做私有区域

    9.8.1.1 共享对象

    技术分享

    • 进程1,将共享对象映射到虚拟存储器中,然后虚拟存储器将这一段找一块物理存储器存储。
    • 进程2也要引用同样的共享对象时。

      • 内核迅速判定,进程1已经映射了这个对象。
      • 使进程2虚拟存储器直接指向了那一块进程1指向的物理存储器
    • 即使对象被映射到多个共享区域,物理存储器依旧只有一个共享对象的拷贝。

      • 大大解决了物理存储器内存。

    9.8.1.2 私有对象

    技术分享

    私有对象使用一种叫做写时拷贝(conpy-on-write)的巧妙技术。

    • 私有对象开始生命周期的方式基本与共享对象一样。

      • 即使对象被多个引用,在物理内存都只保留一个拷贝。
    • 对于每个映射私有对象的进程,相应私有区域页表条目都被标记为只读。

      • 并且区域结构(vm_area_structs)被标记为私有的写时拷贝
    • 过程:只要有进程试图写私有区域内的某个页面,那么这个写操作触发保护异常

      • 故障处理程序会在物理存储器中创建被修改页面的一个新拷贝。
      • 更新页表条目(PTE)指向这个新的拷贝,恢复被修改页面的可写权限。
      • 故障处理程序返回,CPU重新执行这个写操作
    • 通过延迟私有对象中的拷贝直到最后可能的时刻,写时拷贝充分使用了稀缺的物理存储器。

    9.8.2 再看fork函数(私有对象的应用)

    了解fork函数如何创建一个带有自己独立虚拟地址空间的新进程。

    • fork函数被当前进程调用时。

      • 内核为新进程创建内核数据结构,并分配给它唯一一个PID
      • 为了给新进程创建虚拟存储器。
        • 创建了当前进程的mm_struct,区域结构和页表的原样拷贝。
        • 将两个进程的每个页面都标记为只读。并给两个区域进程的每个区域结构都标记为私有的写时拷贝
        • 注意:并没有对物理存储器进行拷贝哦~,利用的是私有对象写时拷贝技术。
    • fork函数在新进程返回时。

      • 新进程现在的虚拟存储器刚好和调用fork时存在的虚拟存储器相同。
      • 当两个进程中任一个需要被时,触发写时拷贝机制

    9.8.3 再看execve函数

    理解execve函数实际上如何加载和运行程序。

    • 假设运行在当前的进程中的程序执行了如下的调用:
      • Execve("a.out",NULL,NULL);
    • execve函数在当前进程加载并执行目标文件a.out中的程序,用a.out代替当前程序。

      • 加载并运行需要以下几个步骤。

        • 删除已存在的用户区域
          • 删除当前进程虚拟地址的用户部分中已存在的区域结构
        • 映射私有区域

          • 为新程序的文本,数据,bss和栈区域创建新的区域结构

            • 所有新的区域结构都是私有的写时拷贝的。
            • 文本和数据区域被映射到a.out文件中的文件和数据区。
            • bss区域是请求二进制零,映射到匿名文件。

              • 大小包含在a.out
            • 堆,栈区域也是请求二进制零

        • 映射共享区域

          • a.out程序与共享对象链接。
            • 这些对象都是动态链接到这个程序。
            • 然后映射到用户虚拟地址的共享区域。
        • 设置程序计数器(PC)

          • execve最后一件事设置PC指向文本区域的入口点。

    技术分享

    9.8.4 使用mmap函数的用户级存储器映射

    Unix进程可以使用mmap函数来创建新的虚拟存储器区域,并将对象映射到这些区域中。

    #include <unistd.h>
    #include <sys/mman.h>
    
    void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset);
    
    返回:若成功时则为指向映射区域的指正,若出错则为MAP_FAILED(-1).
    

    参数解释:

    技术分享

    fd,start,length,offset:

    mmap函数要求内核创建一个新的虚拟存储器区域,最好是从地址start开始的一个区域,并将文件描述符fd指定的对象的一个连续的片chunk映射到这个新的区域。

    • 连续对象片大小为length字节
    • 从据文件开始处偏移量为offset字节的地方开始。
    • statr地址仅仅是个暗示
      • 一般被定义为NULL,让内核自己安排。

    prot

    参数prot包含描述新映射的虚拟存储器区域的访问权限位。(对应区域结构中的vm_prot位)

    • PROT_EXEC:这个区域内的页面由可以被CPU执行的指令组成。
    • PROT_READ:这个区域内的页面可读。
    • PROT_WRITE: 这个区域内的页面可写。
    • PROT_NONE: 这个区域内的页面不能被访问。

    flag

    参数flag由描述被映射对象类型的组成。

    • MAP_ANON标记位:映射对象是一个匿名对象
    • MAP_PRIVATE标记位:被映射对象是一个私有的,写时拷贝的对象。
    • MAP_SHARED标记位:被映射对象是一个共享对象。

    例子

    bufp = mmap(NULL,size,PROT_READ,MAP_PRIVATE|MAP_ANON,0,0);

    • 让内核创建一个新的包含size字节的只读,私有,请求二进制零的虚拟存储区域。
    • 如果调用成功,那么bufp包含新区域地址。

    munmap函数删除虚拟存储器的区域:

    技术分享

    9.9 动态存储器分配

    虽然可以使用更低级的mmapmunmap函数来创建和删除虚拟存储器的区域。

    但是C程序员还是觉得用动态存储器分配器(dynamic memory allocator)更方便。

    技术分享

    • 动态存储器分配器维护着一个进程的虚拟存储区域,称为堆(heap)

      • 系统之间细节不同,但是不失通用型。
      • 假设
        • 是一个请求二进制零的区域。
        • 紧接着未初始化的bss区域,并向上生长(向更高的地址)。
        • 对于每个进程,内核维护一个变量brk(break),指向堆顶。
    • 分配器视为一组不同大小的块block的集合来维护。

      • 每个块就是一个连续的虚拟存储器,即页面大小。
      • 要么是已分配,要么是空闲
        • 已分配
          • 已分配的块显式地保留供应用程序使用。
          • 已分配的块保持已分配状态,直到它被释放
            • 这种释放要么是应用程序显示执行。
            • 要么是存储器分配器自身隐式执行(JAVA)。
        • 空闲
          • 空闲块可用于分配。
          • 空闲快保持空闲,直到显式地被应用分配。
    • 分配器有两种基本分格。

      • 都要求应用显式分配。
      • 不同之处在于那个实体负责释放已分配的块。
      • 显式分配器(explict allocator)

        • 要求应用程序显式释放
        • C语言中提供一种叫malloc程序显示分配器。

          • mallocfree
        • C++

          • newdelete
      • 隐式分配器(implicit allocator)

        • 要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。
        • 隐式分配器又叫做垃圾收集器(garbage collector).

          • 自动释放未使用的已分配的块的过程叫做垃圾收集(garbage collection).
        • Lisp,ML以及Java等依赖这种分配器。

    本节剩余的部分讨论的是显示分配器的设计与实现。

    9.9.1 malloc和free 函数

    malloc

    C标准库提供了一个称为malloc程序包的显示分配器

    #include<stdlib.h>
    void* malloc(size_t size);
                        返回:成功则为指针,失败为NULL
    
    • malloc 返回一个指针,指向大小为至少size字节的存储器块。
      • 不一定是size字节,很有可能是48倍数
        • 这个会为可能包含在这个内的任何数据对象类型做对齐
        • Unix系统用8字节对齐。
      • malloc不初始化它返回的存储器。
        • 如果想要初始化,可以用calloc函数。
          • callocmalloc一个包装函数。
      • 想要改变已分配块大小。
        • realloch函数
    • 如果malloc遇到问题。

      • 返回NULL, 并设置errno
    • 动态存储分配器,可以通过使用mmapmunmap函数,显示分配和释放堆存储器。

      • 或者可以使用sbrk函数。

        #include<unistd.h>
        
        void *sbrk(intptr_t incr);
        
                            返回:若成功则为旧的brk指针,若出错则为-1,并设置errno为ENOMEML.
        
        • sbrk函数通过将内核的brk指针增加incr(可为负)来收缩和扩展堆。

    free

    程序通过调用free函数来释放已分配的堆块。

    #include<stdlib.h>
    
    void free(void *ptr);
                    返回:无
    
    • ptr参数必须指向一个从malloc,calloc,realloc获得的已分配块的起始位置。
      • 如果不是,那么free行为未定义。
      • 更糟糕的是,free没有返回值,不知道是否错了。

    这里的字=4字节,且malloc是8字节对齐。

    技术分享

    技术分享

    9.9.2 为什么要使用动态存储器分配

    程序使用动态存储器分配的最重要原因是:

    • 经常直到程序实际运行时,它们才知道某些数据结构的大小。

    9.9.3 分配器的要求和目标

    约束

    显式分配器有如下约束条件

    • 处理任意请求序列。
    • 立即响应请求。
      • 不允许为提高性能重新排列或缓冲请求。
    • 只使用
    • 对齐
      • 上文的8字节。
    • 不修改已分配的块。

    目标

    吞吐率最大化存储器使用率最大化。这两个性能要求通常是相互冲突的。

    • 目标1:最大化吞吐率

      • 假定n个分配和释放请求的某种序列R1,R2,R3.....Rn

        • 吞吐率 :每个单位时间完成的请求数。
      • 通过使分配和释放请求平均时间最小化 来最大化吞吐率

    • 目标2:最大化存储器利用率

      • 设计优秀的分配算法。
      • 需要增加分配和释放请求的时间

      • 评估使用的效率,最有效的标准是峰值利用率(peak utilization)

        • 假定n个分配和释放请求的某种序列R1,R2,R3.....Rn

          • 有效载荷(payload):如果一个应用程序请求一个p字节的块,那么得到的已分配块有效载荷p字节。(很有可能会分配p+1个字节之类的)
          • 聚集有效载荷(aggregate payload):请求Rk完成之后,Pk表示当前已分配块的有效载荷之后。又叫做聚集有效载荷
          • Hk表示堆的当前的大小(单调非递减的)。
        • 峰值利用率为Uk

        技术分享

    • 吞吐率存储器利用率是相互牵制的,分配器设计的一个有趣的挑战就是在两者之间找到一个平衡。

    9.9.4 碎片

    造成堆利用率很低的主要原因是一种称为碎片(fragmentation)的现象。

    • 碎片:虽然有未使用的存储器但不能满足分配要求时的现象。

      • 1.内部碎片:已分配块比有效载荷(实际所需要的)大时发生。

        • 比如:上文中只要5个字(有效载荷),却给了6个字(已分配块),那一个多的就是碎片.
        • 任何时刻,内部碎片的数量取决于以前请求的模式和分配器的实现方式。
          • 可计算的,可量化的。
      • 2.外部碎片:当空闲存储器合计起来足够满足一个分配请求,但是没有一个单独的空闲块足够大可以处理这个请求发生的。

        • 外部碎片的量化十分困难。
          • 不仅取决于以前请求的模式和分配器的实现方式,还要知道将来请求的模式。
        • 优化: 需要启发式策略来用少量的大空闲块替换大量的小空闲块。

    9.9.5 实现问题

    一个实际的分配器要在吞吐率利用率把我平衡,必须考虑一下几个问题。

    • 空闲块组织: 如何记录空闲块? (对应 9.9.6)
    • 放置: 如何选择一个合适的空闲快来放置一个新分配的块? (对应 9.9.7)
    • 分割: 将一个新分配的块放入某个空闲块后,如何处理这个空闲快中的剩余部分?(对应9.9.8)
    • 合并: 我们如何处理一个刚刚被释放的块

    9.9.6 隐式空闲链表(老本行了)

    技术分享

    堆块(十分巧妙的利用了本该永远为0的低三位):

    • 一个由一个字的头部有效载荷,以及可能的填充组成。
      • 头部:编码了这个的大小(包括头部和填充),以及这个是否分配。
        • 假设是8字节对齐约束条件
          • 那么头部低三位一定是0
          • 所以释放低三位来表示一些其他信息。
            • 即块大小还是能表示0~2^32(只是必须是8的倍数),非0~2^29
            • 低三位就能表示是否分配之类的信息。

    组织为一个连续的已分配块空闲块的序列。

    技术分享

    这种结构就叫做隐式空闲链表

    • 隐式 :

      • 为什么叫隐式链表

        • 因为不是通过指针(next)来链接起来。
        • 而是通过头部的长度隐含地链接起来。
      • 终止头部(类似与普通链表的NULL)

        • 已分配,大小为的块
    • 优缺点:

      • 优点:简单
      • 缺点1:任何操作的开销都与已分配块和空闲块的总数呈线性关系O(N).
        • 放置分配的块。
        • 对空闲链表的搜索。
      • 缺点2: 即使申请一个字节,也会分配2的块。空间浪费。

    9.9.7 放置已分配的块

    当应用请求k字节的块,分配器搜索空闲链表,查找一个足够大可以放置请求的空闲块。

    有一下几种搜索放置策略

    • 首次适配
      • 从头开始搜索空闲链表,选择第一个合适的空闲块。
    • 下一次适配
      • 首次适配很类似,但不是从头开始,而是从上一次查询的地方开始。
    • 最佳适配
      • 检查每个空闲块,找一个满足条件的最小的空闲块(贪心)。

    优缺点

    • 首次适配
      • 优点
        • 往往将大的空闲块保留在链表后面。
      • 缺点
        • 小的空闲块往往在前面,增大了对较大快的搜索时间。
    • 下一次适配
      • 优点
        • 速度块。
      • 缺点
        • 存储器利用率
    • 最佳适配

      • 优点
        • 利用率高
      • 缺点

        • 要完整搜索链表,速度慢。
      • 后面有更加精细复杂的分离式空闲链表

    9.9.8 分割空闲块

    两种策略

    • 占用所有空闲块

      • 缺点:产生更多的内部碎片(但是如果内部碎片很少,可以接受)
      • 优点:能使得 空闲块+已分配块的数量减少

        • 能加快搜索速度
        • 有的外部碎片(几个字节,很有可能是外部碎片)可能根本放置不了东西,但是却占用了搜索时间,还不如当内部碎片算了
      • 放置策略趋向于产生好的匹配中使用。

        • 即占用所有空闲块,内部碎片也很少。






































































































    以上是关于虚拟存储器][十分核心]的主要内容,如果未能解决你的问题,请参考以下文章

    OpenStack都有哪些核心组件?

    部署桌面虚拟化,你家的存储准备好了吗?

    虚拟化专栏——一文看懂存储虚拟化技术

    2018-2019-1 20165226 《信息安全系统设计基础》第9周学习总结

    操作系统简介

    linux虚拟内存异步io及零拷贝简介