Table of Contents
获取内存的方式:物理地址和虚拟地址
- 物理地址:最简单的方式,用于简单的电子系统
- 虚拟地址:用于所有的现代智能设备
以[[存储器层次结构#磁盘|CPU访问磁盘]]为例:磁盘控制器将磁盘抽象成一系列逻辑块提供给内核,并将内核发送的逻辑块号转换为实际的物理地址
地址空间:
- 线性地址空间:顺序非负整数 {0, 1, 2, 3, …}
- 虚拟地址空间:个虚拟地址{0, 1, 2, 3, …, N-1}
- 物理地址空间:个物理地址{0, 1, 2, 3, …, M-1} 虚拟地址空间比物理地址空间大.
为什么使用虚拟内存(VM:Virtual Memory)?
- 作为缓存高效使用内存
- 虚拟内存是存储在磁盘上的数据的DRAM缓存
- 简化内存管理
- 每个进程具有相同格式的线性虚拟地址空间:代码和数据总是加载到固定的地址,但实际上那些地址对应的内容分布在整个内存里.
- 独立的地址空间
- 一个进程不能使用另一个进程的内存
- 用户进程不能访问内核特权信息和代码
缓存的工具 Link to 缓存的工具
从概念上讲,虚拟内存是存储在磁盘上的字节序列,存储在磁盘上的虚拟内存的内容缓存在DRAM中.每个缓存的块称为页(字节). 有一种映射告诉我们哪些页面已被缓存.
DRAM缓存组织 Link to DRAM缓存组织
DRAM比SRAM慢10倍;磁盘比DRAM慢10000倍. 所以:
- 虚拟内存有很大的页面:4KB/4MB
- [[高速缓存#全相联高速缓存|全相联]]
- 任何VP能放在任何PP
- 需要复杂的映射函数(页表)
- 复杂的替换算法
- 不会采取write-through而是write-back
在我的Windows笔记本电脑上,“Win+R”输入”msinfo32”查看系统信息:
有页的信息
页表(Page Table) Link to 页表(Page Table)
页表是内存中的一个数组数据结构,将虚拟页(VP)匹配到物理页(PP).每个进程都有自己的页表.
页命中(Page Hit) Link to 页命中(Page Hit)
要访问的数据在物理内存中(DRAM 缓存命中) 上图中,CPU发来一个虚拟内存地址2,MMU去查找页表中第2条,得到其物理地址,内存返回物理地址的数据.
页错误(Page Fault) Link to 页错误(Page Fault)
要访问的数据不在物理内存中(DRAM缓存不命中),触发[[异常控制流(异常和进程)#页错误|页错误]]. 上图中,CPU发来的虚拟内存地址3触发页错误异常.内核处理页错误的代码决定替换的页面VP4,然后从磁盘中取出该页面VP3,加载到内存中,更新此页表条目.缺页处理程序返回到原来产生错误的指令处重新执行.
分配新页面 Link to 分配新页面
使用 malloc
分配了一大块虚拟地址空间,如果其中一个页面未分配,那么内核通过 sbrk
系统调用来分配该内存.sbrk
函数的功能是在磁盘中分配一个新的页面,修改此页表条目. 上图中为VP5分配了一个新页面.
局部性的体现 Link to 局部性的体现
在任何时间点,程序倾向于使用的活跃虚拟页称为工作集(working set). 程序的局部性越好,工作集越小. 如果工作集 < 主存,必命中. 如果SUM(工作集)>主存,页面来回替换
内存管理的工具 Link to 内存管理的工具
每个进程拥有自己的虚拟地址空间,内核通过给每个进程提供独立的页表来实现这一点. 在虚拟内存中的页面可以映射到DRAM物理地址空间的任何位置.可能存在着多对一的关系.
虚拟内存是物理内存的一种抽象视图,我想起了在数据库中,数据表可以为不同的用户提供视图,根据他们的需求和权限,也达到了再组织的功能.我认为虚拟内存在这一点上是跟数据库的视图是相似的,物理内存对应于数据库的表格,不是非常整齐;虚拟内存对应视图,无需将表格数据重新复制一份而是从表格中取出;不同的进程对应不同用户,拥有不同需求和权限.
如果设想没有这一个抽象层,程序在链接时是不知道程序加载时数据会放在哪里,寻址无从谈起.如果内存为每个程序都提前分配一个固定内存空间的话,在程序加载前无法得知程序的大小,会造成空间的浪费.虚拟内存就是解决了这个问题.程序加载时才创建页表,将相对地址映射到内存的物理地址. 不同的页可以映射相同的内存地址,也实现了[[链接#动态链接库/共享库(.so文件)|共享库]]的功能!
简化链接和加载过程 Link to 简化链接和加载过程
- 链接 通过虚拟内存这一层,现在,链接器可以假设每个程序加载到相同的位置,所以链接器可以[[链接#连接器的行为|重定向]]
- 加载
execve
为[[链接#可执行可链接格式(ELF)|.text和.data section]]分配虚拟页,并创建页表,每一个元素都标记为无效(uncached,需要时再去复制到内存里,这样就节省了启动时间和内存空间).
内存保护的工具 Link to 内存保护的工具
在PTE的地址前拓展权限位,MMU在每一次访问前检查这些权限位,确保操作合法.
地址翻译 Link to 地址翻译
页表实现地址翻译 回想缓存的知识,由于这是全相联缓存,所以无需set位,只有tag位和offset位.
页命中 Link to 页命中
- 处理器发送虚拟地址给MMU
- MMU发送PTE地址给页表
- MMU接到PTE
- MMU发送物理地址给缓存/内存
- 缓存/内存发送数据给处理器
页错误 Link to 页错误
- 处理器发送虚拟地址给MMU
- MMU发送PTE地址给页表
- MMU接到PTE
- 无效,触发页错误异常
- 替换,写回磁盘
- 读取需要的页,更新PTE
- 返回原指令,重新执行
从内存中获取的内容都要经过[[存储器层次结构|缓存层次结构]],所以实际上的过程是这样的(只列出L1缓存):
转换后备缓冲区(TLB:Translation Lookaside Buffer) Link to 转换后备缓冲区(TLB:Translation Lookaside Buffer)
页表的条目缓存在MMU内的一个硬件缓存TLB中.
- 是[[高速缓存#组相联高速缓存|组相联高速缓存]]
- 映射:虚拟页码->物理地址
跟套娃一样hhh
TLB命中 Link to TLB命中
TLB不命中 Link to TLB不命中
TLB miss发生的频率很低
多级页表 Link to 多级页表
还在套娃XD
上面的页表结构中,每一个VP对应有一个页表的条目.如果一个程序有很多VP未分配,页表就有很多项是无效的,这会造成页表空间的浪费.使用多级页表来解决这问题. 上图的虚拟内存中,VP0
VP1023,VP1024VP2047这2K页有内容,分别用两个二级页表来存储.从VP2048开始有6K未分配页面,不设二级页表.最后1024页,前1023个未分配,最后一页是栈,设二级页表,该二级页表的最后一个指针指向VP9215,其他指针是空指针. 对于k级页表,将VPN划分成k段,像一棵树一样一次逐级查找,最后取得物理地址. 一般是4级页表.根据局部性,页表缓存在TLB中,倒也不会增加太多开销,反而能缩小页表的大小.
啊啊啊好绕要绕晕了…