寻址
物理寻址
计算机系统的主存被组织成一个由MM个连续的字节大小的单元组组成的数组,每个字节都有唯一的物理地址
第一个字节的地址是0,访问物理地址的方式叫做物理寻址
当CPU执行加载指令,会生成一个有效的物理地址,通过存储器总线,将地址传递给主存。主存取出物理地址处的4字节的字,返回给CPU
虚拟寻址
CPU通过生成一个虚拟地址来访问主存,虚拟地址在被送到存储器之前先转换成适当的物理地址(地址翻译)
地址翻译需要CPU硬件和操作系统紧密合作,CPU芯片上叫做存储器管理单元(MMU)的专用硬件,存放在主存中的查询表来动态翻译虚拟地址,表的内容由操作系统管理
地址空间
地址空间是一个非负整数地址的有序集合
如果地址空间中整数是连续的,叫做线性地址空间
物理地址空间
与系统中物理存储器的MM个字节相对应,MM不要求是2的幂
虚拟地址空间
位地址空间{0,1,2,…,N−1}{0,1,2,…,N−1},N=2nN=2n
虚拟存储器
虚拟存储器(VM)是硬件异常、硬件地址翻译、主存、磁盘文件、内核软件的完美交互,为每个进程提供了一个大的、一致的、私有的地址空间
虚拟存储器(VM)被组织为一个由存放在磁盘上的NN个连续的字节大小的单元组成的数组,每个字节都有一个唯一的虚拟地址,这个唯一的虚拟地址是作为道数组的索引的
能力
- 将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动活动区域,根据需要在磁盘和主存之间来回传送数据,高效地使用主存
- 为每个进程提供了一致的地址空间地址,简化了存储器管理
- 保护了每个进程的地址空间不被其他进程破坏
缓存的工具
VM系统通过将虚拟存储器分割为大小固定的块(虚拟页(VP))实现磁盘上数组的内容被缓存在主存中。虚拟页的大小为
物理存储器被分割为物理页/页帧,大小也为PP字节
因为磁盘不命中处罚和访问第一字节的开销
- 虚拟页往往很大,典型的是
- 虚拟页是全相联的
- 替换策略复杂精密
- 使用写回策略
虚拟页的分类
在任意时刻,虚拟页面的集合都分为3个不相交的子集
未分配的
VM系统还未分配(或创建)的页
未分配的块没有任何数据和它们相关联,因此不占用磁盘空间
缓存的
当前缓存在物理存储器中的已分配页
未缓存的
没有缓存在物理存储器中的已分配页
页表
页表是将虚拟页映射到物理页的数据结构,存放在物理存储器
每次地址翻译硬件将一个虚拟地址转换为物理地址时都会读取页表,操作系统负责维护页表内容,以及在磁盘和主存之间来回传送页
页表是一个页表条目(PTE)的数组,虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个PTE
有效位
如果设置了有效位,地址字段表示在主存中相应的物理页的起始位置
如果没有设置有效位,空的地址字段表示虚拟页还没被分配,否则地址字段表示在磁盘上的起始位置
许可位
- SUP
如果设置了SUP位,进程必须在内核模式下才可以访问
- READ
如果设置了READ位,进程可以读页面
- WRITE
如果设置了WRITE位,进程可以修改页面
nn位有效地址
表示主存中相应物理页的起始位置或者磁盘上的起始位置
页命中
MMU将虚拟地址作为页表的索引定位PTE,如果有效位被设置,MMU直接使用PTE中的位有效地址作为物理地址,称为页命中
缺页
MMU将虚拟地址作为页表的索引定位PTE,如果有效位未被设置,称为缺页
缺页会触发缺页异常,缺页异常调用内核中缺页异常处理程序,该程序会选择一个牺牲页,将牺牲页写回到磁盘,将需要的页加载到内存
磁盘和存储器之间传送页的活动叫做交换/页面调度,包括页从磁盘换入/页面调入到内存和从内存换出/页面调出到磁盘
由于局部性原则,程序汪汪在一个较小的活动页面集合上工作(工作集/常驻集),在初始开销之后,虚拟存储器系统能够工作相当好。但是当工作集大小超出了物理存储器的大小,页面会不断地换入换出(颠簸),程序性能大大下降
存储器管理的工具
实际上,操作系统为每个进程提供了一个独立的页表,因而也就是一个独立的虚拟地址空间
按需页面调度和独立的虚拟地址空间结合,对系统存储器的使用和管理造成了深远的影响
简化链接
独立的地址空间允许每个进程的存储器映像使用相同的基本格式,而不管代码和数据实际存放在物理存储器的何处
简化加载
虚拟存储器还使得容易向存储器中加载可执行文件和共享对象文件,加载器从不实际拷贝任何数据从磁盘到存储器,虚拟存储器系统会按照需要自动地调入数据页
简化共享
独立地址空间为操作系统提供了一个管理用户进程和操作系统自身之间共享的一致机制。在需要共享代码和数据的情况下(内核、C标准库),操作系统通过将不同进程中适当的虚拟页面映射到相同的物理页面,从而安排多个进程共享这部分代码的一个拷贝,而不是在每个进程中都包括单独的内核和C标准库的拷贝
简化存储器分配
虚拟地址上连续的页面,在物理地址上可能是不连续的,可以随机分散在物理存储器中
存储器保护的工具
现代计算机系统必须为操作系统提供手段来控制对存储器系统的访问
- 不允许一个用户进程修改它的只读文本段
- 不允许用户进程读或修改任何内核中的代码和数据结构
- 不允许用户进程读或写其他进程的私有存储器
- 不允许用户进程修改任何与其他进程共享的虚拟页面,除非共享者都显式地允许(通过调用明确的进程间通信系统调用)
虚拟存储器系统通过许可位来控制访问,如果一条指令违反了许可条件,那么CPU就出发一个一般保护故障,将控制传递给一个内核中的异常处理程序。Unix的Shell一般将这种异常报告为段错误(segmentation fault)
地址翻译
地址翻译是一个NN元素的虚拟地址空间(VAS)中的元素和一个元素的物理地址空间(PAS)中元素之间的映射
MAP:VAS→PAS∪ϕMAP(A)={A′ϕ如果虚拟地址A处的数据在物理地址A′处如果虚拟地址A处的数据不在物理存储器中MAP:VAS→PAS∪ϕMAP(A)={A′如果虚拟地址A处的数据在物理地址A′处ϕ如果虚拟地址A处的数据不在物理存储器中- CPU中的页表基址寄存器(PTBR)指向当前页表
- nn位的虚拟地址包含两个部分
- 位的虚拟页面偏移(VPO)
- n−pn−p位的虚拟页号(VPN)
- MMU利用虚拟地址中的VPN选择PTE,将物理地址页号(PPN)和虚拟地址中的VPO串联起来得到物理地址
当页面命中时,CPU硬件执行的步骤
- CPU生成一个虚拟地址,传给MMU
- MMU生成PTE地址,从高速缓存/主存请求得到它
- 高速缓存/主存向MMU返回PTE
- MMU构造物理地址,传给高速缓存/主存
- 高速缓存/主存返回所请求的数据字给处理器
当页面不命中时,CPU硬件执行的步骤
- CPU生成一个虚拟地址,传给MMU
- MMU生成PTE地址,从高速缓存/主存请求得到它
- 高速缓存/主存向MMU返回PTE
- MMU触发一次缺页异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序
- 缺页处理程序确定出物理存储器中的牺牲页,换出到磁盘
- 缺页处理程序调入新的页面,更新存储器中的PTE
- 缺页处理程序返回原来的进程,再次执行导致缺页的指令,CPU将引起缺页的虚拟地址重新发送给MMU
- MMU生成PTE地址,从高速缓存/主存请求得到它
- 高速缓存/主存向MMU返回PTE
- MMU构造物理地址,传给高速缓存/主存
- 高速缓存/主存返回所请求的数据字给处理器
结合高速缓存和虚拟存储器
主要思路是地址翻译发生在高速缓存查找之前
利用TLB加速地址翻译
在MMU中包括了一个关于PTE的小的缓存,叫做翻译后备缓冲器(TLB)
如果TLB有2t2t个组,那么TLB索引(TLBI)是由VPN的tt个最低位组成,而TLB标记(TLBT)是由VPN中剩余的位组成
当TLB命中时,CPU硬件执行的步骤
- CPU生成一个虚拟地址,传给MMU
- MMU生成PTE地址,从TLB请求得到它
- TLB向MMU返回PTE
- MMU构造物理地址,传给高速缓存/主存
- 高速缓存/主存返回所请求的数据字给处理器
当TLB不命中时,CPU硬件执行的步骤
- CPU生成一个虚拟地址,传给MMU
- MMU生成PTE地址,从TLB请求失败,继而转向从高速缓存/主存请求得到它
- 高速缓存/主存向MMU返回PTE,并放在TLB中
- MMU构造物理地址,传给高速缓存/主存
- 高速缓存/主存返回所请求的数据字给处理器
多级页表
多级页表减少了存储器要求
- 如果一级也表中的一个PTE是空的,那么相应的二级页表根本不会存在,这是巨大的潜在节约
- 只有一级页表才需要总是在主存中,虚拟存储器系统可以在需要时创建、页面调入调出二级页表,减少了主存的压力
优化地址翻译
当CPU需要翻译一个页表条目时,L1高速缓存利用VPO位查找相应的组,病毒出组里的标记和数据字,当MMU从TLB的到PPN,缓存已经准备好试着把PPN与八个标记中的一个进行匹配,而不是等到MMU发送物理地址后才开始匹配
存储器映射
Linux通过将一个虚拟存储器区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容,这个过程称为存储器映射
虚拟存储器区域可以映射到两种类型的对象中的一种
Unix文件系统中的普通文件
一个区域可以映射到一个普通磁盘文件的连续部分
文件区(section)被分成页大小的片,每一片包含一个虚拟页面的初始内容。如果区域比文件区大,那么用0来填充区域剩下的部分
因为按需调度,所以虚拟页面没有实际交换到物理存储器,直到CPU第一次引用到页面
匿名文件
匿名文件是由内核创建的,包含的全是二进制零
CPU第一次引用这样一个区域内的虚拟页面时,内核就在物理存储器中找到一个合适的牺牲页面并换出,用二进制零覆盖牺牲页面并更新页表(请求二进制零的页),将这个页面标记为驻留在存储器中的(并没有实际的数据传送)
一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件/交换空间/交换区域之间换来换去
在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数
共享对象
如果一个进程将一个共享对象映射到它的虚拟地址空间的一个区域,那么这个进程对这个区域的任何写操作,对于那么也把这个共享对象映射到它们虚拟存储器的其他进程而言也是可见的,并且这些变化也会反映在磁盘上的原始文件中
如果两个进程将一个私有对象映射到虚拟存储器的不同区域,但是共享这个对象的同一份物理拷贝。对于每个映射私有对象的进程,相应的私有区域的页表条目都被标记为只读,并且区域结构被标记为私有的写时拷贝。只要没有进程写,就可以继续共享物理存储器中对象的一份单独拷贝;如果有一个进程试图写,那么这个写操作就会触发保护故障,先在物理存储器中创建这个页面的新拷贝,更新页表条目指向这个新拷贝,然后恢复页面的可写权限。当故障处理程序返回时,CPU重新执行这个写操作,现在在新创建的页面上的写操作就可以正常执行了
fork函数
fork将两个进程中的每个页面都标记为只读,并将两个进程中每个区域结构都标记为私有的写时拷贝
execve函数
加载并运行新的程序,需要以下步骤
删除已存在的用户区域
删除当前进程虚拟地址的用户部分中的已存在的区域结构
映射私有区域
为新程序的文本、数据、bss和栈创建新的区域结构,都是私有的写时拷贝的
- 文本和数据区域映射到可执行文件中的文本和数据区
- bss区域时请求二进制零的,映射到匿名文件,大小包含在可执行文件中
- 栈和堆也是请求二进制零的,初始长度为零
映射共享区域
如果可执行程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域
设置程序计数器
将程序计数器指向文本区域的入口点
动态存储器分配
动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆(heap)
对每个进程,内核维护着一个变量
brk
,指向堆的顶部分配器将堆视为一组大小不同的块的集合来维护,每个块是一个连续的虚拟存储器片,要么是已分配的,要么是空闲的
动态存储器分配器严格的约束条件
- 处理任意请求序列
- 立即响应请求
- 只使用堆
- 对齐要求
- 不修改已分配的块
动态存储器分配器风格
显式分配器
要求应用显式地释放人和已分配的块
隐式分配器/垃圾收集器
要求分配器监测一个已分配块何时不再被程序所使用,就释放这个块
动态存储器分配器的目标
最大化吞吐率
吞吐率定义为每个单位时间里完成的请求数
最大化存储器利用率
峰值利用率来描述堆的效率
聚集有效载荷PkPk为当前已分配的块邮有效载荷之和
堆的当前大小HkHk单调非递减
<Empty Math Block><Empty Math Block>Uk=maxi≤kPiHkUk=maxi≤kPiHk动态存储器分配器碎片问题
内部碎片
已分配块比有效载荷大
外部碎片
当空闲存储器合计起来足够一个分配请求,但是没有一个单独的空闲块足够大来处理这个请求
C语言中的函数
程序通过
malloc
从堆中分配块返回的指针指向大小至少为size字节的存储器块,可能会包含在这个块内的任何数据对象类型做对齐。Unix系统是8字节边界对齐
通过
free
从堆中释放已分配的堆块
#include <stdlib.h> /* 分配指定大小的块 * 如果成功,返回指向大小至少为size字节的存储器块 * 如果出错,返回NULL */ void *malloc(size_t size); // 释放已分配的堆块 void free(void *ptr);
显式分配器的实现
隐式空闲链表
将一些信息嵌入在块本身
头部
使用最低位的几个位来之名块是已分配还是未分配的
使用高位来纪录块的大小
有效载荷
填充
脚部
头部的拷贝,用来进行边界标记
优点
- 简单
缺点
- 开销和堆中已分配块和空闲块的总数呈线性关系
放置块已分配块
放置策略
首次适配
从头开始搜索空闲链表,选择第一个合适的空闲块
下一次适配
从上一次查询结束的地方开始搜索空闲链表,选择第一个合适的空闲块
最佳适配
从头开始搜索所有空闲块,选择适合所需请求大小的最小空闲块
分割空闲块
一旦找到了匹配的空闲块,就要进行分割
分割策略
不分割
产生内部碎片
分割成两块
可能产生外部碎片
合并空闲块
邻接的空闲块叫做假碎片,必须合并相邻的空闲块
合并策略
立即合并
可能产生抖动
推迟合并
当块带有边界标记
- 通过使用当前空闲块的头部计算得到下一个块的头部地址(头部地址+块大小),如果是空闲块,进行合并
- 通过使用当前空闲块的头部计算得到上一个块的脚部地址(头部地址-1),如果是空闲块,进行合并
获取额外的堆存储器
合并之后如果不能生成足够大的块,那么分配器会通过调用
sbrk
函数,向内核请求额外的堆存储器。分配器将额外的存储器转化成一个大的空闲块,将这个块插入到空闲链表中,然后将被请求的块放置在这个新的空闲块显式空闲链表
使用双向链表
维护链表
先进后出(LIFO)
新释放的块放置在链表开始处
按照地址顺序
链表中每个块的地址小于它后继地址
分离的空闲链表
对单向空闲块链表的分配器的改进,使用多个分离的空闲链表
将所有可能的块大小分成一些等价类(大小类)
例如
{1},{2},{3,4},...,{1025∼2048},{2049∼4096},{4097∼∞}{1},{2},{3,4},...,{1025∼2048},{2049∼4096},{4097∼∞}{1},{2},{3},{4},...,{1023},{1024},{1025∼2048},{2049∼4096},{4097∼∞}{1},{2},{3},{4},...,{1023},{1024},{1025∼2048},{2049∼4096},{4097∼∞}简单分离存储
每个大小类的空闲链表包含大小相等的块,每个块大小就是这个大小类中最大元素的大小
- 分配
检查相应的空闲链表,如果链表非空,简单分配第一块的全部;如果链表为空,分配器向系统请求一个固定大小的额外存储器片,将片分成大小相等的块,并连接起来形成新的空闲链表
- 释放
简单地将这个块插入到相应的空闲链表的前部
优点
- 分配和释放块都是很快的常数时间
- 片中都是大小相等的块,不分割、不合并,存储器开销小
- 头部不需要已分配/空闲标记,已分配的块不需要头部和脚部
- 最小块就是1个字
缺点
- 容易造成内部碎片和外部碎片
分离适配
分配器维护着一个空闲链表的数组,每个空闲链表和一个大小类相关联,被组织称某种类型的显示或隐式链表
- 分配
对适当的空闲链表做首次适配,如果查找一个合适的块,进行分割(可选),将剩余部分插入到适当的空闲链表;如果找不到合适的块,搜索下一个更大的大小类的空闲链表,重复直到找到一个合适的块。如果空闲链表中没有合适的块,就向操作系统申请额外的堆处理器,从新的堆处理器中分配出一个块,将剩余部分放置在适当大小的大小类
- 释放
执行合并,将结果放知道相应的空闲链表中
优点
- 快速
- 存储器利用率高
伙伴系统
是分离适配的特例,每个类大小都是2的幂。基本思路是假设一个堆的大小为2m2m个字,为每个块大小2k2k维护一个分离空闲链表,其中0≤k≤m0≤k≤m
- 分配
请求块大小向上摄入到最接近的2的幂2k2k,如果找不到可用的块,向系统申请2m2m个字的堆;如果找到第一个可用的、大小为2j2j的块,k≤j≤mk≤j≤m,如果j=kj=k,完成分配,否则递归地而分割这个块,直到j=kj=k。而分割时剩下的半块(伙伴)放置在相应的空闲链表中
- 释放
对释放的块继续合并空闲的伙伴,直到遇到已分配的伙伴
优点
- 快速
缺点
- 内部碎片显著
- 不适用通用目的的工作负载
隐式分配器/垃圾收集器的实现
垃圾收集器将存储器视为一张可达图,图的节点被分成一组根节点和一组堆节点
当存在一条从任意根节点触发并到达节点pp的有向路径,就说是可达的,不可达的节点对应于垃圾
Mark&Sweep垃圾收集器
两个阶段
标记(mark)阶段
标出所有可达的河已分配的后记
清除(sweep)阶段
释放未标记的分配块,清除已标记的分配块
// mark function void mark(ptr p) { if ((b = isPtr(p)) == NULL) return; if (blockMarked(b)) return; markBlock(b); len = length(b); for (i=0; i < len; i++) mark(b[i]); return; } // sweep function void sweep(ptr b, ptr end) { while (b < end) { if (blockMarked(b)) unmarkBlock(b); else if (blockAllocated(b)) free(b); b = nextBlock(b); } return; }