第9章 虚拟存储器

  • 虚拟存储器是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供一个大的、一致的私有的地址空间;
    • 主存是磁盘的高速缓存、主存中只保留活动区域、并且根据需要,在磁盘和主存之间来回传送数据;
    • 每个进程提供一致的地址空间,简化了存储器的管理;
    • 保护每个进程的地址空间,保护不被破坏;

9.1 物理和虚拟寻址

  • 内存中每个字节都有一个物理地址
  • CPU利用物理地址进行访问,是物理寻址;
  • CPU利用虚拟地址,在地址被传送到存储器之前,先进行地址翻译(存储器管理单元(MMU)和系统的合作),在访问物理主存;

9.2 地址空间

  • 地址空间:非负整数地址的有序结合;整数是连续的,我们称为 线型地址空间
  • 虚拟地址空间:这是CPU生成的地址空间;
  • 物理地址空间:对应于物理存储器的M个字节;
  • 物理地址可以对应多个虚拟地址(如共享内存);

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

  • 虚拟存储器:被组织为一个由存放在磁盘上N个连续字节大小的数组;
  • 虚拟页:将虚拟存储器分割为虚拟页,以此为单位用来与物理内存交换;
    • 未分配的:系统还未分配的页,在页表中体现为,既不指向物理存储器,也不指向虚拟页
    • 缓存的:当前虚拟页已被加载到物理内存中,页表体现为指向物理内存;
    • 未缓存的:当前虚拟页未被加载到物理内存中,页表体现为指向虚拟页(磁盘);
  • 物理页:物理内存被分割为物理页;
  • DRAM总是使用写回,而不是直写;
9.3.2 页表
  • 页表将虚拟页映射到物理页,页表常驻在内存中;
  • 每次地址翻译时,都将读取页表,这需要系统、内存管理单元中的地址翻译硬件和页表的共同作用;
  • 系统负责维护页表,以数据页的相互传送;
  • 页表是一个条目的数组,虚拟页页表中都有一个条目,具有固定的偏移;
    这里写图片描述
9.3.3 页命中

当CPU通过虚拟地址翻译后,对应的虚拟页已缓存到内存中时,称为页命中;

9.3.4 缺页
  • DMA缓存不命中称为缺页;
  • 当CPU的虚拟地址访问页表发现虚拟页并未缓存时(有效为未标记),触发缺页异常;
  • 缺页异常调用缺页异常处理程序;程序会选择一个牺牲页,如果牺牲页发生过修改,就写回;并修改牺牲页的条目,表明此虚拟页不在存储器中;
  • 将需要缓存的虚拟页加载到存储器中,跟新对应条目,表明此虚拟页已缓存;之后重新启动导致缺页的指令;
    这里写图片描述
9.3.5 分配页面

在磁盘上创建空间,并更新对应页表目,指向新创建的虚拟页(未被缓存);

9.3.6 页的局部性
  • 局部性保证了代码往往在较小的活动页面集合上工作,这个集合叫做工作集;
  • 当工作集的大小超过物理存储器时,会产生颠簸,频繁的发生页面交换;

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

  • 操作系统为每个进程提供一个独立的页表,因此每个进程拥有独立的地址空间
  • 按需调度页面和独立的虚拟地址空间
    • 简化链接:独立的地址空间允许每个进程的存储器映像使用相同的基本格式 ;这样简化了链接器的设计;可执行文件独立于最终的物理地址;
    • 简化加载:加载时只需要将条目指向目标文件的对应位置;在实际中,加载器从不拷贝任何数据到存储器,而是按需交换数据页;
    • 简化共享:只需要将页表指向相同的物理内存页;
    • 简化存储器分配:当用户程序需要堆内存时,系统分配K个连续虚拟存储器页面,但由于页表的存在,实际的物理存储器中并不存在;

9.5虚拟存储器作为存储器的保护工具

一个用户进程不能:

  • 修改其只读文本段
  • 内核代码和数据结构
  • 其他进程的存储器
  • 共享的物理页面,除非显式允许(共享内存)

    解决办法,在页表中添加标志位:

9.6 地址翻译

地址翻译是N元素的虚拟地址空间和M元素的物理地址空间之间的映射:
这里写图片描述

虚拟地址-》物理地址的大致翻译原理:
这里写图片描述

  • 虚拟页偏移量(VPO)和物理页偏移量(PPO)是相同的,因为虚拟页和物理页大小相同,可以直接映射;

物理地址翻译过程:
这里写图片描述

