CSAPP - 9. 虚拟内存
9.1 物理和虚拟寻址¶
9.2 地址空间¶
- 虚拟地址空间,n位,N=2^n,n是虚拟地址长度,64位系统则为64位
- 物理地址空间,m位,M=2^m
x86-64处理器下,64位虚拟内存实际是2^{48}大小,可以用如下命令查看
cat /proc/cpuinfo
# address sizes : 39 bits physical, 48 bits virtual
9.3 虚拟内存作为缓存的工具¶
概念上,虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组,每字节都有一个唯一的虚拟地址,磁盘上数组的内容被缓存到主存上
虚拟页是磁盘和主存交互的基本单位,磁盘上的数据被分割为物理页,虚拟内存也被分割为虚拟页,两者大小相同
页大小为P=2^p字节
页表存储在物理内存中
虚拟页表三种状态
- 未分配: VM系统还没有分配,不占用任何磁盘空间
- 缓存: 已缓存在物理内存的已分配页
- 未缓存: 未缓存在物理内存中的已分配页
9.3.1 DRAM缓存的组织结构¶
物理内存 —— DRAM
9.3.2 页表¶
页表是一个页表条目(Page Table Entry, PTE)的数组
PTE结构为有效位|物理页号/磁盘地址
,有效位表示有没有被缓存,有效位为0时,可能是null->未分配,或磁盘地址->未缓存
9.3.3 页命中¶
9.3.4 缺页¶
缺页 -> 触发缺页异常 -> 调用内核的缺页异常处理程序 -> 选择一个牺牲页 -> 如果牺牲页已经修改,则写回磁盘 -> 更改牺牲页的PTE -> 复制VP到内存中 -> 更新VP对应的PTE -> 返回 -> 重新启动导致缺页的指令
现代操作系统都是按需页面分配,即当有不命中发生的时候,才换入页面
9.3.5 分配页面¶
9.3.6 局部性¶
Linux中getrusage
函数可以检测缺页的数量。link1 link2
9.4 虚拟地址作为内存管理的工具¶
虚拟内存一般大于物理内存
实际中每个进程会维护一个页表
VM有以下好处
- 简化链接: 每个进程使用类似的内存格式,对于64位空间,代码段总是从0x400000开始,数据段跟在代码段之后,中间有一段符合要求的对齐空白,栈则占据用户进程地址空间的最高部分,并向下生长
- 简化加载: 要把目标文件中.text和.data节加载到一个新创建的进程中,Linux加载器为代码和数据分配虚拟页,把它们标为无效,将页表条目指向目标文件中适当的位置即可,每个页只有在初次引用时才会调入数据页,加载器不会从磁盘向内存复制任何数据。将一组连续的虚拟页映射到任意一个文件中的任意位置的表示法称作内存映射,Linux提供名为mmap的系统调用,允许程序自己做内存映射
- 简化共享: 有些代码和数据是需要进程之间共享的,例如每个进程必须调用相同的操作系统内核代码,操作系统会将不同进程中适当的虚拟页映射到相同的物理页面,如下图所示
- 简化内存分配,当一个程序要求额外堆空间时(如调用malloc),操作系统分配一个适当数字个连续的虚拟内存页面,并且将它们映射到物理内存中任意位置的k个任意的物理页面,并不需要物理页面连续
9.5 虚拟内存作为内存保护的工具¶
页表可以在PTE上添加一些额外的许可位来控制进程对虚拟页面的访问权限,违反权限会触发内核中的异常处理程序,Linux一般报告为"Segmentation fault"
- SUP: 进程是否必须运行在内核模式下才能访问该页
- READ: 控制对页面的读
- WRITE: 控制对页面的写
9.6 地址翻译¶
页表翻译 —— 命中
- 处理器生成VP,传给MMU
- MMU生成PTE地址(利用VPN来选择恰当的PTE),向高速缓存/主存请求
- 高速缓存/主存返回PTE
- MMU构造物理地址,并传送给高速缓存/主存
- 高速缓存/主存返回请求的数据字给处理器
没命中
- 相同
- 相同
- 相同
- PTE中有效位是0,MMU触发异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序
- 缺页异常处理程序确定物理内存中的牺牲页,如果已经修改,写回磁盘中
- 缺页异常处理程序调入新的页面,并更新内存中的PTE
- 缺页处理程序返回到原来的进程,再次执行导致缺页的命令,CPU将引起缺页的虚拟地址重新发送给MMU,然后正常执行
9.6.2 利用TLB加速地址翻译¶
TLB在芯片上,因此在TLB可以命中时,所有的地址翻译都是在芯片上的MMU执行的,因此非常快
- CPU产生虚拟地址
- MMU从TLB去除相应的PTE
- MMU将其翻译成物理地址
- 从高速缓存/主存取回对应的数据字
如果不命中,MMU则会取出相应的PTE,存放在TLB中,这时可能会覆盖已经存在的条目
9.6.3 多级页表¶
为了节省空间,将页表列为多级结构,只有第一级页表才需要总是在主存中
下图为linux中多级页表结构,因为linux页大小为4KB(12位),VPN分为4段,每段9位,分别用作一个页表的偏移量
linux中所有和分配了的页相关的页表都驻留在内存中
9.6.4 综合: 端到端的地址翻译¶
以下是一个运行在TLB和L1 d-cache上的小系统
- 内存按字节寻址
- 内存访问针对1字节的字
- 虚拟地址长为14位
- 物理地址长为12位
- 页面大小为64
- TLB为四路组相联,共16个条目
- L1 d-cache为物理寻址、直接映射,行大小为4字节,总共有16个组
接下来翻译虚拟地址0x03d4,虚拟地址分为这几段
- 最后p位是vpo(virtual page offset,虚拟页面偏移),由页面大小决定,和对应的物理地址最后p位一样,直接附在物理地址后面即可
- 前半部分为VPN(Virtual Page Number, 虚拟页号),MMU利用VPN来查询页表,选择PTE
- 有TLB缓存加速时,中间t位为TLB索引,即TLB有T=2^t组,上例中TLB有16/4=4组,因此t=2,前面部分则为TLB标记,用来和TLB中标记位对比
如上,得到TLBI和TLBT后,找到对应PPN为0x0d,物理地址结构也为PPN|PPO
,其中PPO和VPO一样,则拼接得到物理地址
然后,按照高速缓存查询格式,字节偏移为0x0,缓存索引为0x5,缓存标记位0xd,查询得到结果为36
9.7 案例研究: Intel Core i7/Linux内存系统¶
页面大小为4KB的Core i7上VPO和PPO是12位,而八路组相联、物理寻址的L1高速缓存有64个组合大小为64字节的缓存块,所有对物理地址来说,页面偏移位为6,索引位也为6,正好是12位的PPO。因此在拿到VP后,可以同时进行VPN->PPN的翻译和在L1中找到对应组并读出8个标记和数据字
Linux内核中为每个进程维护了一个单独的任务结构task_struct
,里面的一个条目指向mm_struct
,描述内存的当前状态
mm_struct
里,其中两个字段是pgd
和mmap
,前者指向第一级页表的基址,后者指向一个vm_area_structs
的链表
每个vm_area_structs
描述了当前虚拟地址空间的一个区域
9.8 内存映射¶
内存映射: Linux通过将一个虚拟内存区域与一个磁盘上的对象(object),来初始化这个虚拟内存区域的内容
一个虚拟页面被初始化后,就在一个由内核维护的专门交换文件中之间换来换去,交换文件也叫交换空间
9.8.1 共享对象¶
共享对象¶
一个进程将共享对象映射到它的虚拟空间的一个区域内,那么这个进程对这个区域的所有写操作,对于那些也把这个共享对象映射到它们虚拟空间内的进程来说,都是可见的。这些变化也都会反映在磁盘的原始对象中
物理内存中只需要存放共享对象的一个副本
私有对象¶
私有对象采用的是写时复制(copy on write),一个私有对象开始生命周期的方式基本和共享对象一样,在物理内存上只保留一份副本。
对于每个映射私有对象的进程,相应私有区域的页表条目都被标记为只读,并且区域结构被标记为私有的写时复制
一个进程试图写私有区域内的某个页面,那么这个写操作就会触发一个保护故障,故障处理程序会在物理内存中是创建这个页面的一个新副本,更新页表条目指向这个新副本,然后恢复这个页面的可写权限,如下图所示。之后重新执行这个写指令,则写操作可以正常执行
9.8.2 fork函数¶
fork函数就是私有对象的一个使用案例
在当前进程调用fork函数时,内核为新进程创建各种数据结构,并分配给它一个PID,它创建了当前进程的mm_struct、区域结构和页表的原样副本,将两个进程中的