20145327 《信息安全系统设计基础》第十四周学习总结

20145327 《信息安全系统设计基础》第十四周学习总结

教材学习内容总结

第9章 虚拟存储器

9.1 物理和虚拟寻址

计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每宇节都有一个唯一的物理地址 (Physical Address, PA)。第一个字节的地址为 0,接下来的字节地址为 1,再下一个为 2, CPU 访问存储器的最自然的方式就是使用物理地址。我们把这种方式称为物理寻址 (physical addressing)。
早期的 PC 使用物理寻址,现代处理器使用的是一种称为虚拟寻址 (virtual addressing) 的寻址形式。

一个使用物理寻址的系统:

一个使用虚拟寻址的系统:

使用虚拟寻址时, CPU 通过生成一个虚拟地址 (Virtual Address, VA) 来访问主存,这个虚拟地址在被送到存储器之前先转换成适当的物理地址。将一个虚拟地址转换为物理地址的任务叫做地址翻译 (address translation)。地址翻译需要 CPU 硬件和操作系统之间的紧密合作。 CPU 芯片上叫做存储器管理单元 (Memory Management Unit, MMU) 的专用硬件, 利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容是由操作系统管理的。

9.2 地址空间

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

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

在一个带虚拟存储器的系统中, CPU 从一个有N=2n次方个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间(virtual address space)

一个地址空间的大小是由表示最大地址所需要的位数来描述的。
一个系统还有一个物理地批空间 (physical address space),它与系统中物理存储器的M个字节相对应。

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

