【第 8 周】深入理解计算机系统共读心得体会

所在小组

第六组

组内昵称

undefined

你的心得体会

地址翻译

地址翻译:程序如何从虚拟地址转换为物理地址从过程
image

初始数据
逻辑地址=0x80495b0
段基地址=0x1000000
线性地址=(逻辑+段基)=0x90495b0
预设页表=[0x9049:0x0000000]
物理地址=(页表+offset)=0x00005b0

核心问题:
段基地址的管理?
页表的管理?
权限的控制?

各种寄存器的设计
GDT: Global Descriptor Table,全局描述符表
GDTR:用来存放GDT的入口地址
LDT: Local Descriptor Table,局部描述符表
LDTR:用来存放LDT的入口地址
PDBR:页目录基址寄存器,CR3寄存器
CS:代码段寄存器
DS:数据段寄存器

image

特权级保护

image

所在小组

第四组

组内昵称

张旭辉

心得体会

  • 内存映射是 Linux 把磁盘上的一个-对象,关联起来并初始化一个虚拟内存区域的内容。
  • fork 原理是虚拟内存全部页面改为只读,新页面私有写时复制。
  • 地址翻译过程:

    • 命中:

      • 处理器生成虚拟地址 VA,传给 MMU。
      • MMU 生成 PTE 地址,通过高速缓存或主存请求得到它。
      • 高速缓存或主存返回PTE。
      • MMU 构造物理地址,并传送给高速缓存或主存。
      • 高速缓存或主存返回请求的数据字给处理器。
    • 未命中:

      • 处理器生成虚拟地址 VA,传给 MMU。
      • MMU 生成 PTE 地址,通过高速缓存或主存请求得到它。
      • 高速缓存或主存返回PTE, PTE 有效位为 0,传递 CPU 的控制,让操作系统内核执行缺页异常处理程序。
      • 物理内存的牺牲页,如果该牺牲页已被修改则换出磁盘。
      • 缺页处理程序 调入新的页面,并更新内存中的 PTE。
      • 返回到原来的进程,再次执行缺页指令,此时会命中,MMU 会将返回请求的数据字给处理器。

所在小组

第六组

组内昵称

慎思明辨笃行

你的心得体会

虚拟内存9.6

第 1 步:处理器生成一个虚拟地址,并把它传送给 MMU。
第 2 步:MMU 生成 PTE 地址,并从高速缓存/主存请求得到它。
第 3 步:高速缓存/主存向 MMU 返回 PTE。
第 4 步:MMU 构造物理地址,并把它传送给高速缓存/主存。
第 5 步:高速缓存/主存返回所请求的数据字给处理器。

虚拟内存9.7

Linux内核下的缺页异常处理步骤:

  1. 判断给定的虚拟地址值本身是否合法。判断方法是把给定值和区域结构链表的每个结点的起止点比较。如果结果不合法,则触发一个段错误,终止进程。

  2. 判断对这个地址进行的操作是否合法。如果不合法,触发一个保护异常并终止进程。

  3. 如果前两步骤均未终止进程,则内核选择一个牺牲页面。如果该页面之前被修改过,则先写回磁盘。之后换入新的页面,更新页表。缺页处理程序返回,并把控制流交还给引发缺页异常的指令。之后MMU能正常地翻译给定的虚拟地址。

虚拟内存9.8

Linux 通过将一个虚拟内存区域与一个磁盘上的对象(object)关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping),虚拟内存区域可以映射到两种类型的对象中的一种:
Linux 文件系统中的普通文件:一个区域可以映射到一个普通磁盘文件的连续部分,例如一个可执行目标文件。文件区(section)被分成页大小的片,每一片包含一个虚拟页面的初始内容。因为按需进行页面调度,所以这些虚拟页面没有实际交换进入物理内存,直到 CPU 第一次引用到页面(即发射一个虚拟地址,落在地址空间这个页面的范围之内)。如果区域比文件区要大,那么就用零来填充这个区域的余下部分。
匿名文件:一个区域也可以映射到一个匿名文件,匿名文件是由内核创建的,包含的全是二进制零。CPU 第一次引用这样一个区域内的虚拟页面时,内核就在物理内存中找到一个合适的牺牲页面,如果该页面被修改过,就将这个页面换出来,用二进制零覆盖牺牲页面并更新页表,将这个页面标记为是驻留在内存中的。注意在磁盘和内存之间并没有实际的数据传送。因为这个原因,映射到匿名文件的区域中的页面有时也叫做请求二进制零的页(demand-zero page)。