9.6.1 结合高速缓存和虚拟存储器
  • 当存在SRAM和虚拟存储器时,大多数系统使用物理地址访问SRAM(SRAM相当于主存,因此也可以采用虚拟地址的方式,但大多数系统没有采用);
  • 高速缓存无需处理保护的问题,因为这是在地址翻译的过程中解决的;
    这里写图片描述
9.6.2 利用TLB加速地址翻译
  • 在MMU中包括了一个关于PTE的小缓存,翻译后备缓冲器(Translation Lookaside Buffer,TLB);
  • TLB每一行都保存着一个PTE条目;
  • 访问TLB的标记和索引都是从虚拟地址中的VPN提取的;
    这里写图片描述
  • 应为所有的操作都是在MMU中进行的,速度非常快;
    这里写图片描述
9.6.3 多级页表

-当使用一级页表时,对于32位的虚拟地址,页表就占就有4MB的空间,64位占有跟多;
- 使用多级页表可以解决这样的问题;
这里写图片描述
- 一级页表条目指向二级页表,二级页表条目指向虚拟存储器;
- 当一级页表有些条目为空时,二级页表不需要存在,大大节省了空间
- 只有一个页表总是存储在存储器中,系统可以在需要时创建,调入,调出二级页表;只有经常使用的二级页表存在在主存中;

#### 9.7.2 Linux虚拟存储器系统

  • Linux为每个进程维护了一个单独的虚拟地址空间
  • Linux也将一组连续的虚拟页面映射到一组连续的物理页面,大小为DRAM,方便系统访问任何位置;(因为用户空间中没有映射所有的物理页,所以不能通过用户空间访问所有物理页)

    完整的虚拟地址空间:
    这里写图片描述

    1. Linux将虚拟存储器区域
      • Linux将虚拟存储器组织成一些区域,也叫
      • 一个区域或者段,就是已经分配的虚拟存储器的连续片;
      • 每个存在的虚拟页都是在某个区域中的,不属于某个区域的虚拟页是不存在的;
      • 区域的存在,使得虚拟地址空间存在间隙,内核也不用记录不存在的虚拟页;(页表记录)
        这里写图片描述
    2. Linux缺页处理
      当MMU试图翻译一个虚拟地址时,触发缺页异常,处理程序会执行下面的步骤:
      • 虚拟地址是否合法,地址是否在某个区域中;
      • 访问是否合法,即是否有权限读、写或者执行的权限;
      • 到此处就是合法的地址和访问权限,异常是由于缺页造成的;交换页之后,再次执行这个虚拟地址的翻译
        这里写图片描述

9.8 存储器映射

  • 将虚拟存储器区域与磁盘上的一个对象关联起来,以初始化这个虚拟存储器区域的内容,此称为存储器映射
  • 虚拟存储器可以映射两种文件:
    • 普通文件:将文件分成页大小的片,将页表指向这些片,但不缓存这些片,采用按需调度的策略,直到CPU第一次引用这个页面;如果区域要比文件区大,就用零填充;
    • 匿名文件:匿名文件由内核创建,包含的全是二进制零;过程是:CPU在物理存储器找一个合适的牺牲页面(修改过则写回),用零覆盖,更新页表,将此页标记为缓存在存储器中;磁盘和存储器没有数据交换;
9.8.1 在看共享对象
  • 一个对象可以被映射到虚拟存储器的一个区域,要么作为一个共享对象,要么私有对象;
  • 当作为共享对象时,任何修改对其他进程是可见的,也会反映到磁盘的文件上;
  • 映射到私有区域的对象,修改对其他进程是不可见的,也不会影响磁盘上的文件;
    这里写图片描述
  • 私有对象使用了一种写时拷贝技术
    • 写时拷贝只会拷贝要修改的页,使页表条目指向新拷贝的物理页
      这里写图片描述
9.8.2 再看fork函数
  • 当调用fork时,内核为新进程创建各种数据结构,并分配一个唯一的PID;
  • 给新进程创建虚拟存储器,复制当前进程的页表,mm_struct和区域结构;并将两个进程的页面都标记为只读,区域结构标记为写时拷贝;
9.8.3 再看execve函数

利用execve函数加载程序有以下几步:

  • 删除已经存在的用户区域结构:删除当前进程中用户空间中的区域结构;
  • 映射私有区域:为新程序的文本、数据、bss和栈区创建新的区域结构;所有的这些区域都是私有的,写时拷贝的;
  • 映射共享区域:将共享库动态链接到此程序,然后映射到用户虚拟地址空间的共享区域;
  • 设置程序计数器: 设置当前进程的上下文程序计数器指向文本区域的入口点;
    这里写图片描述
