### 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. 分段 > 出现原因: > > - 基于硬件的地址转换太过于浪费内存,在堆和栈之间的内存,并没有被完全使用,但是依然占用了物理内存 > - 剩下的内存如果连续范围不够大,就会造成进程无法运行 - 原理:给内存中的每个段都分配一对基址+限界寄存器 - 代码段 - 栈段 - 堆段 - 分段的机制使操作系统可以将不同的段放在物理内存不同位置 image-20230725204859460 #### 5.1 怎么确定当前地址访问哪个段 - 地址二进制前两位,用来表示代码段,栈段和堆段 - 用两位表示3个短,空间浪费 - 还可以用地址的产生方式来表示 - 如果地址由程序计数器产生(即它是指令获取),那么地址在代码段 - 如果基于栈或基址指针,它一定在栈段。其他地址则在堆段 #### 5.2 段共享 - 硬件和操作系统在地址中加标识,可以用来共享段 - 代码段共享,每个进程都可以访问执行代码段中指令 - 物理内存中的一个段可以映射到多个虚拟地址空间 #### 5.3 分段机制缺陷 - 空间割裂,造成内存浪费 - 每个段之间的空闲空间发利用 - 空间大小不一致,内存无法分配 - 进程需要一个24KB的空间,但是分段后没有一个足够大的内存空间来支撑分配 image-20230726113528993 > 上面展示了该问题的一个例子。在这个例子中,全部可用空闲空间是 20 字节,但被切成 > 两个 10 字节大小的碎片,导致一个 15 字节的分配请求失败 #### 5.4 空闲空间管理 > 空闲空间列表:将所有空闲的空间统计到一起,方便统计和分配空间 > > image-20230726114634791 - 分割与合并 - 分割:此时如果需要分配大小为1个的空间,将会从前后两个free空间中分配1个,假设分割20后的空闲空间,则空闲列表的第二个空闲空间将会变成21 image-20230726114850318 - 合并:此时如果中间使用的10个空间释放了,则空闲列表会进行合并为一个节点,而不是变成三个节点 - 合并前,三个连续的10个大小的空闲空间,只能分配小于等于10空间给其他进程 - 合并后,可以分配更大的空间给其他进程 image-20230726115025892 #### 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位为每个页面内的偏移量 image-20230726182834419 > - 虚拟地址`21`,二进制`为010101`,则页号位第一页,偏移量为5,也就是第一页第5个字节处 > - 根据页表,找出属于第7个物理页,因此可以通过替换虚拟页号为物理页号,进而找到物理地址 image-20230726183239264 #### 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 重新理解