《深入理解计算机系统》CSAPP第九章——虚拟内存

物理和虚拟内存

计算机系统中主存被组织为一个由M个连续字节大小的单元组成数组。每字节都有一个唯一的物理地址。

虚拟地址是虚拟的,CPU通过MMU(内存管理单元——利用主存中的查询表动态翻译虚拟地址 )硬件将虚拟地址——>物理地址,流程如图:

在这里插入图片描述

地址空间

主存中的每个字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址。

虚拟内存作为缓存的工具

虚拟内存被组织为一个由存放在磁盘上的N个连续字节大小的单元组成数组。

虚拟内存系统通过将虚拟内存分割为虚拟页的大小固定的块来处理这个问题。物理内存和虚拟内存被分割为物理页和虚拟页,大小都是P字节,物理页也称之为页帧。

虚拟页面的集合都分为三个不相交的子集:

  • 未分配的:VM系统还没有分配(创建)的页。未分配块没有它们的任何数据和它们管理,也不占磁盘
  • 缓存的:当前已经缓存在物理内存的已分配页
  • 未缓存的:未缓存在物理内存的已分配页

如图:

在这里插入图片描述

DRAM缓存的组织结构

  • DRAM中如果不命中,需要从磁盘中读取数据,往往时间消耗巨大
  • 对磁盘访问时间长,DRAM缓存总是写回,而不是直写。

页表

虚拟内存机制的完成,是由软硬件联合提供的,包括OS软件、MMU中的地址翻译硬件和一个存放在物理内存中的页表的数据结构。

页表是一个页表条目的数组:

在这里插入图片描述

页命中

CPU根据虚拟地址,通过MMU把虚拟地址定位页表,如果页表对应项中有效位为一,页命中

缺页、分配页面次数也是如此

幸好有局部性

程序倾向于在一个较小的活动页面集合上工作,这个集合叫做工作集或常驻集合。

抖动:如果工作集超过了物理内存大小,就会产生抖动现象。

  • 虚拟内存作为内存管理的工具
  • 可以多个虚拟内存同时映射同一个共享的物理内存
  • 按需页面调度和独立的虚拟地址空间集合对内存管理极其重要,VM简化链接、加载、代码和数据共享以及应用程序的内存分配。

地址翻译

地址翻译符号:

在这里插入图片描述

使用页表的地址翻译:

在这里插入图片描述

CPU的一个控制寄存器页表基址寄存器指向当前页表。其中虚拟地址分为虚拟页号和虚拟页偏移量,MMU利用虚拟页号选择适当的页表条目,从页表条目中得到物理页号,利用物理页号再进行拼接虚拟页偏移量即可得到响应的物理地址。

当页面命中时,CPU执行硬件操作如下:

  1. CPU生成一虚拟地址,传给MMU
  2. MMU生成PTE地址,从高速缓存/主存请求得到它
  3. 高速缓存/主存向MMU返回PTE(页表条目)
  4. 构造物理地址,并把它传给高速缓存/主存
  5. 高速缓存/主存返回所请求的数据字给处理器

在这里插入图片描述

页面命中完全由硬件来处理,如果缺页的话,由硬件和OS内核协作完成

第一步到第三步:和页命中一样

第四步发现PTE有效位为0,MMU触发一个异常,传递CPU中的控制到OS内核中的缺页异常处理程序

第五步,缺页异常处理程序确定物理内存中的牺牲页,如果这个页面已经被修改了,把它换出磁盘

第六步,缺页处理程序页面调入新的页面,并更新内存的PTE

第七步,缺页返回程序返回到原来的进程,再次执行导致缺页的指令。之后就是再走一遍页命中的老路

结合高速缓存和虚拟内存

在PTE中,我们之前总是在主存中获取,并且经常访问PTE。其具有较高的局部性,如果设置为缓存那不是美滋滋,如图:

在这里插入图片描述

利用TLB加速地址翻译

之前说的如果把PTE放在L1缓存中,可以把开销减小到1或2个周期。然而很多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小缓存,称之为TLB(Translation Lookaside Buffer)

TLB示意图如图:

在这里插入图片描述

TLB通常有高度的相联度,用于组选择和行匹配的索引和标记字段是从虚拟地址的虚拟页号提取出来。如果TLB有 2 t 2^t 2t个组,则标记为为VPN中剩余位数构成。

