为什么操作系统需要虚拟内存

前言

在计算机中,CPU执行程序之前,得先把程序的内容加载到内存中一段连续的空间里,这样CPU才能根据内存中排列好的指令顺序执行。

因此,当你同时开启很多程序,他们在内存的分布如下图所示。

之后每当有新启动的程序,系统就会从剩余的内存中分配一段连续的空间给他,而若有程序结束了,那系统也会把他占用的内存清除掉。

虽然这样的做法听起来很美好,但实际上却经常遇到内存碎片化的问题。

内存碎片化

内存碎片化简单来说就是虽然剩余的空间总量够大,但因为那些空间被切割成大大小小的区块,导致没有一段足够大的连续空间可以使用。

以上图来说,原本我的内存最右侧还剩下 3GB 可以用,如果我再把 VSCode 关掉,那就会有 6GB 的空闲内存。

纵使有 6GB 的空闲内存,但如果现在想打开一个 4GB 应用,系统就会因为找不到连续的 4GB 而无法打开

而且一般在使用电脑时程序都会开开关关,所以碎片化的问题会越来越严重。虽然看似有很多空闲内存,由于这些空闲内存分散无法合并成连续的 4GB,所以什么程序都运行不了。

内存虚拟化

为了解决碎片化的问题,现在的操作系统都会使用内存虚拟化 方案,也就是给每个进程一块独立的虚拟内存 (Virtual Memory),然后将其映射到的物理内存(Physical Memory)中。

操作系统会分别为它们分配一大块虚拟内存,使得它们感觉像是各自拥有完整、连续的内存空间来使用,但实际上这些空间在物理内存中可能是分散存储的。

比如现在我同时打开了 Firefox 跟 Chrome,操作系统会分别为它们分配一大块虚拟内存,使得它们感觉像是各自拥有完整、连续的内存空间来使用。如此一来 Firefox 跟 Chrome 就会觉得自己拿到的内存是连续的一大块 ,但实际上这些空间在物理内存中可能是分散存储的。

再换句话说,如果你今天写程序声明了一个超大的数组, 逻辑上你确实拿到一块很大的连续空间,但实际上那个巨大数组在物理内存中是分散的 ,不过程序本身察觉不到这一点。

Memory Management Unit(MMU)

虽然 内存虚拟化 听起来完美解决了碎片化的问题,但如果每次程序要去存取内存时,操作系统都要花时间把虚拟地址(Virtual Address)转成物理地址(Physical Address),那程序跑起来就会慢很多。

为了解决这个问题,从 1980 年代开始的电脑都会加上一块硬件叫 MMU,大概长下面这样。

这个 MMU 内部有一个 page table 记录了虚拟/物理地址的对应关系 ,当程序试图访问某个变量时,CPU 就会马上叫 MMU 去找对应的物理地址,由MMU迅速查找并返回该变量的实际物理地址,然后CPU再从物理内存中读取数据。 通过硬件实现地址转换,尽管性能会有所下降,但相比纯软件方式,已经极大地减少了额外开销。

其它好处

前面有提到一开始做 内存虚拟化 是为了解决碎片化的问题,但除此之外还带来不少其他好处。

进程间共享物理内存

在开发过程中,我们常遇到不同程序需要打开同一文件的情况,例如同时用 node app.js 运行程序,又用 VSCode 编辑 app.js。这时,操作系统只需加载一次 app.js,并将两个进程中对 app.js 的虚拟地址映射到相同的物理内存区域,实现资源共享。

除了文件之外,很多程序会共同使用一些常用的动态链接库,如 Mac 系统下的 ls、cat 等命令都需要 libSystem.B.dylib。系统会在内存充足的情况下持续保留这类常用库在内存中,方便后续进程快速地调用。

按需加载

在有虚拟内存之前,要执行一个程序往往需要把整个程序加载进内存。

但仔细想一想, 每个程序都有很多地方根本不太会被执行到 :比如说有些代码的功能是在程序崩溃之时把 stack trace 印出来、有些则是在服务异常时发送 slack 通知给开发人员。 如果这些例外状况极少发生,那把整个程序都加载内存内显然不是个好主意

系统只会把当前马上要用到的部分加载至物理内存,而那些不太可能被执行到的部分(比如异常处理函数或极少使用的代码)则暂时不用加载。这种按需加载的方式被称为懒加载,可减少程序启动等待时间,并避免少数大型程序占用大量内存。

交换(Swapping)

当所需内存过大以至于物理内存不足时,系统会采取交换机制,即将曾经使用过但短期内可能不再需要的内存内容临时移出到硬盘上。例如,程序启动初期执行过的init()函数或偶尔才调用的error_handler()函数,在内存紧张时会被换出到硬盘,需要时再载入。

比如说程序刚启动时要跑的 init() 、偶尔才跑一次的 error_handler()他们都曾经被执行过所以一定有被加载内存 。但如果可用的内存快没了,系统就会把他们 swap 出去(没用的东西都给我滚),哪天需要时再从硬盘拿回来就好

有了 swapping 机制后虽然可以增进内存的使用效率,而且内存绝对不会不够用(说穿了就是拿硬盘当扩充内存),虽然交换机制可以有效扩大内存利用率,但如果频繁进行交换操作,由于硬盘速度远低于内存,会导致系统整体性能显著降低。

那怎么知道系统用了多少 Swap 呢?看 htop 就可以了。我的 htop 打开后会看到 Swp 是 0/1023MB,意思是系统没有把任何内存 swap 到硬盘上(因为我的 Mem 还够用),但如果需要的话最多可以把 1023MB 的内存 swap 出去,等需要时再拿回来就好

如果没有装 htop 的话,top 最上面也有 swap in 跟 swap out 可以看目前用了多少 swap 哦~

那知道 Swap 使用量可以做什么呢?刚刚有提到说频繁的做 swapping 会导致性能变差,因此如果常常觉得电脑、主机慢到炸裂,开个浏览器一分钟才跳出来,而且刚好 Swap 的使用量又很高,那就很有可能是内存不足,快帮你的机器升级吧~

总结

回到这篇的主题,为什么需要多加一层虚拟内存呢?我想现在大家都知道了。总的来说 虚拟内存 就是在 物理内存应用程序 之间加上一个中间层,这一层允许操作系统悄无声息地进行各种内存优化操作,如共享内存、延迟加载和内存交换,而应用程序只需专注于自身的逻辑,无需关心具体数据何时加载进内存或者何时被交换出去,一切交给操作系统来妥善处理。

相关推荐
2401_882727573 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
追逐时光者4 小时前
.NET 在 Visual Studio 中的高效编程技巧集
后端·.net·visual studio
大梦百万秋5 小时前
Spring Boot实战:构建一个简单的RESTful API
spring boot·后端·restful
斌斌_____5 小时前
Spring Boot 配置文件的加载顺序
java·spring boot·后端
路在脚下@5 小时前
Spring如何处理循环依赖
java·后端·spring
海绵波波1076 小时前
flask后端开发(1):第一个Flask项目
后端·python·flask
小奏技术7 小时前
RocketMQ结合源码告诉你消息量大为啥不需要手动压缩消息
后端·消息队列
AI人H哥会Java9 小时前
【Spring】控制反转(IoC)与依赖注入(DI)—IoC容器在系统中的位置
java·开发语言·spring boot·后端·spring
凡人的AI工具箱9 小时前
每天40分玩转Django:Django表单集
开发语言·数据库·后端·python·缓存·django
奔跑草-9 小时前
【数据库】SQL应该如何针对数据倾斜问题进行优化
数据库·后端·sql·ubuntu