内存虚拟化(第15~22章)

内存虚拟化

地址转换

假设

  1. 用户的地址空间必须连续地放在物理内存中
  2. 地址空间不是很大, 小于物理内存
  3. 每个地址空间完全一样

按照假设, 可以如下分配地址空间:

进程空间大小为0~16KB

操作系统将物理内存中0~16KB空间留给了自己

将进程空间重定位到32KB~48KB

 

动态重定位

操作系统通过基址寄存器和界限寄存器将地址空间放在物理内存的任意位置:

物理地址 = 虚拟地址 + 基址寄存器

这种转换是在运行时发生的:动态重定位

 

界限寄存器用来提供访问保护, 如果访问超过这个界限或负值, 将触发异常。

CPU这个负责地址转换的部分统称为内存管理单元:MMU

 

空闲列表:记录哪些内存空闲, 操作系统将根据空闲列表分配内存

操作系统还有移动程序空间的能力

 

动态重定位有效率低下的问题

会出现内部碎片(已分配但未使用), 造成了浪费

 

 

分段

三个基址:代码段、栈、堆, 依然是通过虚拟地址加上基址判断是否在界限内

如果分为三个段, 需要前两位标识, 剩下的用来表示段内偏移

如果只分为代码段、栈段、堆段三个段, 但是实际分为了四个段(两个位可以标识四个数), 浪费了一个, 有些系统会将堆和栈当作同一个段, 只需要一位标识。

栈反向增长, 堆正向增长

 

分段可能会出现外部碎片, 一些段散落在各处, 没法利用

操作系统可以将以分配的段拷贝到一起, 使内存紧凑, 但是成本很高

目前有很多算法, 都无法彻底消除外部碎片, 只能减小

 

空闲空间管理

free use free
0~10 10~20 20~30

10~20的空间已经使用了, 0~10、20~30的空间是空闲的, 空闲的空间总数为20, 被分为了两个碎片

但是如果我们需要15的连续空间, 分配请求会失败

我们需要让碎片最小化

假设:这里主要讨论外部碎片、程序申请的空间不可以移动、程序管理的是连续的一块字节区域(不可增长)

 

底层机制

通过链表将空闲的空间连起来:空闲链表

操作

分割:如果我们只申请大小为 1 的内存, 10 大小的内存将会被分割为 1 和 9 进行分配

合并:将合并可用的连续内存块

 

大多数分配程序会先分配一个小堆, 然后让堆增长

 

基本策略

保证快速和碎片最小化

1. 最优匹配:找到最接近用户请求的内存块, 从而避免空间浪费, 需要遍历(牺牲性能)

2. 最差匹配:很差

3. 首次匹配:找到第一个足够大的块, 分割后返回给用户。 但是会导致开头有很多碎片, 有速度优势, 一般将空闲列表基于地址排序, 合并操作会比较容易

4. 下次匹配:从上一次查找结数的位置开始, 和首次匹配很接近, 避免了堆列表开头的频繁分割, 将空闲空间的查找扩散到了整个链表中

5. 分离空闲列表:如果应用程序经常申请一种或几种大小的空间, 独立除一个列表来管理这样大小的对象。 (厚块分配程序)

6. 伙伴系统:

 

分页

分段之后, 空间本身会碎片化, 随着时间的推移, 分配内存将会变得困难

第二种方法:将空间分割成固定长度的分片。

进程的地址空间分成固定大小的单元, 每个单元称为一页。

物理内存的分割, 每个单元称为页帧, 每个页帧包含一个虚拟内存页。

操作系统在分配页的时候只需要将空闲页拿出来就可以了, 需要几个空闲页就给几个(灵活、空间管理的简单性)

为每一个进程保留一个数据结构:页表, 记录虚拟地址对应内存中的页帧。

分页:

虚拟地址分为两部分:虚拟页面号、页内偏移量

只需要将虚拟页面号(PFN)转换为(VPN), 页内偏移量不变。

页表很大, 一般放在内存中, 或者转移到磁盘中

分页的过程很慢, 并且占用太多内存

分也不会导致外部碎片, 其次非常灵活, 支持稀疏的地址空间

 

快速地址转换(TLB)

由于(PFN)映射为(VPN)的速度慢, 增加了地址转换旁路缓冲寄存器(地址转换缓存)(TLB)

每次内存访问, 先检查TLB中是否有映射, 如果有, 将很快完成转换, 不用访问页表(页表中存在全部的映射)。

TLB带来巨大的性能提升。

TLB在处理其核心附近, 设计的访问速度很快

基本算法

如果TLB命中, 则直接在TLB中转换即可

如果TLB未命中, 则需要在页表中查找, 更新TLB, 下次再访问同一页, 将会命中

 

我们希望尽可能避免TLB未命中

如果连续访问最近访问的页, TLB命中率会很高

 

当发生未命中时, 硬件抛出一个异常, 由操作系统完成TLB的更新

 

TLB中的内容

典型的TLB有32项、64项、128项

一项TLB中的内容可能是这样:VPN | PFN | 其他位(有效位、保护位等)

TLB中的地址映射只对当前进程有效

 

进程切换时管理TLB:

1. 上下文切换时, 清空TLB

2. 跨上下文切换的TLB共享, 使用ASID标识该映射属于哪个进程(地址空间标识符)

如果不同的进程虚拟页映射到了同一个物理页中, 代表两个进程共享了该物理页, 共享代码页减少了物理页的使用。

 

缓存替换策略

当TLB中插入新项时, 需要替换旧项, 应该替换哪一项?

1. 替换最近最少使用项(LRU):

2. 随机替换:如果程序循环访问 n + 1 页, 而TLB中只能放下 n 项, 此时LRU将会裂开..., 使用随即替换策略更好。

 