当TLB命中的流程如何:

  1. CPU生成一个虚拟地址
  2. MMU根据虚拟地址直接在TLB取出响应的PLE
  3. MMU将这个PLE翻译成物理地址,然后把他发送到高速缓存/主存中
  4. 高速缓存/主存中将所请求的数据字返回给CPU

当TLB不命中时,MMU必须从L1缓存中清楚PLE

在这里插入图片描述

多级页表

到目前为止,我们一直都是使用单独的页表来进行地址翻译。如果我们在32位地址空间、4KB的页面和一个4字节的PTE,那么即使应用所引用的只是虚拟地址空间中很小的一部分,也需要4MB的页表驻留在主存中。

4MB来源于

虚拟地址有32位,页内偏移为 4 K = 2 12 4K=2^{12} 4K=212。一共有20位地址记载页面大小。即页表存储一共需要 2 20 2^{20} 220 2 2 2^2 22 ,需要4MB大小。

二级页表从两方面减少了内存要求:

  1. 若一级页表的PTE为空,则相应的二级表不会被分配。对于一个典型的程序,一般来说4GB的内存大小大部分都是未分配的
  2. 只有一级页表才需要总是在主存中,虚拟地址系统在系统在需要时创建页面调入或调入二级页表,减少主存压力

在这里插入图片描述

使用K级页表层次结构的地址翻译,VA被划分成为K个VPN和一个VPO:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LsbsiFWo-1633835098745)(E:\研究生\工作\阅读书籍\csapp\第九章\image-20211009200848151.png)]

端到端的地址翻译

这里参考课本的示例,可以对高速缓存、TLB、虚拟地址和物理地址

Intel Core i7/Linux内存系统

如图是Intel Corei7内存系统重要组成部分,处理器封装包括四个核、一个大的所有核共享的L3高速缓存,以及一个DDR3内存控制器。

在这里插入图片描述

在这里插入图片描述

类似与多级页表读取PLE的过程,其中CR3寄存器包含了L1页表的物理地址,VPN1提供了VPN2的地址,以此类推。

Linux 虚拟内存系统

还是老图:
在这里插入图片描述

在这里内核虚拟内存包含内核中的代码和数据接哦古。内核虚拟内存的某个区域被映射到所有进程共享的物理页面。比如每一个进程共享内核的代码和全局数据结构。

有趣的是,Linux也将一组连续的虚拟页面(大小等于系统中的DRAM的总量)映射到相应的一组连续的物理页面。这为内核提供一种便利的方式访问物理内存的任意位置。

内核虚拟内存的其他区域包含每个进程不一样的数据。比如页表、内核在进程上下文执行代码时使用的栈,及其记录虚拟地址空间当前组织的各种数据结构。

  1. Linux 虚拟地址区域

    Linux将虚拟内存组织成一些区域的集合。一个区域就是已经存在的(已分配)的虚拟内存的连续片,这些页是以某种方式相关联的。

    每个存在的虚拟页面都保留在某个区域中,而不属于某个区域的虚拟页是不存在的,并且不能被进程引用。

    区域的概念很重要,因为它允许虚拟地址有间隙。内核不用记录哪些不存在的虚拟页,而这样的页不用占用内存、磁盘、还有内核本身的任何额外资源。

    如图,强调了记录一个进程中虚拟内存区域的内核数据结构:

    在这里插入图片描述

    任务结构元素包括或者指向内核运行该进程所需要的所有信息(例如,PID,指向用户栈的指针,可执行目标文件名字,及其程序计数器)

    任务结构种一个条目指向mm_struct,描述虚拟内存的当前状态。我们感兴趣的是pgd(指向第一级页表[页全局目录]的基址),mmap(指向一个vm_area_struct[区域结构])的链表,其中vm_area_struct都描述了当前虚拟地址空间的一个区域。当内核运行这个进程时,就把pgd放入到CR3控制寄存器。

  2. Linux 缺页异常处理

    1. 段错误

    2. 保护异常

    3. 正常缺页

      在这里插入图片描述

内存映射

Linux 通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程叫做内存映射

虚拟内存区域可以映射到两种类型的对象中的一种:

  • Linux文件相同中的普通文件:一个区域可以映射到一个普通磁盘文件的连续部分,例如一个可执行目标文件。这些虚拟页面并没有加入物理内存,知道CPU第一次引用。
  • 匿名文件:一个区域也可以映射到一个匿名文件,匿名文件是由内核创建的,包含的都是二进制零。