虚拟内存9.9

申请(虚拟)内存空间时,可以使用mmap、munmap函数,不过使用经过封装的动态内存分配器,更方便,移植性更好。

动态内存分配器维护一个进程的虚拟内存区域。对每个进程,内核维护一个变量brk,是一个指向堆顶的指针。

分配器将堆视为一组不同大小的块的集合。每个块是一个连续的虚拟内存片,或是已分配的,或是空闲的。

分配器存在两种风格:显式分配器、隐式分配器。显式分配器的典型例子就是malloc、free。隐式分配器广泛被如Java、Lisp等依赖垃圾回收机制的高级语言应用。本节之后讨论的内容都属于显式分配器。

unistd.h内声明了sbrk函数,使用该函数手动扩展和收缩堆大小。

显式分配器在设计时需要满足如下要求:

处理任意请求序列。分配器不可以假设未来任何分配和释放请求的顺序。
立即响应请求。意味着缓存、重新排列请求等策略在分配器设计中不可用。
只使用堆。为保证分配器扩展性,分配器使用的任何非标量数据结构(是啥我也不知道)都必须保存在堆中。
对齐块。分配器所分配的块必须满足操作系统和硬件的字节对齐要求,使块可以保存任何数据对象。
不修改已分配的块。只能操作或改变空闲块,意味着压缩已分配块这类技术在分配器设计中无法使用。

所在小组

第三组

组内昵称

晴天

你的心得体会

  1. 虚拟内存是对主存的一个抽象,支持虚拟内存的处理器通过虚拟寻址的方式来引用内存。
  2. 页表是用来翻译虚拟地址,其内容是操作系统提供的。
  3. 虚拟内存提供了3个非常重要的功能:主存中自动缓存最近使用的存放磁盘上的虚拟地址空间的内容,当缺页时将控制转移到操作系统中的一个缺页程序处理;虚拟内存简化了内存管理,进而又简化了链接,在进程间共享数据。
  4. 大多数页条目是位于L1高速缓存中。
  5. 操作系统通过虚拟内存片和磁盘文件片关联,来达到初始化虚拟内存片的目的。
  6. 内存泄漏是导致系统变慢,CPU飙升的杀手之一。

所在分组

第二组

组内昵称

文弱书生

你的心得体会

  1. 虚拟内存是抽象在物理内存之上的一片连续的地址空间,可以使操作系统很容易的管理内存,更容易编程以及隔离内存操作。
  2. 虚拟内存优势是简化了内存管理,简化链接,实现进程间通信,并且可以保护内存
  3. 虚拟内存的地址是由MMU处理的,PAGETABLE负责映射,将不同进程得不同虚拟内存地址映射到同一个物理地址,以便共享内存
  4. 因为内存地址变多,导致pagetable表项也增加。需要用多级页表来处理。多级页表不需要为所有的可能表项分配内存,是按照需要分配的。
  5. 写程序要考虑内存泄露的问题,会导致系统变慢,CPU飙升。

所在小组

第三组

组内昵称

uucloud

你的心得体会

  • 虚拟寻址:CPU通过虚拟地址(VA)访问主存,VA被内存管理单元(MMU)翻译成物理地址(PA)。
  • 页表:物理内存中,MMU读取的,维护虚拟地址空间和实际存储位置关系的数据结构,虚拟地址都可以转为一个PTE(Page Table Entry),每一项PTE表明了某个虚拟页是否被缓存在DRAM中(有效位),设置了有效位,地址则为物理页号,否则是磁盘的起始位置。
  • 分配页面:malloc,会创建新的PTE,并指向磁盘的某个空地址。
  • 利用Linux的getrusage函数可以监测缺页数量。
  • 每个进程都有独立的页表,不同虚拟页面可以映射到同一个物理页上(共享内存的一种方式),这带来了很多好处:
    • 简化链接:程序的内存格式都是一致的,链接器的设计和实现就变得简单了很多。
    • 简化加载:当需要加载文件时,只需要分配虚拟页,然后标记为未被缓存,把地址指向目标文件,这样在实际用到的时候就会引发缺页然后按需实际加载(mmap系统调用)。
    • 简化共享:比如大家都共享内核或标准库的代码
    • 简化内存分配:比如malloc,新分配的内存在实际物理页上可以是不连续的。
  • 内存读写保护:只需要给PTE加一个标识位就行。(SUP READ WRITE,SUP是必须在内核模式才能访问),如果违反了,内核会报段错误(segmentation fault)。
  • 为了减少读PTE的开销,MMU中也有缓存,叫翻译后备缓冲期(TLB,Translation Lookaside Buffer)
  • 多级页表:有一个很显然的问题是,如果页表只有一层,那么每个程序都要存储一份完整的虚拟内存总大小的PTE。多级页表可以解决这个问题,比如一个二级页表中第一级可以指向一个二级页表,这个二级页表实际对应着虚拟内存,没用到的内存一级页表为空,这样内存只用存一级页表和部分二级页表即可。

