20145316 《信息安全系统设计基础》第十四周学习总结
教材学习内容总结
第九章 虚拟存储器
为了更加有效地管理存储器并且少出错,现代系统提供了一种对主存的抽象概念,叫做虚拟存储器(VM)。虚拟存储器是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的和私有的地址空间。通过一个很清晰的机制,虚拟存储器提供了三个重要的能力:1)它将主存看成是一个存储在磁盘上的地址空间同,主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。2)它为每个进程提供了一致的地址空间,从而简化了存储器管理。3)它保护了每个进程的地址空间不被其他进程破坏。
- 虚拟存储器是中心的。
- 虚拟存储器是强大的。
- 细腻存储器是危险的。
9.1 物理和虚拟寻址
当CPU执行这条加载指令时,它会生成一个有效物理地址,通过存储器总线,把它传递给主存。主存取出从物理地址4处开始的4字节的字,并将它返回给CPU,CPU会将它存放在一个寄存器里。现代处理器使用的是一种称为虚拟寻址的寻址形式。
将一个虚拟地址转化为物理地址的任务叫做地址翻译。CPU芯片上叫做存储器管理单元的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容是由操作系统管理的。
9.2 地址空间
地址空间是一个非负整数地址的有序集合。
如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间。
一个地址空间的大小是由表示最大地址所需要的位数来描述的。一个系统还有一个物理地址空间,它与系统中物理存储器的M个字节相对应。
9.3虚拟存储器作为缓存的工具
VM系统通过将虚拟存储器分割为称为虚拟页的大小固定的块来处理这个问题。物理存储器被分割为物理页大小也为P字节(物理页也称为页帧)。
在任意时刻,虚拟页面的集合都分为三个不相交的子集:
- 未分配的:VM系统还未分配(回或者创建)的页。
- 缓存的:当前缓存在物理存储器中的已分配页。
- 未缓存的:没有缓存在物理存储器中的已分配页。
9.3.1 DRAM缓存的组织结构
我们使用术语SRAM缓存来表示位于CPU和主存之间的L1、L2和L3高速缓存,并且用术语DRAM缓存来表示虚拟存储器系统的缓存,它在主存中缓存虚拟页。
在存储层次结构中,DRAM缓存的位置对它的组织结构有很大的影响。
因为对磁盘的访问时间很长,DRAM缓存总是使用写回,而不是直写。
9.3.2 页表
这些功能是由许多软硬件联合提供的,包括操作系统软件、MMU(存储器管理单元)中的地址翻译硬件和一个存放在物理存储器中叫做页表的数据结构,页表将虚拟页映射到物理页。
页表就是一个页表条目的数组。
9.3.3 页命中
9.3.4 缺页
在虚拟存储器的习惯说法中,DRAM缓存不命中称为缺页。
缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页。
9.3.5 分配页面
当操作系统分配一个新的虚拟存储器页时对我们示例页表的影响。
9.3.6 又是局部性救了我们
尽管在整个运行过程中程序引用的不同页面的总数可能超出物理存储器总的大小,但是局部性原则保证了在任意时刻,程序将往往在一个较小的活动页面集合上工作,这个集合叫做工作集或者常驻集。
如果工作集的大小超出了物理存储器的大小,那么程序将产生一种不幸的状态,叫做颠簸这时页面将不断地换进换出。
9.4 虚拟存储器作为存储器管理的工具
按需页面调度和独立的虚拟地址空间的结合,对系统中存储器的使用和管理造成了深远的影响。VM简化了链接和加载、代码和数据共享,以及应用程序的存储器分配。
- 简化链接
- 简化加载
- 简化共享
- 简化存储器分配
9.5 虚拟存储器作为存储器保护的工具
任何现代计算机系统必须为操作系统提供手段来控制对存储器系统的访问。不应该允许一个用户进程修改它的只读文本段。而且也不应该允许它读或修改任何内核中的代码和数据结构。不应该允许它读或者写其他进程的私有存储器,并且不允许它修致任何与其他进程共享的虚拟页面,除非所有的共享者都显式地允许它这么做(通过调用明确的进程间通信系统调用)。
提供独立的地址空间使得分离不同进程的私有存储器变得容易。但是,地址翻译机制可以以一种自然的方式扩展到提供更好的访问控制。
9.6 地址翻译
当页面命中时,CPU硬件执行的步骤
- 第一步:处理器生成一个虚拟地址并把它传送给MMU
- 第二步:MMU生成PTE地址,并从高速缓存/主存请求得到它
- 第三步:高速缓存/主存向MMU返回PTE
- 第四步:MMU构造物理地址并把它传送给高速缓存/主存
- 第五步:高速缓存/主存返回所请求的数据字给处理器。
页面命中完全是由硬件来处理的,与之不同的是,处理缺页要求硬件和操作系统内核协作完成。
- 第一步到第三步:和图中的第一步到第三步相同;
- 第四步:PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
- 第五步:缺页处理程序确定出物理存储器中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。
- 第六步:缺页处理程序页面调入新的页面,并更新存储器中的PM。
- 第七步:缺页处理程序返虚拟地址重新发送给MMU。因为虚拟页面现在缓回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的现在缓存在物理存储器中,所以就会命中,在MMU执行了图中的步骤之后,主存就会将所请求字返回给处理器。
9.6.1 结合高速缓存和虚拟存储器
在任何既使用虚拟存储器又使用SRAM高速缓存的系统中,都存在应该使用虚拟地址还是使用物理地址来访问SRAM高速缓存的问题,但是大多数系统是选择物理寻址的。使用物理寻址L多个进程同时在高速缓存中有存储块和共享来自相同虚拟页面的块成为很简单的事情。而且,高速缓存无需处理保护问题,因为访问权限的检查是地址翻译过程的一部分。
9.6.2 利用TLB加速地址翻译
如果PTE正碰巧缓存在L1中,那么开销就下降到1个或2个周期。然而,许多系统都试图消除这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓冲器。
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联性。如果TLB有T=2的t次方个组,那么TLB索引(T田1)是由VPN的t个最低位组成的,而TLB标记是由VPN中剩余的位组成的。
9.6.3 多级页表
实际上,带多级页表的地址翻译并不比但级页表慢很多。
9.6.4 综合:端到端的地址翻译
为了保证可管理性,我们做出如下假设:
- 存储器是按字节寻址的。
- 存储器访问是真对1字节的字的。
- 虚拟地址是14位长的。
- 物理地址是12位长的。
- 页面大小是64字节。
- TLB是四路组相联的,总共有16个条目。
- L1 d-cache是物理寻址、直接映射的,行大小为4字节,而总共有16个组。
- TLB是利用VPN的位进行虚拟寻址的。
- 页表。这个页表是一个单级设计,一共有256个页表条目(PTE)。
- 高速缓存。直接映射的缓存是通过物理地址中的字段来寻址的。
9.7 案例研究:intel core i7/linux存储器系统
处理器包包括四个核、一个大的所有核共享的L3高速缓存,以及一个DDR3存储器控制器。
9.7.1 core i7地址翻译
PTE有三个权限位,控制对页的访问。R/W位确定页的内容是可以读写的还是只读的。U/S位确定是否能够在用户模式中访问该页,从而保护操作系统内核中的代码和数据不被用户程序访问。XD(禁止执行)位是在64位系统中引入的,可以用来禁止从某些存储器页取指令。
每次访问一个页时,MMU都会设置A位,称为引用位。内核可以用这个引用位来实现它的页替换算法。每次对一个页进行了写之后,MMU都会设置D位,又称脏位。脏位告诉内核在拷贝替换页之前是否必须写回牺牲页。
9.7.2 linux虚拟存储器系统
一个虚拟存储器系统要求硬件和内核软件之间的紧密协作。
内核虚拟存储器包含内核中的代码和数据结构。
1.Linux虚拟存储区域
Linux将虚拟存储器组织成一些区域的集合。一个区域就是已经存在着的虚拟存储器的连续片,这些页是以某种方式相关联的。
2.Linux缺页异常处理
- 虚拟地址A是合法的吗?
- 试图进行的存储器访问是合法的吗?
- 选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。
9.8 存储器映射
Linux通过将一个虚拟存储器区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容,这个过程称为存储器映射。
虚拟存储器区域可以映射到两种类型的对象的一种:
- Unix文件上的普通文件。
- 匿名文件。映射到匿名文件的区域中的页面有时也叫做请求二进制零的页(demand-zero page)。
无论在哪种情况下,一旦一个虚拟页面被初始化了, 它就在一个由内核维护的专门的交换文件(swap file)之间换来换去。交换文件也叫做交换空间(swap space)或者交换区域(swap area)。
9.8.1 再看共享对象
一个对象可以被映射到虚拟存储的一个区域,要么作为共享对象,要么作为私有对象。
一个映射到共享对象的虚拟存储器区域叫做共享区域。类似地,也有私有区域。
私有对象是使用一种叫做写时拷贝(copy-on-write)的巧妙技术被映射到虚拟存储器中的。
9.8.2 再看fork函数
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。
9.8.3 再看execve函数
假设运行在当前进程中的程序执行了如下的调用:
- Execve("a.out",NULL,NULL) ;
加载并运行a.out需要以下几个步骤:
- 删除已存在的用户区域。
- 映射私有区域。
- 映射共享区域。
- 设置程序计数器(PC)。
9.8.4 使用mmap函数的用户级存储器映射
连续的对象片大小为length字节,从距文件开始处偏移量为offset字节的地方开始。start地址仅仅是一个暗示,通常被定义为NULL。
参数prot包含描述新映射的虚拟存储器区域的访问权限位:
- PROT_EXEC:由可以被CPU执行的指令组成
- PROT_READ:可读
- PROT_WRITE:可写
- PROT_NONE:不能被访问
9.9 动态存储器分配
动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟存储器片,要么是已分配的,要么是空闲的。
分配器有两种基本风格。
- 显示分配器
- 隐式分配器。
9.9.1 malloc和free函数
C标准库提供了一个称为malloc程序包的显式分配器。程序通过调用malloc函数来从对中分配块。
动态存储器分配器,例如malloc,可以通过使用mmap和munmap函数,显式地分配和释放堆存储器,或者还可以使用sbrk函数。
9.9.2 为什么要使用动态存储器分配
9.9.3 分配器的要求和目标
显式分配器必须在一些相当严格的约束条件下工作:
- 处理任意请求序列
- 立即响应请求
- 只使用堆
- 对齐块(对齐要求)
- 不修改已分配的块
- 目标1:最大化的吞吐率
- 目标2:最大化存储器利用率
9.9.4 碎片
造成堆利用率很低的主要原因是一种称为碎片的现象,有两种形式的碎片:内部碎片和外部碎片。
- 内部碎片是在一个已分配块比有效载荷大时发生的。
- 外部碎片是当空闲存储器合计起来足够满足一个分配要求,但是没有一个单独的空闲块足够大可以处理这个请求时发生的。
9.9.5 实现问题
一个实际的分配器要在吞吐率和利用率之间把握好平衡,就必须考虑好一下几个问题:
- 空闲块组织
- 放置
- 分割
- 合并
9.9.6 隐式空闲链表
隐式空闲链表是因为空闲块是通过头部的大小字段隐含地连接着的。
9.9.7 放置已分配的块
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大可以放置所请求块的空闲块,执行这种搜索的方式由放置策略确定的。
- 首次分配从头开始搜索空闲链表,选择第一个合适的空闲块。
- 下一次适配和首次适配很相似,只不过不是从链表的起始处开始每次搜索,而是从上一次查询结束的地方开始。
- 最佳适配检查每个空闲块,选择适合所请求大小的最小空闲块。
9.9.8 分割空闲块
9.9.9 获取额外的堆存储器
9.9.10 合并空闲块
这些邻接的空闲块可能引起的一种现象,叫做假碎片,就是有许多可用的空闲块被切割成小的、无法使用的空闲块。
为了解决假碎片问题,任何实际的分配器都必须合并相邻的空闲块,这个过程称为合并。分配器可以选择立即合并以选择推迟合并。也就是等到某个稍晚的时候再合并空闲块。
9.9.11 带边界标记的合并
想要释放的块称为当前块。
边界标记
考虑当分配器释放当前块时所有可能存在的情况:
- 前面的块和后面的块都是已分配的。
- 前面的块是已分配的,后面的块是空闲的
- 前面的块是空闲的,而后面的块是已分配的。
- 前面的和后面的块都是空闲的。
-
9.9.12 综合:实现一个简单的分配器
- 一般分配器设计
- 操作空闲链表的基本常数和宏
- 创建初始空闲链表
- 释放和合并块
- 分配块
9.9.13 显式空闲链表
一种方法是用后进先出的顺序维护链表,将新释放的块放置在链表的开始处。
另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。
9.9.14 分离的空闲链表
1.简单分离存储
优点:分配和释放块都是很快的常数时间操作。
缺点:简单分离存储很容易造成内部和外部碎片。
2.分离适配
3.伙伴系统
优点:快速搜索和快速合并。
缺点:可能导致显著的内部碎片。
9.10 垃圾收集
垃圾收集器是一种动态存储分配器,它自动释放程序不再需要的已分配块。这些块称为垃圾。自动回收堆存储的过程叫做垃圾收集。
9.10.1 垃圾收集器的基本知识
垃圾收集器将存储器视为一张有向可达图,该图的节点被分成一组根节点和一组堆节点。
当存在一条从任意根节点出发并到达p的有向路径时,我们说节点p是可达的。
9.10.2 mark&sweep垃圾收集器
mark&sweep垃圾收集器由标记和清除阶段组成。
9.10.3 c程序的保守mark&sweep
9.11 c程序中常见的与存储器有关的错误
9.11.1 间接引用坏指针
在这种情况下,scanf将把val的内容解释为一个地址,并试图将一个字写到这个位置。在最好的情况下,程序立即以异常中止。在最糟糕的情况下,val的内容对应于虚拟存储器的某合法的读/写区域,于是我们就覆盖了存储器,这通常会在相当长的段时间以后造成灾难性的、令人困惑的后果。
9.11.2 读未初始化的存储器
一个常见的错误就是假设堆存储器被初始化为零:
9.11.3 允许栈缓冲区溢出
如果一个程序不检查输入串的大小就写入栈中的目标缓冲区,那么这个程序就会有缓冲区溢出错误。
使用fgets()纠正错误,这个函数限制了输入串的大小。
9.11.4 假设指针和它们指向的对象是相同大小的
一种常见的错误是假设指向对象的指针和他们所指向的对象是大小相同的。
9.11.5 造成错位错误
错位错误是一种很常见的覆盖错误来源。
9.11.6 引用指针,而不是它所指向的对象
9.11.7 误解指针运算
另一种常见的错误是忘记了指针的算术操作是以它们指向的对象的大小为单位来进的,而这种大小单位并不一定是字节。
9.11.8 引用不存在的变量
不理解栈的规则,有时会引用不再合法的本地变量。
9.11.9 引用空闲堆块中的数据
一个相似的错误是引用已经被释放了的堆块中的数据。
9.11.10 引起存储器泄漏
存储器泄露是缓慢隐形的杀手。
学习心得
本周通过学习虚拟存储器的相关知识,对于计算机系统中存储器部分又有了更深一步的了解。虚拟存储器作为现代操作系统中不可或缺的一部分,贡献着它自己的作用。学期接近尾声,计算机存储器这一部分的知识也已经基本学习完毕,在学习这一部分知识的时候,由于虚拟存储器的特性导致一些概念需要一定的时间琢磨,除了课本上的知识,互联网上也有一些关于虚拟存储器的知识值得我们去学习。
《深入理解计算机系统》这本书已经差不多要学习完毕了,是不是会反思自己,书的确是看完了,博客也写完了。可是知识呢?技能呢?是否都掌握了?想了想,的确又些知识掌握的不牢固,有些技能不熟练,这都在平时的测验中暴露出来,希望自己往后能够将知识、技能掌握得更加牢固一点,这样才能不枉费自己用在学习上的大量时间。
托管链接
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 0/0 | 1/2 | 15/30 | |
第二周 | 56 /56 | 2/3 | 15/45 | |
第三周 | 89/145 | 1/4 | 20/65 | |
第五周 | 500/645 | 1/5 | 20/85 | |
第六周 | 150/795 | 1/6 | 20/105 | |
第七周 | 124/919 | 1/7 | 20/125 | |
第八周 | 0/919 | 1/8 | 15/140 | |
第九周 | 98/1017 | 1/9 | 15/155 | |
第十周 | 448/1465 | 1/10 | 20/175 | |
第十一周 | 634/2099 | 1/11 | 25/200 | |
第十二周 | 0/2099 | 7/18 | 25/225 | |
第十三周 | 681/2431 | 2/20 | 25/250 | |
第十四周 | 224/2655 | 1/21 | 26/226 |