### 1. 虚拟内存

- 透明：对应用程序透明，程序觉得自己获得的都是私有的物理内存
- 效率：
  - 时间上，分配虚拟内存要快，不要拖慢应用程序的执行速度
  - 空间上，不要占用太多内存来支持虚拟化
- 保护：操作系统应该要对进程的内存进行保护，免受其他进程影响，操作系统也不应该受其他进程影响
  - 进程和进程之间隔离

### 2. 栈内存和堆内存

- 栈内存：申请和释放操作由编译器来管理，常用于函数内部定义的变量
- 堆内存：长期存在，函数结束后不会被释放掉，所有的申请和释放操作都显示完成

### 3. 地址转换

- 基于硬件的地址转换
- hardware-based address translation
- 将应用程序中的内存引用重定向到实际物理内存地址
- 操作系统提供内存的管理操作，记录被占用和空闲的位置

### 4. 基于硬件的重定位

- 每个CPU含有两个寄存器
  - 基址寄存器：存储物理内存的开始地址
  - 限界寄存器：存储物理内存最大分配的范围，保护内存访问不会越界
- 进程访问超过限界寄存器的地址或负数的地址，会被终止
- 真实物理地址 = 基址寄存器地址 + 虚拟内存地址
- 缺点：
  - 内存空间碎片
  - 需要空闲链表来表示哪个区块是空闲的

![进程内存重定位](https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/blog/note/image-20230724150902703.png)

> CPU负责内存地址转换的部分也称为MMU，内存管理单元，Memory Management Unit

### 5. 分段

> 出现原因：
>
> - 基于硬件的地址转换太过于浪费内存，在堆和栈之间的内存，并没有被完全使用，但是依然占用了物理内存
> - 剩下的内存如果连续范围不够大，就会造成进程无法运行

- 原理：给内存中的每个段都分配一对基址+限界寄存器
  - 代码段
  - 栈段
  - 堆段
- 分段的机制使操作系统可以将不同的段放在物理内存不同位置

<img src="https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/blog/note/image-20230725204859460.png" alt="image-20230725204859460" style="zoom: 40%;" />

#### 5.1 怎么确定当前地址访问哪个段

- 地址二进制前两位，用来表示代码段，栈段和堆段
  - 用两位表示3个短，空间浪费
- 还可以用地址的产生方式来表示
  - 如果地址由程序计数器产生（即它是指令获取），那么地址在代码段
  - 如果基于栈或基址指针，它一定在栈段。其他地址则在堆段

#### 5.2 段共享

- 硬件和操作系统在地址中加标识，可以用来共享段
- 代码段共享，每个进程都可以访问执行代码段中指令
- 物理内存中的一个段可以映射到多个虚拟地址空间

#### 5.3 分段机制缺陷

- 空间割裂，造成内存浪费
  - 每个段之间的空闲空间发利用
- 空间大小不一致，内存无法分配
  - 进程需要一个24KB的空间，但是分段后没有一个足够大的内存空间来支撑分配

<img src="https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/blog/note/image-20230726113528993.png" alt="image-20230726113528993" style="zoom:50%;" />

> 上面展示了该问题的一个例子。在这个例子中，全部可用空闲空间是 20 字节，但被切成
> 两个 10 字节大小的碎片，导致一个 15 字节的分配请求失败

#### 5.4 空闲空间管理

> 空闲空间列表：将所有空闲的空间统计到一起，方便统计和分配空间
>
> <img src="https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/blog/note/image-20230726114634791.png" alt="image-20230726114634791" style="zoom:50%;" />

- 分割与合并

  - 分割：此时如果需要分配大小为1个的空间，将会从前后两个free空间中分配1个，假设分割20后的空闲空间，则空闲列表的第二个空闲空间将会变成21

    <img src="https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/blog/note/image-20230726114850318.png" alt="image-20230726114850318" style="zoom:50%;" />

  - 合并：此时如果中间使用的10个空间释放了，则空闲列表会进行合并为一个节点，而不是变成三个节点

    - 合并前，三个连续的10个大小的空闲空间，只能分配小于等于10空间给其他进程
    - 合并后，可以分配更大的空间给其他进程

    <img src="https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/blog/note/image-20230726115025892.png" alt="image-20230726115025892" style="zoom:50%;" />