9.8.4 使用mmap函数的用户级存储器映射

mmap函数创建新的虚拟存储器区域,并将对象映射到这些区域中;
这里写图片描述

9.9 动态存储器分配

  • 动态存储器分配器维护虚拟存储器中的堆区域;
  • 对于每一个进程,内核维护着一个变量brk,指向堆的顶部
  • 分配器将堆视为一组不同大小的块(就是malloc分配的对齐的空间),而每个块就是连续的虚拟存储器片,要么已分配,要么空闲;
  • 分配时,都要求显式执行;
  • 已分配的块的释放方式:
    • 显式分配器
      • 要求引用显式的释放已分配的块
      • 例如malloc&free new&delete
    • 隐式分配器
      • 当分配器检测到不再使用的已分配的块时,就释放这个块;
9.9.1 malloc和free函数

这里写图片描述

  • 返回块大小至少为申请的大小,可能为了能够保存各种数据类型,保证内存对齐
  • malloc从初始化分配的块,可以使用calloc函数,此函数将块初始化为0;
  • 改变已分配块的小小,realloc函数;
  • 可以使用mmap和mumap函数显式分配和释放堆存储器
  • 也可以使用sbrk函数:

这里写图片描述

  • 通过将内核的brk指针增加incr来扩展或者收缩;

  • 程序通过调用free来释放以分配的块:
    这里写图片描述

    • ptr必须指向一个从malloc、calloc、realloc获得的已分配块的起始位置;
9.9.2 为何要使用动态存储器分配
  • 程序经常在运行时才知道某些数据结构的实际地址;
9.9.3 分配器的要求和目标
  • 处理任意的请求序列
    • 一个应用可以有任意的分配请求和分配请求序列
    • 每个释放请求必须对应一个通过分配请求的块;
  • 立即响应请求
    • 分配器必须立即响应请求,不可为提高性能而重新排列或者缓冲请求
  • 只使用堆
    • 任何非标量数据结构都必须都必须保存在堆中;
  • 对齐块
    • 对齐块,保证可以存储任何类型的数据对象;
  • 不修改已经分配的块

    • 分配器只能操作已分配的块,一旦块被分配,就不可以移动或者修改他;
    9.9.4 碎片

    虽然有未使用的存储器,但不能用来满足分配请求,这些存储空间被称为碎片
    碎片的两种形式:

  • 内部碎片
    • 发生在分配的块比有效载荷大时发生的;比如为了对齐;
  • 外部碎片

    • 当空闲块合并时满足请求,但没有一个单独的块满足请求;
    9.9.6隐式空闲链表

    一个简单块的结构:
    这里写图片描述

    • 由于双字对齐,所以地址前3位总是0,用来存放标记;

    隐式空闲链表结构:
    这里写图片描述

    • 空闲块是通过头部中的大小字段隐含的链接着,所以称为隐式空闲链表;
    • 最后为一个结束块,大小0,已分配;
    9.9.7 放置已分配的块

    当请求一个k字节的块时,结果是由放置策略决定的,常用的放置策略:

    • 首次适配
    • 下一次适配:每一次搜索都是从上一次结束的地方开始
    • 最佳适配:大小最合适
    9.9.8 分割空闲块
    • 匹配较好时,只会产生内部碎片;
    • 匹配较差时,一部分变成分配块,一部分变成空闲块;
    9.9.9 获取额外的堆存储器

    当存储器不能找到合适的块时:

    • 合并相邻的空闲块
    • 当还不满足时,分配器调用sbrk函数,申请额外的堆存储器,插入链表中;
    9.9.10 合并空闲块
    • 假碎片:有两个相邻的空闲块,单独时不能满足分配请求,合并则可以,则这两个空闲块形成假空闲块
      这里写图片描述
  • 合并策略:

    • 立即合并:释放块时,合并相邻的块;
    • 推迟合并:推迟到某个时刻,例如没有合适的空闲块时;
    9.9.11 带边界标记的合并

    这里写图片描述

    • 在块的尾部添加一个标记,那么释放的当前块就可以用常数时间去查询前面一个块是否空闲,而链表又需要重新遍历到当前块的前一个块;
    9.9.13 显式空闲链表

    显式空闲链表显式的将指针记录在空闲块中:
    这里写图片描述

  • 使用双向链表而不是隐式空闲链表,使得空闲块的寻找时间从总块数的线型时间,下降到空闲块的线性时间;
  • 显式链表的缺点是空闲块的空间必须足够大,用以存放
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值