Wed Feb 05 2025
2321 words · 12 minutes

虚拟内存(概念)


Table of Contents

获取内存的方式:物理地址和虚拟地址

  • 物理地址:最简单的方式,用于简单的电子系统
  • 虚拟地址:用于所有的现代智能设备 以[[存储器层次结构#磁盘|CPU访问磁盘]]为例:磁盘控制器将磁盘抽象成一系列逻辑块提供给内核,并将内核发送的逻辑块号转换为实际的物理地址

地址空间:

  • 线性地址空间:顺序非负整数 {0, 1, 2, 3, …}
  • 虚拟地址空间:N=2nN=2^n个虚拟地址{0, 1, 2, 3, …, N-1}
  • 物理地址空间:M=2mM=2^m个物理地址{0, 1, 2, 3, …, M-1} 虚拟地址空间比物理地址空间大.

为什么使用虚拟内存(VM:Virtual Memory)?

  • 作为缓存高效使用内存
    • 虚拟内存是存储在磁盘上的数据的DRAM缓存
  • 简化内存管理
    • 每个进程具有相同格式的线性虚拟地址空间:代码和数据总是加载到固定的地址,但实际上那些地址对应的内容分布在整个内存里.
  • 独立的地址空间
    • 一个进程不能使用另一个进程的内存
    • 用户进程不能访问内核特权信息和代码

缓存的工具 Link to 缓存的工具

从概念上讲,虚拟内存是存储在磁盘上的字节序列,存储在磁盘上的虚拟内存的内容缓存在DRAM中.每个缓存的块称为(P=2pP=2^p字节). 有一种映射告诉我们哪些页面已被缓存.

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 页命中

  1. 处理器发送虚拟地址给MMU
  2. MMU发送PTE地址给页表
  3. MMU接到PTE
  4. MMU发送物理地址给缓存/内存
  5. 缓存/内存发送数据给处理器

页错误 Link to 页错误

  1. 处理器发送虚拟地址给MMU
  2. MMU发送PTE地址给页表
  3. MMU接到PTE
  4. 无效,触发页错误异常
  5. 替换,写回磁盘
  6. 读取需要的页,更新PTE
  7. 返回原指令,重新执行

从内存中获取的内容都要经过[[存储器层次结构|缓存层次结构]],所以实际上的过程是这样的(只列出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未分配,页表就有很多项是无效的,这会造成页表空间的浪费.使用多级页表来解决这问题. 上图的虚拟内存中,VP0VP1023,VP1024VP2047这2K页有内容,分别用两个二级页表来存储.从VP2048开始有6K未分配页面,不设二级页表.最后1024页,前1023个未分配,最后一页是栈,设二级页表,该二级页表的最后一个指针指向VP9215,其他指针是空指针. 对于k级页表,将VPN划分成k段,像一棵树一样一次逐级查找,最后取得物理地址. 一般是4级页表.根据局部性,页表缓存在TLB中,倒也不会增加太多开销,反而能缩小页表的大小.

啊啊啊好绕要绕晕了…

Thanks for reading!

虚拟内存(概念)

Wed Feb 05 2025
2321 words · 12 minutes