深入理解计算机系统(第三版)第九章重要内容摘要

9.1物理和虚拟寻址

计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每字节都有唯一的一个物理地址,CPU访问内存最自然的方式就是使用物理地址。我们把这种方式叫做物理寻址。

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


 

9.2地址空间

地址空间(address space)是一个非负整数地址的有序集合。
如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间(linear address space)。为了简化讨论,我们总是假设使用的是线性地址空间。在一个带虚拟内存的系统中,CPU从一个有\(N=2^n\)个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间(virtual address space):

\[\{0,1,2,\cdots,N-1\} \]

  一个地址空间的大小是由表示最大地址所需要的位数来描述的。例如,一个包含\(N=2^n\)个地址的虚拟地址空间就叫做一个\(n\)位地址空间。现代系统通常支持32位或者64位虚拟地址空间。

  一个系统还有一个物理地址空间(physical address space),对应于系统中物理内存的\(M\)个字节:

\[\{0,1,2,\cdots,M-1\} \]

\(M\)不要求是2的幂,但是为了简化讨论,我们假设\(M=2^m\)

9.3虚拟内存作为缓存的工具


 
 

 

9.3.1DRAM缓存的组织结构


 
 

9.3.2页表



 
 
 
 页表就是一个页表条目(PTE)的数组,每个PTE都由一个有效位和一个n位地址字段组成

 
 

9.3.3页命中


 
 

9.3.4缺页


 
 

 
 

 
 
 如果VP4已经被修改,那么内核就会将它复制回磁盘

9.3.5分配页面


 
 

9.3.6又是局部性救了我们


 
 

 
 

9.4虚拟内存作为管理内存的工具


 
 

 
 

 
 

9.5虚拟内存作为内存保护的工具

 

 
 

 
 

9.6地址翻译


 
 

 
 

 
 

 
 

 
 

 
 

 
 

9.6.1结合高速缓存和虚拟内存


 
 

9.6.2利用TLB加速地址翻译

PTE的一个小的缓存,称为翻译后备缓冲器(Translation Lookaside Buffer,TLB),其中每一行保存一个由单个PTE组成的块。

 
 

 
 

 
 

 
 

9.6.3多级页表


 
 

 
 

 
 

 
 

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


 
 

 
 

 

 
 

 

 
 

 
 

9.7案例研究:Intel Core i7/Linux内存系统

9.7.1Core i7地址翻译


 
 

 
 

 
 

 
 

9.7.2Linux虚拟内存系统


 
 

 1.Linux虚拟内存区域


 
 

 
 

 
 

 2.Linux缺页异常处理


 
 

9.8内存映射


 
 

 
 

9.8.1再看共享对象


 

 

9.8.2再看fork函数


 

9.8.3再看execve函数


 

9.8.4使用mmap函数的用户级内存映射


 

 


 

9.9动态内存分配


 

 

 

9.9.1malloc和free函数


 

 

 

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

9.9.3分配器的要求和目标

要求:

 
 目标:

 

 

9.9.4碎片


 

 

9.9.5实现问题


 

9.9.6隐式空闲链表


 

 

9.9.7放置已分配的块


 

 

 

9.9.8分割空闲块


 

9.9.9获取额外的堆内存


 

9.9.10合并空闲块


 

 

 

9.9.11带边界标志的合并

1)前面的块和后面的块都是已分配的。

2)前面的块是已分配的,后面的块是空闲的。

3)前面的块是空闲的,后面的块是已分配的。

4)前面的块和后面的块都是空闲的。

在情况1中,两个邻接的块都是已分配的,因此不可能进行合并。所以当前块的状态只是简单地从已分配变成空闲。在情况2中,当前块与后面的块合并。用当前块和后面块的大小的和来更新当前块的头部和后面块的脚部。在情况3中,前面的块和当前块合并。用两个块大小的和来更新前面块的头部和当前块的脚部。在情况4中,要合并所有的三个块形成一个单独的空闲块,用三个块大小的和来更新前面块的头部和后面块的脚部。在每种情况中,合并都是在常数时间内完成的。

9.9.12综合:实现一个简单的分配器

9.9.13显式空闲链表


  使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于我们所选择的空闲链表中块的排序策略。

  一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块。在这种情况下,释放一个块可以在常数时间内完成。如果使用了边界标记,那么合并也可以在常数时间内完成。

  另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后续的地址。在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序的首次适配比LIFO排序的首次适配有更高的内存利用率,接近最佳适配的利用率。

  一般而言,显式链表的缺点是空闲块必须足够大,以包含所有需要的指针,以及头部和可能的脚部。这就导致了更大的更小块大小,也潜在地提高了内部碎片的程度。

9.9.14分离的空闲存储

9.10垃圾收集

9.10.1垃圾收集器的基本知识


 

9.10.12Mark&Sweep垃圾收集器

 
 

9.11C程序中常见的与内存相关的错误

9.11.1间接引用坏指针


 

9.11.2读未初始化的内存

  虽然bss内存位置(诸如未初始化的全局C变量)总是被加载器初始化为零,但是对于堆内存却并不是这样的。一个常见的错误就是假设堆内存被初始化为零:

9.11.3允许栈缓存区溢出

9.11.4假设指针和它们指向的对象是相同大小的

9.11.5造成错位错误

9.11.6引用指针,而不是它所指向的对象

9.11.7误解指针运算

9.11.8引用不存在的变量

9.11.9引用空闲堆块中的数据

9.11.10引起内存泄漏

  内存泄漏是缓慢、隐性的杀手,当程序员不小心忘记释放已分配块,而在堆里创建了垃圾时,会发生这种问题。

posted @ 2021-01-18 23:53  丸子球球  阅读(205)  评论(0编辑  收藏  举报