物理主存被组织成一个由连续字节组成的数组,每个字节都有一个唯一的物理地址。早期的CPU使用物理寻址,即CPU通过存储器总线直接读取主存上特定物理地址的内容到寄存器中。现代处理器使用虚拟寻址方式,通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到存储器之前先转换成适当的物理地址(地址翻译)。CPU上的存储器管理单元(MMU)利用存放在主存中的查询表(页表)来动态地翻译虚拟地址,该表的内容由操作系统管理。

地址翻译

当页命中时,CPU硬件的执行步骤如下:

  1. 处理器生成一个虚拟地址,并把它传送给MMU;
  2. MMU生成PTE地址,并从高速缓存/主存中请求得到它;
  3. 高速缓存/主存向MMU返回PTE;
  4. MMU构造物理地址,并把它传送给高速缓存/主存;
  5. 高速缓存/主存返回所请求的数据字给处理器; 可见页命中完全是由硬件处理的。

当缺页时,需要硬件和操作系统内核协作完成,步骤如下:

  1. 处理器生成一个虚拟地址,并把它传送给MMU;
  2. MMU生成PTE地址,并从高速缓存/主存中请求得到它’
  3. 高速缓存/主存向MMU返回PTE;
  4. PTE中的有效位为零,所以MMU触发一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序;
  5. 缺页处理程序确定出物理存储器中的牺牲页,如果这个页已经被修改了,则把它换出到磁盘;
  6. 缺页处理程序调入新的页,并更新存储器中的PTE;
  7. 缺页处理程序返回到原来的进程,再次执行导致缺页的指令,CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理存储器中,所以就会被命中,主存会将所请求的字返回给处理器。

在MMU中包括了一个关于PTE的小的缓存,称为TLB(翻译后备缓冲器)。在有TLB,且TLB命中的情况下,CPU的执行步骤如下:

  1. 处理器生成一个虚拟地址,并把它传送给MMU;
  2. MMU从TLB中取出相应的PTE;
  3. MMU构造物理地址,并把它传送给高速缓存/主存;
  4. 高速缓存/主存返回所请求的数据字给处理器;

垃圾收集

垃圾收集器将存储器视为一张有向图,由堆节点组成,每个堆节点对应堆中一个已分配的块,当存在一条从任意根节点(根节点是一些不在堆中的位置,但是它们包含指向堆中的指针)出发并达到p节点的有向路径时,就说节点p是可达的,而不可达的节点就是垃圾,是不能被应用再次使用的。垃圾收集器的角色是维护可达图的某种表示,并通过释放不可达节点来定期地回收它们。

所在小组
第七组

组内昵称
hayden

心得体会
为了更加有效的管理内存并且少出错,现在操作系统提供了一种主存的抽象概念,叫做虚拟内存。虚拟内存是计算机系统管理内存的一种技术,使得应用程序操作内存更加简单,应用程序认为内存是一个连续可用的内存,其实物理内存是有很多的碎片,部分存储在磁盘上,进行数据交换,使得编程更加容易,也方便了操作系统管理内存。

每个进程都有自己的虚拟地址空间,进程可以简单的认为在操作线性空间.虚拟内存带来的另一个好处就是可以简化链接和载入的结构。

操作系统使用虚拟寻址,将虚拟地址转换成物理地址进行地址翻译。虚拟内存的定义是基于对地址空间的重定义的,使的程序以为使用线性的连续的虚拟内存地址”,以为自己正在使用一大块的“连续”地址。

在进行地址映射时,如果发现要访问的页面不在内存中,会发生中断,当发生缺页中断时,如果操作系统内存中没有空闲页面,则操作系统 必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。而用来选择淘汰哪一页的规则叫做页面置换算法,页面置换算法的作用是实现虚拟存储管理。

