0%

虚拟内存详解

虚拟内存

虚拟内存的实现是一门复杂的学问,各种细节非常繁杂,对于这些实现的细节我们不必深究,但作为一名软件工程从业者,虚拟内存的设计思想是非常重要的,所以我们依然有必要从整体上对其的设计有一定的了解。本篇文章从以下几个方面来讨论虚拟内存,相信看完后一定会对虚拟内存会有一个总体的认识:

  1. 为什么要有虚拟内存?、物理内存存在什么问题?
  2. 什么是虚拟内存?
  3. 虚拟内存的实现用到了什么技术?
  4. 虚拟内存到物理内存的转换?

为什么需要虚拟内存?

  1. 进程直接访问物理地址会带来不可预估的风险;
  2. 物理内存资源是宝贵的且非常有限;
  3. 内存分配时,连续内存分配会产生很多难以分配的内存碎片。

什么是虚拟内存?

虚拟内存是在物理内存和进程中间的中间层,他为进程隐藏了物理进程的存在,从进程的视角看,它以为自己独享了内存,这个内存空间就是虚拟内存实现的。

其实我们可以把内存管理看作这么一个工作流:虚拟内存其实就是磁盘上的一片空间,当这个空间中的某一部分访问比较频繁的时候,就把这一部分加载到物理内存中,加快访问速度。最后从进程角度上来说,以为系统的内存又大又快,但其实大是通过磁盘实现的,而快是通过将页加载到物理内存实现的。

虚拟内存的实现用到了什么技术?

虚拟内存的实现是通过一整套的机制实现的,其中包括了以下三个比较重要的部分:

  1. 从虚拟内存到物理内存的转换--->页表
  2. 缺页中断
  3. 页面置换算法

下面分别来讨论上面的几种技术:

页表

虚拟地址经过内存管理单元(MMU)的转换变成物理地址,然后再通过物理地址访问内存。一个虚拟地址的低位部分为页内偏移量,高位部分为页号。我们需要做的就是把高位的页号转换为这一页在物理内存中实际的首地址,而页表就可以帮我们实现这个功能。页表就是一个多层的索引,我们通过页表可以获得页首地址,通过拼接低位的页内偏移量,可以得到数据最终的物理地址。

页表的作用:

  1. 实现虚拟地址到物理地址的转换

  2. 碎片化存储(以页为单位)

  3. 内存保护,页表可以存储进程对物理地址的访问权限(读、写、执行等)

  4. 实现了共享内存

    比如在linux中,父进程通过fork系统调用生成子进程时,其中子进程的页表是通过复制父进程的页表得到的,此时父子进程通过同一分页表,共享了物理内存。

注意点:

  1. 内存映射主要由操作系统的内存管理单元(Memory Management Unit, MMU)管理操作的,一切都是同它来掌控。
  2. 页表中有一个有效位,来确定页是否被加载到物理内存中
  3. 虚拟地址也被称为逻辑地址
  4. 低地址的位数?为什么需要偏移地址?以32位系统为例:
    1. 虚拟内存大小:\[2^{32}Byte\]
    2. 因为计算机一次按照4个字节取数据(参考这篇文章),对于地址来说,就有\(\frac{2^{32}}{4}=2^{30}\)个逻辑地址
    3. 如果不做分页,那么页表就有\(2^{30}\)条索引,太多了
    4. 如果我们以4KB分页,那么我们需要\(4KB=2^{12}\),低12位地址作为页内偏移地址,同时也大大减少了页表的索引条目

缺页中断

怎么判断需要的页是否已经load到物理内存中?--->通过判断页表中的映射是否指向硬盘

进程通过虚拟地址访问数据,会先判断数据所在的页是否被加载到物理内存中,如果没有加载,此时就会引发缺页中断,总的来说缺页中断会把缺失的页加载到物理内存中,具体来说:

  1. 如果没有可用的物理内存,会先通过页面置换算法选出需要替换的页;
  2. 如果被替换的页面是脏数据,先把它写回到硬盘中
  3. 从硬盘中读取页面到内存中
  4. 更新页表的映射关系
  5. 跳出异常处理程序,继续从中断指令处开始向下执行

注意点:当缺页中断处理完后,是直接返回数据,还是重新通过页表去读取数据?答案是会重新通过页表去读取数据。

页面置换算法

OPT(最佳置换算法)

定义:淘汰以后不会使用的页面,理论上是最优算法,可以作为衡量的标准。

FIFO(先进先出算法)

定义:淘汰掉最先进入内存的页面;

实现:

  1. 操作系统会维护一个进入内存的时间;
  2. 通过队列实现,每次淘汰队首的页面,把新加入的页面加入队尾

结果:可以预想,这种方式是最简单性能也最不理想/稳定的一种方式。

LRU(最近最少使用算法)

定义:选择最近最长时间未访问过的页面予以淘汰,它认为过去一段时间内未访问过的页面,在最近的将来可能也不会被访问。

实现:为每个页面设置一个访问字段,来记录页面自上次被访问以来所经历的时间,淘汰页面时选择现有页面中值最大的予以淘汰

Clock(时钟置换算法)

定义:也被称为最近未用算法

实现:简单的 CLOCK 算法是给每一帧关联一个附加位,称为使用位。用于替换的候选帧集合看做一个循环缓冲区,并且有一个指针与之相关联。当需要替换一页时,操作系统扫描缓冲区,以查找使用位被置为0的一帧。每当遇到一个使用位为1的帧时,操作系统就将该位重新置为0。

以下两种算法都不常用:

LFU(最不常用算法)

定义:置换具有最小计数的页面。计数值为使用次数。

MFU(最常使用算法)

定义:最经常使用(MFU)页面置换算法是基于如下论点:具有最小计数的页面可能刚刚被引入并且尚未使用。

总结

通过以上部分对虚拟内存原理的大概了解,我们可以知道虚拟内存可以解决我们一开始提出的“为什么需要虚拟内存”的问题:

  1. 虚拟内存可以控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统的安全性。
  2. 虚拟内存可以结合磁盘和物理内存的优势为进程提供看起来速度足够快并且容量足够大的存储;
  3. 虚拟内存可以为进程提供独立的内存空间并引入多层的页表结构将虚拟内存翻译成物理内存,进程之间可以共享物理内存减少开销,也能简化程序的链接、装载以及内存分配过程。

总结起来就是三个部分:提高安全性、结合磁盘和缓存的优势、减少物理内存开销。

参考连接

为什么 Linux 需要虚拟内存

页面置换算法

虚拟内存与物理内存的联系与区别