实际系统的TLB表项

全局位(G):标识该页是不是所有进程共享的, 如果为1, 会忽略ASID

ASID有8位, 如果正在运行的进程超过8位

一致位(C)

脏位(D):标识改为是否被写入新数据

有效位(V):标识该地址映射是否有效

还有一些未使用的位

 

较小的页表

页表和页的大小有直接关系, 如果使用更大的页, 页表将会更小。

但是大的页表将会造成每页内部的浪费:内部碎片。

大多数系统使用4KB或8KB

 

混合方法:分页和分段

典型的线性页表中, 大量的页表是无效项, 没有使用, 但是依然占用页表的一项。

图中仅使用了4页, 但是页表中将16页都表示了出来(没有使用的页将会标记无效)

杂合的方法是为每一个逻辑分段各提供单个页表, 再这个例子中, 我们有三个页表

 

基址和界限改变了用法:

基址保存该段页表的物理地址

界限用于指示页表的结尾

 

在这个例子中, 将有三个基址/界限对。

杂合方法关键区别在于, 界限标明了页表的有效项, 未分配的页不再占用页表空间。

 

这种方法也存在问题:

如果有一个大而稀疏的堆, 依然导致大量的页表浪费

导致外部碎片再次出现

 

多级页表

不同于杂合方法, 多级页表将线性页表变成类似树的结构, 这种方法非常有效。

 

将页表分页, 如果该页全部无效, 将不分配该页的页表(通过页目录查找页)

页目录(PDE)

如果PDE是有效的, 代表该页目录中至少有一页是有效的, 即PDE指向的页中, 至少有一个PTE(页表项)是有效的

如果PDE无效, 则PDE其余部分均无定义。

 

多级页表分配的页表空间, 和正在使用的地址空间内存量成比例, 不会将未使用的空间放入页表中。

非常紧凑, 支持稀疏的地址空间。

有了多级页表, 我们可以通过页目录将页表放在物理内存中的任意位置

 

多级页表是有成本的, TLB未命中时, 需要先加载页目录, 再加载PTE本身(两级页表)

多级页表比先行页表更加复杂。

 

详细示例:

我们可能需要越过两级的页表。

 

反向页表:更极端的空间节省

 

机制

大而慢的硬盘位于底层, 我们如何利用硬盘为进程支持巨大的地址空间?

 

在硬盘上开辟出一部分空间用于物理页的移入和移出。(交换空间, 操作系统需要记住给定页的硬盘地址)

 

当TLB未命中时, 需要增加额外的机制:

硬件检查PTE时发现该页不再物理内存中(存在位), 访问不在物理内存中的页, 称为页错误 / 页未命中, 唤醒页错误处理程序。

 

页错误

页表中存储交换到磁盘中的页的地址, 当出现页错误时, 操作系统会在PTE中查找, 将请求发送到硬盘, 将页读取到内存中。

操作系统更新页表, 将该页设置为存在, 更新表项(PTE)的PFN字段记录新获取页的位置。

重试指令, TLB未命中, 但这次未命中是在内存中。

 

内存不足

选择哪些页被交换到磁盘中的过程, 称为页交换策略

 

交换的发生

操作系统不会等待内存满了才会交换页, 大多数会设置高水位线和低水位线。

负责的进程称为交换守护进程或页守护进程。

执行完毕后这个进程会进入休眠状态。

有时会执行一些优化:将多个写入的页聚集或分组写入到交换区间, 提高硬盘效率。

 

策略

当内存不足时, 需要选择替换出哪些页到磁盘中。

 

1. 最优替换策略(OPT):总能达到总体未命中(内存)最小, 是一个理想策略。

踢出最远将来才会访问的页。 但是未来的访问是无法预知的。

 

2. FIFO策略

实现相当简单, 先进先出。

 

3. 随机策略(RAND)

 实现也很简单, 随机... ...

 

4. LRU:替换最近最少使用

FIFO和随机策略可能都会有一个共同的问题, 尽管一个页很重要, 它还是会将这个页踢出去(尽管它很快会被载入)。

如果一个页被访问了很多次, 也许不应该被替换(最近访问的页, 再次访问的可能性就会大)——近期性。

 

5. LFU:替换最不经常使用

 

6. MFU:最经常使用策略:大多数情况效果不好

 

7. MRU:最近使用策略:大多数情况效果不好

 

实现LRU

为了记录哪些页是最少和最近被使用, 系统必须对每次内存做一些记录。

由于页的数量很多, 记录可能会极大地影响性能。

 

近似LRU

需要硬件增加一个使用位, 每当页被引用时, 使用位会被置 1, 清除该为由操作系统负责。

1. 时钟算法:将所有页放入一个循环列表中, 时钟指针开始指向某一页, 如果该页使用位为 1, 则将该页清零, 指向下一页... ...直到找到一页使用位为 0 的页, 将它踢出。

 

考虑脏页

如果一个页被修改, 因此变脏, 则踢出它必须将它写入磁盘。

所以一些虚拟机系统更倾向于踢出干净页, 而不是脏页

 

其他

有些时候, 操作系统会猜测一个页面即将被使用, 将它从硬盘中提前载入——预取

许多系统会在内存中收集一些待完成写入, 并以一种更高效的方式将他们集中写入——聚集写入, 执行单次大的写操作, 比许多小的写操作更高效

 

抖动

当内存被超额请求时, 系统将不断地进行换页——抖动。

当发生抖动时, 目前一些系系统采用更严格的方法:例如, 内存不足杀死程序。

 

posted @ 2020-08-26 23:59  x_Aaron  阅读(250)  评论(0)    收藏  举报