所在小组

第一组

组内昵称

SADAME

心得体会

使用虚拟内存主要是基于下面三个考虑:

  1. 可以更有效率的使用内存:使用 DRAM 当做部分的虚拟地址空间的缓存
  2. 简化内存管理:每个进程都有统一的线性地址空间
  3. 隔离地址控件:进程之间不会相互影响;用户程序不能访问内核信息和代码

程序员通过动态内存分配(例如 malloc)来让程序在运行时得到虚拟内存。动态内存分配器会管理一个虚拟内存区域,称为堆(heap)。

分配器以块为单位来维护堆,可以进行分配或释放。有两种类型的分配器:

  • 显式分配器:应用分配并且回收空间(C 语言中的 mallocfree
  • 隐式分配器:应用只负责分配,但是不负责回收(Java 中的垃圾收集)
    程序可以用任意的顺序发送 mallocfree 请求,free 请求必须作用与已被分配的 block。

分配器有如下的限制:

  • 不能控制已分配块的数量和大小
  • 必须立即响应 malloc 请求(不能缓存或者给请求重新排序)
  • 必须在未分配的内存中分配
  • 不同的块需要对齐(32 位中 8 byte,64 位中 16 byte)
  • 只能操作和修改未分配的内存
  • 不能移动已分配的块

内存陷阱

关于内存的使用需要注意避免以下问题:

  • 解引用错误指针
  • 读取未初始化的内存
  • 覆盖内存
  • 引用不存在的变量
  • 多次释放同一个块
  • 引用已释放的块
  • 释放块失败

所在小组:静默组
组内昵称:黄小黄
心得体会:

  • SRAM 缓存指的是 CPU 和 L1,L2,L3 和主存之间的缓存,DRAM 缓存指的是虚拟内存系统的缓存,他在主存中缓存虚拟页。
  • 虚拟内存优点:
  1. 简化链接:每个进程都可以使用相同的基本格式(包括 segment 组成,内存地址);
  2. 简化加载:向内存中加载可执行文件和共享对象文件变得简单。加载磁盘文件时是通过虚拟内存地址加载的;
  3. 简化共享:每个进程资源是隔离的,但只要将虚拟页面映射到同一个物理页面,就可以了安排多个进程共享这部分代码的一个副本,而不是每个进程都包含单独一个副本;
  4. 简化内存分配:进程申请额外的堆空间时(如 malloc),操作系统分配连续数字的虚拟内存页,并映射到不一定连续的物理页面中。
  • 虚拟内存的特性可以作为内存保护的工具。进程之间无法随意的访问对方的内存空间。虚拟内存还可以通过设置 flag 来控制内存的读写权限
  • 地址翻译过程:(命中)
  1. 处理器生成虚拟地址 VA,传给 MMU;
  2. MMU 生成 PTE 地址,通过高速缓存或主存请求得到它;
  3. 高速缓存或主存返回PTE;
  4. MMU 构造物理地址,并传送给高速缓存或主存;
  5. 高速缓存或主存返回请求的数据字给处理器。
  • 地址翻译过程:(不命中)
  1. 处理器生成虚拟地址 VA,传给 MMU;
  2. MMU 生成 PTE 地址,通过高速缓存或主存请求得到它;
  3. 高速缓存或主存返回PTE;
  4. PTE 有效位为 0,传递 CPU 的控制,让操作系统内核执行缺页异常处理程序
  5. 确定物理内存的牺牲页,如果该牺牲页已被修改则换出磁盘;
  6. 缺页处理程序页面调入新的页面,并更新内存中的 PTE。
  7. 返回到原来的进程,再次执行缺页指令,此时会命中,MMU 将返回请求的数据字给处理器。
  • 在 SRAM 缓存和虚拟内存共存的系统中,大部分系统使用的是物理寻址来访问 SRAM。这样设计可以让 SRAM 缓存保持简单。
  • PTE 也可以有缓存,来避免每次使用虚拟地址 MMU 都要查阅一个 PTE。PTE 缓存就在 MMU 中,叫 TLB
  • 内存映射是 Linux 把磁盘上的一个对象,关联起来并初始化一个虚拟内存区域的内容。
  • fork 原理是虚拟内存全部页面改为只读,新页面私有写时复制。
  • 内存分配实现:1. 隐式空闲链表。把大小和是否使用写在区块头,通过遍历链表(一般用数组实现),找出空闲区块。匹配策略有三种:首次适配,从头找到第一个适合的空闲块;下次适配,从上一次位置开始找;最佳适配,找到符合要求的最小空闲块。匹配后分割也有策略。合并(碎片整理)也有策略,可以是立即合并(释放时合并)和推迟合并(无空间时合并,通常会使用这个)。
    1. 显式空闲链表。把空闲块信息写在空闲块主体内(因为它是空闲的,所以可以用来存数据),信息数据结构是一个双向链表,记录上一个和下一个空闲块。管理方法可以是 LIFO,刚释放的块放入链表头部,这样释放块时间复杂度是 O(1),如果用了边界标记(末尾标记),合并的时间复杂度也是 O(1);也可以是地址顺序,这样首次匹配复杂度是 O(n),但内存利用率接近最佳匹配。显示链表有最小区块限制,一定程度上提高了内部碎片的程度。
    1. 分离空闲列链表。维护多个空闲链表,同一个链表的空闲块大小大致相等。
    1. 垃圾收集器根据内存的可达图,来判断孤立的内存块,进而回收

所在小组

第七组

组内昵称

jinmiaoluo

你的心得体会

见: https://github.com/jinmiaoluo/blog/tree/main/example-8-reading-notes/csapp/chapter-7-and-chapter-8#虚拟内存如何保护物理内存
Diff: https://github.com/jinmiaoluo/blog/pull/11/files

所在小组

第四组

组内昵称

zhilin

你的心得体会

虚拟内存

虚拟内存不只是“用磁盘空间来扩展物理内存”的意思,把内存扩展到磁盘只是使用虚拟内存技术的一个结果,
它的作用也可以通过覆盖或者把处于不活动状态的程序以及它们的数据全部交换到磁盘上等方式来实现。
对虚拟内存的定义是基于对地址空间的重定义的,即把地址空间定义为“连续的虚拟内存地址”,
以借此“欺骗”程序,使它们以为自己正在使用一大块的“连续”地址。
是现代系统提供的一种对主存的抽象概念,
它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域按需在磁盘和主存之间来回传送数据;为每个进程提供了一致的地址空间,简化了内存管理;并且保护每个进程的地址空间不被其他进程破坏。

内存映射(memory mapping)

Linux通过将一个虚拟内存区域与一个磁盘上的对象(object)关联起来,已初始化这个虚拟内存区域的内容,这个过程称为内存映射。虚拟内存区域可以映射到两种 类型的对象中的一种:

Linux 文件系统中的普通文件: 一个区域可以映射到一个普通磁盘文件的连续部分,例如一个可执行目标文件。

文件区(section)被分成页大小的片,每一片包含一个虚拟页面的初始内容。如果区域比文件区要大,那么就用零来填充这个区域的余下部分。

匿名文件:一个区域也可以映射到一个匿名文件,匿名文件是由内核创建的,包含的全是二进制零。

在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数

写时复制

只要没有进程试图写自己的私有区域,它们就可以在继续共享物理内存中所存储的共享对象的一个单独副本。然而,只要有一个进程试图写私有区域内的某个页面,那么这个写操作就会触发一个保护故障(Protection failure). 当故障处理程序注意到保护异常是由于进程试图写私有的写时复制区域中的一个页面而引起的,它就会在物理内存中创建这个页面的一个新副本,更新页表条目指向这个新的副本,然后恢复这个页面的可写权限。当故障处理程序返回时,CPU重新执行这个写操作,现在在新创建的页面上这个写操作就可以正确执行了。

关于内存的常见的错误示例

间接引用坏指针,读取未初始化的内存,允许栈缓冲区溢出,假设指针和它们指向的对象大小相同,引用指针而不是它所指向的对象,误解指针运算,引用不存在的变量,以及引起内存泄漏

指针运算
指针的算术操作是以它们指向的对象的大小为单位来进行计算的。这个单位的大小并不一定是字节。例如,下面函数的目的是扫描一个int的数组,并返回一个指针。指向val首次出现的位置。

动态内存分配

动态内存分配器,将堆作为大小不同的块集合来维护。每个块要么是已分配的,要么是空闲的。

分配器核心目标:1.最大化吞吐率 2.最大化内存利用率

垃圾收集

Mark&Sweep收集器:由标记和清楚阶段组成