概念上而言,虚拟存储器 (VM) 被组织为一个由存放在磁盘上的 N个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,这个唯一的虚拟地址是作为到数组的索引的。磁盘上数组的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘〈较低层)上的数据被分割成块,这些块作为磁盘和主存(较高层〉之间的传输单元。 VM 系统通过将虚拟存储器分割为称为虚拟页(Virtual Page, VP) 的大小固定的块来处理这个问题。每个虚拟页的大小为 P=2P次方 字节。类似地,物理存储器被分割为物理页 (Physical Page, PP ) ,大小也为 P 字节(物理页也称为页帧 (page frame) )。

在任意时刻,虚拟页面的集合都分为三个不相交的子集 :

  • 未分配的 :VM 系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间。
  • 缓存的:当前缓存在物理存储器中的己分配页。
  • 未缓存的 : 没有缓存在物理存储器中的已分配页。

一个 VM 系统是如何使用主存作为缓存的:

9.3.1 DRAM 缓存的组织结构

SRAM 缓存位于 CPU 和主存之间的 Ll、 L2 和 L3 高速缓存。
DRAM 缓存来表示虚拟存储器系统的缓存,它在主存中缓存虚拟页。

在存储层次结构中, DRAM 缓存的位置对它的组织结构有很大的影响。
DRAM 缓存中的不命中比起 SRAM 缓存中的不命中要昂贵得多,因为 DRAM 缓存不命中要由磁盘来服务,而 SRAM 缓存不命中通常是由基于 DRAM 的主存来服务的。
DRAM 缓存的组织结构完全是由巨大的不命中开销驱动的。

9.3.2 页表

由许多软硬件联合提供的,包括操作系统软件、 MMU (存储器管理单元)中的 地址翻译硬件和一个存放在物理存储器中叫做页在 (page table) 的数据结构,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时都会读取页表。操作系统负责维护页表的内容,以及在磁盘与 DRAM 之间来回传送页。
页表就是一个页在条目 (page table entry,PTE) 的数组。虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个PTE。
有效位表明了该虚拟页当前是否被缓存在 DRAM 中。如果设置了有效位,那么地址宇段就表示 DRAM 中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。否则,这个地址就指向该虚拟页在磁盘上的起始位置。
因为 DRAM 缓存是全相连的,任意物理页都可以包含任意虚拟页。

9.3.3 页命中

VM 页命中:

9.3.4 缺页

DRAM 缓存不命中称为缺页 (page fault)。

缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,无论哪种情况,内核都会修改 VP4 的页表条目,反映出 VP4 不再缓存在主存中这一事实。

习惯说法中,块被称为页。在磁盘和存储器之间传送页的活动叫做交换 (swapping) 或者页面调度 (paging)。页从磁盘换入(或者页面调入) DRAM 和从 DRAM 换出(或者页面调出)磁盘。一直等待,直到最后时刻,也就是当有不命中发生时,才换入页面的这种策略称为按需页面调度 (demand paging)。

所有现代系统都使用的是按需页面调度的方式。

VM 缺页〈之前〉:

VM 缺页(之后〉:

9.3.6 又是局部性救了我们

虚拟存储器工作得相当好,这主要归功于局部性 (locality)。
整个运行过程中程序引用的不同页面的总数可能超出物理存储器总的大小,但是局部性原则保证了在任意时刻,程序将往往在一个较小的活动页面 (active page) 集合上工作,这个集合叫做工作集 (working set) 或者常驻集 (resident set)。
只要我们的程序有好的时间局部性,虚拟存储器系统就能工作得相当好。
如果工作集的大小超出了物理存储器的大小,那么程序将产生一种不幸的状态,叫做颠簸(thrashing ) ,这时页面将不断地换进换出。

分配一个新的虚拟页面:

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

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

  • 简化链接。
  • 简化加载。
    一组连续的虚拟页映射到任意一个文件中的任意位置的表示法称做存储器映射 (memory mapping). Unix 提供一个称为 mmap 的系统调用,允许应用程序自己做存储器映射。
  • 简化共享.
  • 简化存储器分配。虚拟存储器为向用户进程提供一个简单的分配额外存储器的机制。

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

提供独立的地址空间使得分离不同进程的私有存储器变得容易。但是,地址翻译机制可以以一种自然的方式扩展到提供更好的访问控制。因为每次 CPU 生成一个地址时,地址翻译硬件都会读一个 PTE,所以通过在 PTE 上添加一些额外的许可位来控制对一个虚拟页面内容的访问十分简单。
SUP 位表示进程是否必须运行在内核(超级用户)模式下才能访问该页。运行在内核模式中的进程可以访问任何页面,但是运行在用户模式中的进程只允许访问那些 SUP 为 0 的页面。 READ 位和 WRITE 位控制对页面的读和写访 问。

用虚拟存储器来提供页面级的存储器保护:

9.6 地址翻译

地址翻译符号小结:

n 位的虚拟地址包含两个部分:一 个p 位的虚拟页面偏移 (Virtual Page Offset, VPO) 和一个 (n-p) 位的虚拟页号 (Virtual Page Number, VPN). MMU 利用 VPN 来选择适当的 PTE。
将页表条目中物理页号 (Physical Page Number, PPN) 和虚拟地址中的 VPO 串联起来,就得到相应的物理地址。注意,因为物理和虚拟页面都是P 字节的,所以物理页面偏移 (Physical Page Offset, PPO) 和 VPO 是相同的。

使用页表的地址翻译:

页面命中和缺页的操作视图:

VA: 虚拟地址。 PTEA: 页表条目地址。 PTE: 页表条目。 PA: 物理地址

9.6.1 结合高速缓存和虚拟存储器

使用物理寻址,多个进程同时在高速缓存中有存储块和共享来自相同虚拟页面的块成为很简单的事情。而且,高速缓存无需处理保护问题,因为访问权限的检查是地址翻译过程的一部分。

将 VM 与物理寻址的高速缓存结合起来:

VA: 虚拟地址。 PTEA: 页表条目地址。 PTE: 页表条目。 PA: 物理地址

9.6.2 利用 TLB 加速地址翻译

MMU 中包括了一个关于 PTE 的小的缓存,称为翻译后备缓冲器 (Translation Lookaside Buffer, TLB)。
TLB 是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个 PTE 组成的块。 TLB 通常有高度的相连性。

一个用来访问 TLB 的虚拟地址的组成部分:

TLB 命中和不命中的操作视图:

9.6.3 多级页表

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

一个两级页表层次结构。 注意地址是从上往下增加的:

这种方法从两个方面减少了存储器要求。

  1. 如果一级页表中的一个 PTE 是空的,那么相应的二级页表就根本不会存在,这代表着一种巨大的潜在节约,因为对于一个典型的程序, 4GB 的虚拟地址空间的大部分都将是未分配的。
  2. 只有一级页表才需要总是在主存中:虚拟存储器系统可以在需要时创建、页面调入或调出二级页表,这就减少了主存的压力:只有最经常使用的二级页表才需要缓存在主存中。

带多级页表的地址翻译并不比单级页表慢很多。

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

小存储器系统的寻址:

小存储器系统的 TLB、页表以及缓存:

  • TLB。TLB 是利用 VPN 的位进行虚拟寻址的。因为 TLB 有四个组,所以 VPN 的低两位就作为组索引 (TLBI). VPN 中剩下的高 6 位作为标记 (TLBT),用来区别可能映射到同一 个 TLB 组的不同的 VPN。
  • 页表。这个页表是一个单级设计,一共有 2的8此昂=256 个页表条目 (PTE)。为了方便,我们用索引它的 VPN 来标识每个 PTE; 但是要记住这些 VPN 并不是页表的一部分,也不储存在存储器中。另外,注意每个无效 PTE 的PPN都用一个破折号来表示,以加强一个概念:无论刚好这里存储的是什么位值,都是没有任何意义的。
  • 高速缓存。直接映射的缓存是通过物理地址中的字段来寻址的。因为每个块都是 4 字节, 所以物理地址的低 2 位作为块偏移 (CO)。因为有 16 组,所以接下来的 4 位就用来表示组索引 (CI)。剩下的 6 位作为标记 (CT)。

Linux 虚拟存储器系统

一个虚拟存储器系统要求硬件和内核软件之间的紧密协作。
内核虚拟存储器包含内核中的代码和数据结构。内核虚拟存储器的某些区域被映射到所有进程共享的物理页面。
内核虚拟存储器的其他区域包含每个进程都不相同的数据。

一个 Linux 进程的虚拟存储器:

1. Linux 虚拟存储器区域

Linux 将虚拟存储器组织成一些区域(也叫做段〉的集合。一个区域(area) 就是已经存在着的(已分配的)虚拟存储器的连续片(chunk),这些页是以某种方式相关联的。

Linux 是如何组织虚拟存储器的:

task_struct 中的一个条目指向mm_struct,它描述了虚拟存储器的当前状态。其中 pgd 指向第一级页表(页全局目录)的基址,而 mmap 指向一个 vm-area-structs(区域结构)的链表,其中每个 vm-area-structs 都描述了当前虚拟地址空间的一个区域 (area)。当内核运行这个进程时,它就将 pgd 存放在 CR3控制寄存器中。

  • vm_start: 指向这个区域的起始处。
  • vm_end: 指向这个区域的结束处。
  • vm_prot: 描述这个区域内包含的所有页的读写许可权限。
  • vm_flags :描述这个区域内的页面是与其他进程共享的,还是这个进程私有的(还描述了其他一些信息)。
  • vm_next: 指向链表中下一个区域结构。

2. Linux 缺页异常处理

Linux 缺页处理:

9.8 存储器映射

Linux (以及其他一些形式的 Unix) 通过将一个虚拟存储器区域与一个磁盘上的对象 (object) 关联起来,以初始化这个虚拟存储器区域的内容,这个过程称为存储器映射 (memory mapping)。

  1. Unix 文件系统中的普通文件:一个区域可以映射到一个普通磁盘文件的连续部分。文件区 (sectÎon) 被分成页大小的片,每一片包含一个虚拟页面的初始内容。因为按需进行页面调度,所以这些虚拟页面没有实际交换进入物理存储器,直到 CPU 第一 次引用到页面(即发射一个虚拟地址,落在地址空间这个页面的范围之内)。如果区域比文件区要大,那么就用零来填充这个区域的余下部分。
  2. 匿名文件:一个区域也可以映射到一个匿名文件,匿名文件是由内核创建的,包含的全是二进制零。 CPU 第一次引用这样一个区域内的虚拟页面时,内核就在物理存储器中找到一个 合适的牺牲页面,如果该页面被修改过,就将这个页面换出来,用二进制零覆盖牺牲页面并更新页表,将这个页面标记为是驻留在存储器中的。注意在磁盘和存储器之间并没有实际的数据传送。因为这个原因,映射到匿名文件的区域中的页面有时也叫做请求二进制零的页 (demand­ zeropage)。
    一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件 (swap file) 之间换来换去。交换文件也叫做交换空间 (swap space) 或者交换区域 (swap area)。在任何时刻,交换空间都限制着当前运行着的进程能够 分配的虚拟页面的总数。

fork 函数

当 fork 函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的 PID。为了给这个新进程创建虚拟存储器,它创建了当前进程的 mm_struct、区域结构和页表的原样拷贝。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时拷贝。
当 fork 在新进程中返回时,新进程现在的虚拟存储器刚好和调用 fork时存在的虚拟存储器相同。当这两个进程中的任一个后来进行写操作时,写时拷贝机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

execve 函数

execve 函数在当前进程中加载并运行包含在可执行目标文件 a.out 中的程序,用 a.out 程序有效地替代了当前程序。加载并运行 a.out 需要以下几个步骤:

  • 删除已存在的用户区域。
  • 映射私有区域。
  • 映射共享区域。
  • 设置程序计数器 (PC)。

加载器是如何映射用户地址空间的区域的:

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

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

mmap 函数要求内核创建一个新的虚拟存储器区域,最好是从地址 start 开始的一个区域, 并将文件描述符 fd 指定的对象的一个连续的片 (chunk) 映射到这个新的区域。连续的对象片大小为 length 宇节,从距文件开始处偏移量为offset 字节的地方开始。 start 地址仅仅是一个暗示,通常被定义为 NULL。

mmap 参数的可视化解释:

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

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

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

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

munmap 函数删除从虚拟地址 start 开始的,由接下来 length 字节组成的区域。接下来对已删除区域的引用会导致段错误。

9.9 动态存储器分配

当运行时需要额外虚拟存储器时,用动态存储器分配器 (dynamic memory allocator) 更方便,也有更好的可移植性。
动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆 (heap)
分配器将堆视为一组不同大小的块 (block) 的 集合来维护。每个块就是一个连续的虚拟存储器片 (chunk),要么是已分配的,要么是空闲的。己分配的 块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是存储器分配器自身隐式执行的。

堆:

分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放己分配的块。

  • 显式分配器 (explicit allocator),要求应用显式地 释放任何已分配的块。
  • 隐式分配器 (implicit allocator),另一方面,要求分配器检测一个已分配块何时不再被程序 所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器 (garbage collector),而自动释放未使用的己分配的块的过程叫做垃圾收集 (garbage collection)。

malloc 和 free 函数

调用 malloc 函数来从堆中分配块:

malloc 函数返回一个指针,指向大小为至少 size 字节的存储器块,这个块会为可能包含在这个块内的任何数据对象类型做对齐。Unix 系统上, malloc 返回一个 8 字节 (双宇)边界对齐的块。

想要改变一个以前已分配块的大小,可以使用 realloc 函数。

动态存储器分配器,可以通过使用 mmap 和 munmap 函数,显式地分配和释 放堆存储器,或者还可以使用 sbrk 函数:

sbrk 函数通过将内核的 brk 指针增加 incr来扩展和收缩堆。如果成功,它就返回 brk 的旧值,否则,它就返回一1,并将 errno设置为 ENOMEM。如果 incr 为零,那么 sbrk 就返回 brk 的当前值。用一个为负的 incr 来调用 sbrk 是合法的,而且很巧妙,因为返回值 (brk 的旧值)指向距新堆顶向上 abs (incr) 字节处。

程序是通过调用 free 函数来释放己分配的堆块:

ptr 参数必须指向一个从 malloc、 calloc 或者 realloc 获得的已分配块的起始位置。它什么都不返回, free 就不会告诉应 用出现了错误。

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

程序使用动态存储器分配的最重要的原因是经常直到程序实际运行时,它们才知道某些数据结构的大小。
最简单的方法就是用某种硬编码的最大数组大小静态地定义这个数组。
一种更好的方法是在运行时,在己知了 n 的值之后,动态地分配这个数组。使用这种方法,数组大小的最大值就只由可用的虚拟存储器数量来限制了。

9.9.3 分配器的要求和目标

显式分配器必须在一些相当严格的约束条件下工作:

  • 处理任意请求序列是由一个以前的分配请求获得的。
  • 立即响应请求。分配器必须立即响应分配请求。
  • 只使用堆。
  • 对齐块(对齐要求)。
  • 不修改已分配的块

实现吞吐率最大化和存储器使用率最大化,而这两个性能目标通常是相互冲突的。

最有用的标准是峰佳利用率 (peak utilization)。

分配器的目标就是在整个序列中使峰值利用率 Un-1 最大化。
最大化吞吐率和最大化利用率之间是互相牵制的。
以堆利用率为代价,很容易编写出吞吐率最大化的分配器。
在两个目标之间找到一个适当的平衡。

9.9.4 碎片

造成堆利用率很低的主要原因是一种称为碎片 (fragmentation) 的现象,当虽然有未使用的存储器但不能用来满足分配请求时,就会发生这种现象。有两种形式的碎片: 内部碎片 (intemal fragmentation) 和外部碎片(extemal fragmentation)。

内部碎片是在一个已分配块比有效载荷大时发生的。就是已分配块大小和它们的有效载荷大小之差的和。因此,在任意时刻,内部碎片的数量只取决于以前请求的模式和分配器的实现方式。

外部碎片是当空闲存储器合计起来足够满足一个分配请求,但是没有一个单独的空闲块足够大可以来处理这个请求时发生的。

外部碎片比内部碎片的量化要困难得多,因为它不仅取决于以前请求的模式和分配器的实现方式,还取决于将来请求的模式。

因为外部碎片难以量化且不可能预测,所以分配器通常采用启发式策略来试图维持少量的大空闲块,而不是维持大量的小空闲块。

9.9.5 实现问题

一个实际的分配器要在吞吐率和利用率之间把握好平衡,就必须考虑以下几个问题:

  • 空闲块组织:我们如何记录空闲块?
  • 放直:我们如何选择一个合适的空闲块来放置一个新分配的块?
  • 分割:在我们将一个新分配的块放置到某个空闲块之后,我们如何处理这个空闲块中的剩余部分?
  • 合并:我们如何处理一个刚刚被释放的块?

1. 简单分离存储

使用简单分离存储,每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小。

如果链表非空,我们简单地分配其中第一块的全部。空闲块是不会分割以满足分配请求的。如果链表为空,分配器就向操作系统请求一个固定大小的额外存储器片(典型地是页大小的整数倍),将这个片分成大小相等的块,并 将这些块链接起来形成新的空闲链表。要释放一个块,分配器只要简单地将这个块插入到相应的空闲链表的前部。

分配和释放块都是很快的常数时间操作。而且,每个片中都是大小相等的块,不分割,不合并,这意味着每个块只有很少的存储器开销。既然每个片只有大小相同的块,那么一个已分配块的大小就可以从它的地址中推断出来。因为没有合并,所以已分配块的头部就不需要一个已分配的/空闲标记。因此已分配块不需要头部,同时因为没有合并,它们也不需要脚部。因为分配和释放操作都是在空闲链表的起始处操作,所以链表只需要是单向的,而不用是双向的。关键点在于,在任何块中都需要的唯一字段是每个空闲块中的一个字的 succ 指针,因此最小块大小就是一个字。

一个显著的缺点是,简单分离存储很容易造成内部和外部碎片。因为空闲块是不会被分割的,所以可能会造成内部碎片。更糟的是,因为不会合并空闲块,所以某些引用模式会引起极多的外部碎片。

2. 分离适配

每个空闲链表是和一个大小类相关联的,并且被组织成某种类型的显式或隐式链表。每个链表包含潜在的大小不同的块,这些块的大小是大小类的成员。有许多种不同的分离适配分配器。

为了分配一个块,我们必须确定请求的大小类,并且对适当的空闲链表做首次适配,查找一个合适的块。如果我们找到了一个,那么我们〈可选地)分割它,并将剩余的部分插入到适当的空闲链表中。如果我们找不到合适的块,那么就搜索下一个更大的大小类的空闲链表。如此重复,直到找到一个合适的块。如果空闲链表中没有合适的块,那么我们就向操作系统请求额外的 堆存储器,从这个新的堆存储器中分配出一个块,将剩余部分放置在适当的大小类中。要释放一个块,我们执行合并,并将结果放置到相应的空闲链表中。

3. 伙伴系统

伙伴系统 (buddy system) 是分离适配的一种特例,其中每个大小类都是 2 的幂。

伙伴系统的一个关键事实是,给定地址和块的大小,很容易计算出它的伙伴的地址。

一个块的地址和它的伙伴的地址只有一位不相同。

伙伴系统分配器的主要优点是它的快速搜索和快速合并。主要缺点是要求块大小为 2 的幂可能导致显著的内部碎片。因此,伙伴系统先配器不适合通用目的的工作负载。

9.10 垃圾收集

应用通过调用 malloc 和 free 来分配和释放堆块。应用要负责释放所有不再需要的已分配块。

垃圾收集器 (garbage collector) 是一种动态存储分配器,它自动释放程序不再需要的己分配 块。这些块称为垃圾 (garbage) 。

自动回收堆存储的过程叫做垃圾收集 (garbage collection)。在一个支持垃圾收集的系统中,应用显式分配堆块,但是从不显示地释放它们。在 C 程序的上下文中,应用调用 malloc,但是从不调用 free。反之,垃圾收集器定期识别垃圾块,并相应地调用 free,将这些块放回到空闲链表中。

9.10.1 垃圾收集器的基本知识

垃圾收集器将存储器视为一张有向可达图 (reachability graph) 被分成一组根节点 (root node) 和一组堆节点 (heap node)。

垃圾收集器将存储器视为一张有向图:

当存在一条从任意根节点出发并到达p 的有向路径时,我们说节点p 是可达的 (reachable)。在任何时刻,不可达节点对应于垃圾,是不能被应用再次使用的。垃圾收集器的角色是维护可达图的某种表示,并通过释放不可达节点并将它们返回给空闲链表,来定期地回收它们。

通常不能维持可达图的精确表示。这样的收集器也叫做保守的垃圾收集器 (conservative garbage collector)。它们是保守的,即每个可达块都被正确地标 记为可达了,而一些不可达节点却可能被错误地标记为可达。

收集器可以按需提供它们的服务,或者它们可以作为一个和应用并行的独立线程,不断地更新可达图和回收垃圾。

无论何时需要堆空间,应用都会用通常的方式调用 malloc。如果 malloc 找不到一个合适的空闲块,那么它就调用垃圾收集器,希望能够回收一些垃圾到空闲链表。收集器识别出垃圾块,并通过调用 free 函数将它们返回给堆。关键的思想是收集器代替应用去调用 free。当对 收集器的调用返回时, malloc 重试,试图发现一个合造的空闲块。如果还是失败了,那么它就会向操作系统要求额外的存储器。最后, malloc 返回一个指向请求块的指针(如果成功〉或者返回一个空指针(如果不成功〉。

9.10.2 Mark&Sweep 垃圾收集器

Mark&Sweep垃圾收集器由标记 (mark) 阶段和清除 (sweep) 阶段组成,标记阶段标记出根节点的所有可达的和已分配的后继,而后面的清除阶段释放每个未被标记的已分配块。

ptr 定义为 typedef void*ptr.

  • ptr isPtr (ptr p) :如果 p 指向一个己分配块中的某个字,那么就返回一个指向这个 块的起始位置的指针 b。否则返回 NULL。
  • int blockMarked (ptr b) :如果已经标记了块 b,那么就返回 true。
  • int blockAllocated (ptr b) :如果块 b 是已分配的,那么就返回 true。
  • void markBlock (ptr b) :标记块 b。
  • int length (ptr b) :返回块 b 的以字为单位的长度〈不包括头部〉。
  • void unmarkBlock (ptr b) :将块b 的状态由已标记的改为未标记的.
  • ptr nextBlock (ptr b) :返回堆中块 b 的后继。

sweep 函数在堆中每个块上反复 循环,释放它所遇到的所有未标记的已分配块(也就是垃圾〉。

mark 和 sweep 函数的伪代码:

小结

虚拟存储器是对主存的一个抽象。支持虚拟存储器的处理器通过使用一种叫做虚拟寻址的间接形式来引用主存。处理器产生一个虚拟地址,在被发送到主存之前,这个地址被翻译成一个物理地址。从虚拟地址空间到物理地址空间的地址翻译要求硬件和软件紧密合作。专门的硬件通过使用页表来翻译虚拟地址,而页表的内容是由操作系统提供的。

虚拟存储器提供三个重要的功能:

  1. 它在主存中自动缓存最近使用的存放磁盘上的虚拟地址空间的内容。虚拟存储器缓存中的块叫做页。对磁盘上页的引用会触发缺页,缺页将控制转 移到操作系统中的一个缺页处理程序。缺页处理程序将页面从磁盘拷贝到主存缓存,如果必要, 将写回被驱逐的页。
  2. 虚拟存储器简化了存储器管理,进而又简化了链接、在进程间共享数 据、进程的存储器分配以及程序加载。
  3. 最后,虚拟存储器通过在每条页表条目中加人保护位,从而了简化了存储器保护。

地址翻译的过程必须和系统中所有的硬件缓存的操作集成在一起。大多数页表条目位于L1 高速缓存中,但是一个称为 TLB 的页表条目的片上高速缓存, 通常会消除访问在L1上的页表条目的开销。

现代系统通过将虚拟存储器片和磁盘上的文件片关联起来,以初始化虚拟存储器片,这个过程称为存储器映射。存储器映射为共享数据、创建新的进程以及加载程序提供了一种高效的机制。应用可以使用 mmap 函数来手工地创建和删除虚拟地址空间的区域。然而, 大多数程序依赖 于动态存储器分配器,例如 malloc,它管理虚拟地址空间区域内一个称为堆的区域。动态存储器分配器是一个感觉像系统级程序的应用级程序,它直接操作存储器,而无需类型系统的很多帮助。分配器有两种类型。显式分配器要求应用显式地释放它们的存储器块。隐式分配器(垃圾收集器)自动释放任何未使用的和不可达的块。

本周代码托管截图

附上周代码运行链接
代码托管链接

学习进度条

代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
目标 5000行 30篇 400小时
第一周 100/100 2/2 10/10
第二周 100/200 1/3 20/30
第三周 80/280 1/4 15/45
第五周 100/380 1/5 15/60
第六周 100/480 1/6 15/75
第七周 20/500 1/7 15/90
第八周 0/500 3/10 15/105
第九周 61/561 3/13 20/125
第十周 279/840 2/15 20/145
第十一周 419/1259 1/16 20/165
第十二周 14/1273 2/18 20/185
第十三周 1018/2291 2/19 20/205
第十四周 0/2291 1/20 15/220

参考资料

posted @ 2016-12-17 15:24  20145327高晨  阅读(254)  评论(0编辑  收藏  举报