#### 5.5. 多种空闲空间分配方式

- 最优匹配：先遍历整个空闲列表，找到和请求大小一样或更大的空闲块，然后返回这组候选者中最小的一块

  - 优点：减少空间浪费
  - 缺点：遍历列表有性能损失较多

- 最差匹配：与最优匹配相反，尝试找最大的空闲块，分割并满足用户需求后，将剩余的块（很大）加入空闲列表。最差匹配尝试在空闲列表中保留较大的块，而不是向最优匹配那样可能剩下很多难以利用的小块

  - 缺点：遍历列表性能损耗，剩很多小碎块无法分配

- 首次匹配：遍历列表，将遇到的第一个满足需求的空闲块返回给用户，分割后剩下的空间加入空闲列表

  - 优点：性能损耗较少，通常不需要遍历整个列表
  - 缺点：列表开头会有很多小碎空间

- 下次查找：用指针记录上一次分配的位置，本次分配继续沿着上一次的指针向后遍历

  - 优点：避免了遍历查找，碎空间分布在列表各个位置

- 分离空闲列表：如果某个应用程序经常申请一种（或几种）大小的内存空间，就用一个独立的列表，只

  管理这样大小的对象。其他大小的请求交给更通用的内存分配程序

  - 优点：解决碎片问题

- 伙伴系统：空闲空间首先从概念上被看成大小为 2*N* 的大空间。当有一个内存分配请求时，空闲空间被递归地一分为二，直到刚好可以满足请求的大小（再一分为二就无法满足）

  - 优点：合并速度快，当有一个8kb的块释放后，会检查伙伴8kb，是否释放，如果释放就进行合并操作，然后递归向上合并

- 平衡二叉树、伸展树和偏序树

### 6. 分页

> 分段固有缺陷：内存碎片化

- 虚拟内存中将空间分割成固定长度的分片，分割成固定大小的一个单元，每个单元称为一个页
- 把物理内存看成是定长槽块的阵列，叫作页帧

![image-20230726180817486](https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/blog/note/image-20230726180817486.png)

> - 虚拟内存页4个，每个大小16字节
> - 物理内存页帧8个，每个大小也是16字节
> - 如果操作系统希望将 64 字节的小地址空间放到 8 页的物理地址空间中，它只要找到 4 个空闲页。操作系统保存了一个所有空闲页的空闲列表（free list），只需要从这个列表中拿出 4 个空闲页。在这个例子 里，操作系统将地址空间的虚拟页 0 放在物理页帧 3，虚拟页 1 放在物理页帧 7，虚拟页 2放在物理页帧 5，虚拟页 3 放在物理页帧 2。页帧 1、4、6 目前是空闲的。

#### 6.1 页表

- 操作系统为每个进程保存一个数据结构，记录地址空间的每个虚拟页放在物理内存中的位置，称为**页表（page table）**
- 主要作用是为地址空间的每个虚拟页面保存地址转换（address translation），让我们知道每个页在物理内存中的位置
- 对应的例子有四个条目（虚拟页 0→物理帧 3）、（VP 1→PF 7）、 （VP 2→PF 5）和（VP 3→PF 2）

#### 6.2 虚拟地址到物理地址转换

> 首先将虚拟地址分成两个组件：虚拟页面号（virtual page number，VPN）和页内的偏移量（offset）。

- 进程的虚拟地址空间是64字节，2^6，需要6位来表示
- 虚拟页的数量为4个，2^2，需要2位来表示
- 每个页的大小为16字节，2^4，需要4位来表示
- 最高位Va5，最低位Va0
- 于是例子中的虚拟地址表示如下，前两位为页号，后4位为每个页面内的偏移量

<img src="https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/blog/note/image-20230726182834419.png" alt="image-20230726182834419" style="zoom:50%;" />

> - 虚拟地址`21`，二进制`为010101`，则页号位第一页，偏移量为5，也就是第一页第5个字节处
> - 根据页表，找出属于第7个物理页，因此可以通过替换虚拟页号为物理页号，进而找到物理地址

<img src="https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/blog/note/image-20230726183239264.png" alt="image-20230726183239264" style="zoom:50%;" />