再看共享对象

在这里插入图片描述

私有对象使用一种叫做写时复制的技术巧妙地映射到内存中。一个私有对象开始生命周期的方式和共享对象的一样,在物理内存中只保留私有对象的一份副本。

对于每个映射私有对象的进程,相应私有区域的页表条目都被标记为只读,并且区域结构被标记为私有的写时复制。如果有一个进程试图修改私有区域的某个页面,那这个写操作会触发一个保护故障,进行如下图的操作:

在这里插入图片描述

再看fork()函数

父进程创建子进程时使用fork函数,如果父子进程中有一个进程进行写操作,写时复制机制就会创建新的页面,即为每一个进程保持了私有地址空间的抽象概念

再看execve()函数

execve("a.out", NULL, NULL)

execve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效代替当前程序。

加载并运行a.out需要以下步骤:

  1. 删除已存在的用户区域
  2. 映射私有区域 其中bss区域是请求二进制零的,映射到匿名文件
  3. 映射共享区域
  4. 设置程序计数器,execve最后一件事就是设置进程上下文的PC,使它指向代码区域的入口点。

映射如图:

在这里插入图片描述

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

mmap函数要求内存创建一个新的虚拟内存区域,最好是从地址start开始的一个区域,并将文件描述符fd指定的对象的一个连续的片映射到这个新区域。

void * mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)

在这里插入图片描述

int munmap(void *start, size_t length)

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

动态内存分配

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。紧接未初始化的数据区域后面开始向上(高地址增长)。对于每一个进程,内核维护者一个变量brk,它指向堆的顶部。

分配器有两种基本的风格。两种风格都要求应用显式分配块。不同之处在于那个实体负责释放已分配块

  • 显式分配器, C malloc 与 free
  • 隐式分配器, 要求分配器检测一个已分配块何时不被程序使用时释放块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配块的过程叫做垃圾收集。Java语言做的比较出色

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

最重要的原因:经常直到程序运行时,才知道某些数据结构大小。当然正确高效使用分配器十分重要。

分配器要求与目标:

  1. 处理任意请求序列
  2. 立即响应请求
  3. 只使用堆
  4. 对齐块
  5. 不修改已分配块,分配器只能操作或改变(空闲块)

分配器设计始终是在最大化吞吐率与最大化内存利用率之间平衡

碎片

造成堆利用率低的原因是碎片:

  • 内部碎片
  • 外部碎片
  • 量化内部碎片难,难以量化或不可预测,通常常用启发式策略试图维持少量大空闲块

实现问题

  1. 空闲块组织
  2. 放置
  3. 分割
  4. 合并

隐式空闲链表

在这里插入图片描述

在这里插入图片描述

显式空闲链表

在这里插入图片描述

垃圾收集

垃圾收集器是种动态内存分配器,它自动释放程序不再需要的已分配块。

垃圾收集器将内存视为一张有向可达图。

编程语言方面,像ML、Java这样的编程语言的垃圾收集器,对于创建指针比较有严格的规定,能够维护可达图的精准表达

Mark & Sweep 垃圾收集器

由标记阶段和清除阶段组成