#### 6.3 页表存放内容

- 假设页表是一个数组，通过虚拟页号（VPN）检索到页表项（PTN），最终定位到物理页号（PFN）
- 有效位（valid bit）通常用于指示特定地址转换是否有效。例如，当一个程序开始运行时，它的代码和堆在其地址空间的一端，栈在另一端。所有未使用的中间空间都将被标记为无效（invalid），如果进程尝试访问这种内存，就会陷入操作系统，可能会导致该进程终止
- 保护位（protection bit），表明页是否可以读取、写入或执行。以不允许的方式访问页，会陷入操作系统
- 存在位（present bit）表示该页是在物理存储器还是在磁盘上（即它已被换出，swapped out）
- 脏位（dirty bit）也很常见，表明页面被带入内存后是否被修改过

![image-20230726184526113](https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/blog/note/image-20230726184526113.png)

> x86 架构的示例页表项[I09]。它包含一个存在位（P），确定是否允许写入该页面的读/写位（R/W） 确定用户模式进程是否可以访问该页面的用户/超级用户位（U/S），有几位（PWT、PCD、PAT 和 G）确定硬件缓存如何为这些页面工作，一个访问位（A）和一个脏位（D），最后是页帧号（PFN）本身



#### 6.4 芯片内的快速地址转换（地址转换旁路缓冲存储器 （translation-lookaside buffer TLB）

> 背景：页表的信息一般需要放在物理内存中，进行地址转换，需要在内存中查一次页表，进行一次内存读取，影响性能

- 地址转换缓存（address-translation cache），对每次内存访问，硬件先检查 TLB，看看其中是否有期望的转换映射，如果有，就完成转换（很快），不用访问页表（其中有全部的转换映射）
- 首先从虚拟地址中提取出页号（VPN），然后检查TLB中是否有对应页号的转换映射，如果有则命中了TLB，从映射中取出物理页号（页帧号PFN）原来虚拟地址中的偏移量组合形成期望的物理地址（PA），并访问内存
- 如果没有命中TLB，硬件就会访问页表，找到对应的映射，并将映射更新到TLB中，然后重新执行指令
- 连续访问同一个虚拟页中的数据，只会在第一次TLB位命中，后续都会命中，如果TLB足够大，缓存时间足够长，则后续再次访问该虚拟页就都会命中TLB

#### 6.5 更新TLB中内容

- CISC 复杂指令集 x86 架构，由硬件进行更新
- RISC 精简指令集，由操作系统进行更新

#### 6.6 上下文切换TLB的变化

- TLB中存储的映射关系，只对当前进程生效，对其他进程是没有意义的
- 在上下文切换时，操作系统通过某个特权寄存器存储当前进程的地址空间标识符asid（address space identifier）
- 在TLB中增加asid字段，标识每个映射对应的进程，实现跨上下文切换的 TLB 共享

![image-20230727111238640](https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/blog/note/image-20230727111238640.png)

#### 6.7 优化较大的页表

> 32位的地址空间，4KB的页，和4字节的页表项，一个地址空间中大约有一百万个虚拟页面（2^32/2^12）。乘以页表项的大小，发现页表大小为 4MB，通常每个进程都会持有一个页表。如果有上百个进程，页表占用的内存空间就会打到数百兆

##### 6.7.1 简单的解决方案：更大的页

- 以 32 位地址空间为例，这次假设用 16KB（2^14） 的页。因此，会有 18 位的 VPN 加上 14 位的偏移量。假设每个页表项（4字节）的大小相同，现在线性页表中有 2^18 个项，因此每个页表的总大小为 1MB，页表缩到四分之一

##### 6.7.2 混合方法：分段和分页

- 分段依然是代码段，栈段和堆段，依然是用地址空间的前两位表示访问的是哪个段
- 每个段都有一对寄存器
  - 基址寄存器：保存该段的页表的物理地址
  - 限界寄存器：页表的结尾（即它有多少有效页）
- TLB未命中时，用地址空间前两位确定访问哪个段，然后利用基址寄存器存储的物理地址和虚拟页号结合形成页表项

##### 6.7.3 多级页表

- TODO 重新理解