C语言使用Mark & Sweep 垃圾收集器来处理的时候是必须保守的,其根本原因是因为C语言不会使用类型信息来标记内存位置

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

  1. 间接引用坏指针

    scanf("%d", val)
    
  2. 读为初始化内存

    虽然.bss内存位置准备加载初始化为零,但堆内存不同

    int *y = (int *) malloc(n * sizeof(int))   // 这里的y指向的内容不一定为0,eg: y[0]不一定为零
    
  3. 允许栈缓冲区溢出

  4. 假设指针和它们指向对象都是相同的

  5. 造成错位错误

  6. 引用指针,而不是它所指向对象 *size – (*size)–

  7. 误解指针运算

  8. 引用不存在变量

  9. 引用空闲块中的数据

  10. 引起内存泄漏

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 深入理解计算机系统CSAPP)是由Randal E. Bryant和David R. O'Hallaron编写的经典计算机科学教材。该教材通过涵盖计算机体系结构、机器级别表示和程序执行的概念,帮助学生深入理解计算机系统的底层工作原理和运行机制。 深入理解计算机系统的练习题对于学生巩固并应用所学知识非常有帮助。这些练习题涵盖了计算机硬件、操作系统和编译器等多个领域,旨在培养学生解决实际问题和设计高性能软件的能力。 对于深入理解计算机系统的练习题,关键是通过实践进行学习。在解答练习题时,应根据课本提供的相关知识和工具,仔细阅读问题描述,并根据实际需求设计相应的解决方案。 在解答练习题时,需要多角度思考问题。首先,应准确理解题目要求,并设计合适的算法或代码来解决问题。其次,应考虑代码的正确性和效率,以及对系统性能的影响。此外,还要注意处理一些特殊情况和异常情况,避免出现潜在的错误或安全漏洞。 解答练习题的过程中,应注重查阅相关资料和参考优秀的解答。这可以帮助我们扩展对问题的理解,并学习他人的思路和解决方法。同时,还可以通过与同学和老师的讨论,共同探讨问题和学习经验。 总之,通过解答深入理解计算机系统的练习题,可以帮助学生巩固所学知识,同时培养解决实际问题和设计高性能软件的能力。这是一个学以致用的过程,可以加深对计算机系统运行机制和底层工作原理的理解。 ### 回答2: 理解计算机系统CSAPP)是一本经典的计算机科学教材,通过深入研究计算机系统的各个方面,包括硬件、操作系统和编程环境,对于提高计算机科学专业知识与能力具有很大帮助。 练习题是CSAPP中的重要部分,通过练习题的完成,可以加深对计算机系统理解,并将理论知识转化为实践能力。练习题的数量、难度逐渐递增,从简单的概念与基础问题到复杂的系统设计与实现。 在解答练习题时,首先需要对题目进行仔细阅读和理解,明确题目的要求和限制条件。然后,可以利用课堂讲解、教材内容、网络资源等进行查阅和学习相应的知识。同时,还可以参考课后习题解答等资料,了解一些常见的解题方法和思路。 在解答练习题时,可以利用计算机系统的工具和环境进行实际测试和验证。例如,可以使用调试器、编译器和模拟器等工具对程序或系统进行分析和测试。这样可以更加深入理解问题的本质,并找到恰当的解决方法。 另外,解答练习题时还可以与同学、教师和网上社区进行交流和讨论。这样可以互相学习和交流解题思路,共同解决问题。还可以了解不同的解题方法和技巧,提高解题效率和质量。 练习题的解答过程可能会遇到一些困难和挑战,例如理论知识的不足、复杂问题的分析与解决。但是通过不断地思考和实践,相信可以逐渐提高解题能力,更好地理解计算机系统。 总之,深入理解计算机系统CSAPP)练习题是提高计算机科学专业知识和能力的重要途径。通过仔细阅读和理解题目,查阅相关知识,利用计算机系统工具和环境进行实践,与他人进行交流和讨论,相信可以更好地理解计算机系统的各个方面,并将知识转化为实际能力。 ### 回答3: 《深入理解计算机系统CSAPP)》是计算机科学领域的经典教材之一,对于深入理解计算机系统的原理、设计和实现起到了极大的帮助。在阅读这本书的过程中,书中的习题也是非常重要的一部分,通过做习题,我们可以更好地理解书中所讲的概念和思想。 CSAPP的习题涵盖了课本中各个章节的内容,从基础的数据表示和处理、程序的机器级表示、优化技术、程序的并发与并行等方面进行了深入探讨。通过解答习题,我们可以对这些知识进行实践应用,巩固自己的理解,并培养自己的解决问题的思维方式。 在解答习题时,我们需要充分理解题目要求和条件,并从知识的角度进行分析。有些习题可能需要进行一些编程实践,我们可以通过编程实现来验证和测试我们的思路和解决方案。在解答问题时,我们还可以查阅一些参考资料和网上资源,充分利用互联网的学习资源。 在解答习题时,我们需要保持积极的思维和态度。可能会遇到一些困难和挑战,但是通过坚持和努力,我们可以克服这些困难,提高我们的解决问题的能力。同时,我们还可以通过与同学或者其他人进行讨论,相互分享解题经验和思路,从而更好地理解问题。 综上所述,通过深入理解计算机系统CSAPP)的习题,我们可以进一步巩固和深化对计算机系统理解。掌握这些知识,不仅可以提高我们在计算机领域的能力,还可以为我们未来的学习和职业发展奠定重要的基础。因此,认真对待CSAPP的习题,是我们在学习计算机系统知识中不可或缺的一部分。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值