你好,我是 Cone
面试的时候是不是经常面试官被问到一个问题:有了解过虚拟内存吗?那你详细讲讲你了解的虚拟内存吧。我在去年秋招的时候也经常被问题这个问题,那么今天好好来和你一起研究探究这个操作系统的内存管理。
下面就通过一个例子,带你进入操作系统内存管理的世界。
首先,假设我们的内存空间有 64MB,现在我需要运行三个程序,其中程序 A 运行时需要占用的大小为 32MB,程序 B 运行时占用的大小为 30MB,程序 C 运行时占用大小 4MB,那么很显然如果三个程序一起运行,肯定会有地址冲突,因为装不下啊,这只是其中一个问题,还有另外一种问题,程序 A 和 B 一起运行时,如何确保 AB 的程序地址不冲突呢,你可能会想到说,在代码里面写好,让他们不冲突即可,但是那我假如要运行程序 C 呢?是让 B 退出运行还是让 A 退出运行,那程序 C 的地址如何确定,那是不是就得在代码中变化了,虽然 AB两个程序没有出现相互访问的情况,还可以控制,但是程序一旦多起来,就要为了内存地址"打架了"。这个例子中所有的空间地址都硬编码在了程序里,所以每次要改地址的时候就得改程序,这种最原始的地址使用就出现了很多问题。
通过上面的例子想必你已经发现问题了。
也就是如题,三个问题带你彻底解决内存管理,那么是哪三个问题呢?如下图。
针对以上三个问题,我们逐一解决,首先是地址空间不隔离问题。
地址空间不隔离
在计算机学科里有一句名言:
计算机科学领域的任何一个问题都可以增加一个中间层来解决。
对于这类隔离问题也是这样,不如我们在真实的内存地址上增加一层试试看,我们就叫它虚拟地址吧,既然是虚拟地址,那就可以随意让我们来定值了,只要在运行的时候找到一个与之对应的物理地址去运行就可以了。
通过添加虚拟地址,两个运行的程序就无法知道对方的地址(这里暂且不讲进程间通信)。
所以这个问题就这样得到解决,那么新增加了这个虚拟地址到底该如何与真实地址对应上呢?
所以得提供一个 y = f(x) 这样一个转换 关系,这个转换关系在计算机系统设计的时候存在于真实物理地址中,而操作这个转换关系的东西叫做内存管理单元(MMU,Memory Management Unit),具体详细情况,请看图。
MMU 常见情况存在于 CPU 芯片中,也有一些独立于 CPU 的,通过虚拟内存解决了第一个问题,下面来看看第二问题。
地址空间不确定
地址空间不确定的原因就是不知道程序到底应该运行在哪里,有了上面的虚拟内存,其实地址空间也可以确定下来了,不过比较笨,两个程序的虚拟内存是一样的,对应到物理内存的话不一样也可以,那就得全靠MMU了,为了改变这种比较笨的局面以及随着 CPU 的发展,就出现了一种内存管理技术——内存分段。
16 位 CPU 的内存分段
内存分段模型最早出现在 8086CPU 中,8086 的段寄存器只有 16 位,而地址总线有 20 位,为了解决这一不匹配,就引出了代码段寄存器和 IP 指令寄存器,段寄存器左移四位+IP 寄存器的值就是内存地址了,而程序又会分为数据段、代码段、堆等一个个段,每个段都有相应的寄存器对应,比如需要代码段的时候 CS 寄存器就开始工作了、需要数据段的时候 DS 寄存器就开始工作了等等。这也就是最早内存分段模型,计算例子,如下图。
在深入浅出 CPU 的两种工作模式一文中有着详细的介绍 8086 的历史起源、寻址原理等,你可以跳转到那篇文章仔细阅读。
上面提到了程序有会分为很多段,比如数据段、代码段等等,那么这些段是如何来的呢?
这个时候就得引出我们的编译技术了,编译技术其实在没有内存分段的时候已经有了,比如写了那么多汇编指令,得转换成二进制格式的代码,就得用到汇编器了,它的作用你肯定也猜到了,就是将汇编代码转成二进制格式目标文件代码,也就是我们所说的机器语言,汇编器也是最早的编译技术,当然汇编完成之后,还要有链接的过程,链接器的作用就是开始对程序进行分段合并处理,编译汇编的时候会对程序分段也会做一些处理,不过最主要的是在链接过程具体过程如下图,后续会有跟你专题分析编译技术。
没有内存分段的时候汇编器也比较粗糙,就是无脑对应一条条指令翻译汇编代码就好了,因为这个时候的空间地址完全由汇编代码决定,那么内存分段之后,将一个可执行的二进制文件,分成了很多段,这个时候得益于我们编译技术,它帮我们处理了分段模型,最后的二进制文件就是一段一段的。主题与篇幅原因,这里不做展开,牢记二进制是一段一段的就可以了,后续也会专门分享这个二进制文件到底是个什么样子的。
程序既然是一段一段的了,那么 CPU 读取程序指令的时候,不也得有分工,所谓术业有专攻
那么就是我们开始提到的 CS 段读取指令,DS 段读取数据了,还有很多 CPU 的寄存器去做不同的事情,一些常用的寄存器用处,如下图。
我相信你读到这里,肯定豁然开朗,原来内存分段得这么多技术一起配合啊,没错就是需要这么多计算机体系知识一起配合完成,操作系统也是类似,它不仅仅在操作系统层面有难度,编译技术也得更上,所以你也明白自研操作系统的难度了吧,16 位处理还是比较简单的,到 64 位处理起来更加复杂,而计算机就是由国外 16 位发展而来的,甚至 16 位之前还有很多研究,而到国内应用起来就是 32 位、64 位的,没有基础理论的研究,上来就肝 32、64 位的话太难,做一款成熟的操作系统真的太难,好在有华为的鸿蒙,你会说不喜欢用、不好用,但看到这里相信你也会慢慢原谅当前的鸿蒙(我专门体验了很久鸿蒙,感觉还行~),给它时间吧。
扯了一段爱国情怀,继续回到本文主题,16 位的内存分段,已经讲解完了,接下来我们看看 32 位的内存分段。
在进入 32 位内存分段模型前,先简单来看一个问题:
上面我们知道,16 位的寻址方式最大能寻址 cs=0xffff,ip=0xffff 也就是 0x10ffef 这么大了。这个地址算下来也就 1M 多,1M 多是什么概念?你现在打开相机随意拍一张照片可能都比这个大了,实在是太小了,稍有多的东西就放不下了。那怎么办?当然是扩展 16 位到 32 位了。到了 32 位之后,为了兼容原来的 ax,bx 等寄存器,就在高位扩展了 16 位,而低 16 位依然不变,由高位 8AH 和低 8 位 AL 组成,具体为什么要这么做,而不是直接换成两个 16 位寄存器,,那得问 Intel 的工程师们了。扩展成 32 位之后,系统能运行更多的程序,而表示一个程序的内存地址的代价也会增加,表示一个程序内存地址的数据已经不在适合放在 32 位的寄存器中了,因为太大了,放不下,具体是如何放不下的,请接着往下阅读,既然放不下就得寻求新的方式了,这就是 32 位 CPU 内存分段的知识了。
下面进入 32 位 CPU 内存分段一起解决上面提到的问题。
32 位 CPU 的内存分段
话不多说,上图,寻址原理一起看图说话。
上面提到的寄存器放不下这么多大量的信息,所以就得寻求改变,最终就是如上图一样进行了改变,别怕,我带你一起解析这幅图。
这时候虚拟地址由两部分组成,段选择子和偏移量,在CPU 工作模式一文有详细提及。
上图的主要工作流程为:首先虚拟地址中的段选择子会根据自身信息与 GDTR 寄存器配合,在段描述符表中找到需要的段描述符位置,从段描述符的信息中拿到段的起始地址,然后根据虚拟地址中的偏移量进行精确定位到物理地址,这就是分段机制下,从虚拟地址找对于的物理地址方法。
然而是分段类型,具体的分段机制大概是分成:数据段、代码段、堆段、栈段、BSS 段等等,如下图,在映射到物理地址的时候,就是一段一段映射的,这个段就比较大块。
讲解完 32 位 CPU 内存分段模型,你肯定发现了,在程序切换的时候会出现大块内存交换的情况,所谓内存交换就是如果要运行 A 程序要 100MB,但是现在发现连续内存不够了,但是空闲的不联系的内存之和大于 100MB,这个时候操作系统会把这些空闲的内存通过交换的方式得到一片连续的空间,这样 A 就能运行了,具体的话,请看图。
如上图,将中间 40MB 空闲内存和 C 程序运行的 70MB 进行交换,得到 90MB 空闲内存,这时候 A 就行加载进去运行了。20MB、40MB、50MB 这没有运行程序的空闲地址,但又不能运行程序,这就是内存碎片了。
大块内存交换不用想肯定效率低,切换块太大的话还会容易产生内存碎片,下面我们就来讲解如何解决这些问题的。
内存使用效率过低
内存使用效率过低表现为两个方面:
- 内存使用率太低、内存碎片的出现;
- 内存交换速度太慢,大块交换容易卡顿。
所以就出现了内存分页技术,所谓分页技术就是把虚拟内存和物理内存分成固定的大小,每个大小相同的东西叫做页,也就是每一页都是相同大小的,而这个页的大小一般由硬件规格决定,一般可支持多种大小,这个时候操作系统来选择其中一种大小,这就是分页。下面具体来看看分页时的一些寻址原理等知识。
内存分页
这里我们选取 4KB 大小为一页进行讲解 ,这个大小是无所谓的,重点在于原理。
一个例子探究分页
现在,计算机有 32KB 内存大小,假设每页 4KB,也就是从 VP1 到 VP8,我们有 2 个程序 A 和 B,A 运行时被映射到 VP1、VP3、VP5,B 程序运行时需要 VP1、VP2、VP3。开始运行的时候,B 程序的 VP2 没有被映射到物理内存。看图
当程序 B 运行时,需要读取 VP2 时,发现物理内存中,并没有与之对应的内容,这个时候就会触发页错误(Page Fault),就需要进行一次换入(Page In)操作,你肯定也会想到换出(Page Out),这是从内存中保存到磁盘里的操作,了解这一原理,你可能会想到,一次换入操作肯定会有消耗,那打开一个应用或者启动一个程序时,是不是可以减少这类操作从而优化启动耗时呢。你看掌握原理,你的思维有多发散,原理的重要性不言而喻。
上面就是基本的分页两个操作,你会发现,分页完之后,都是一页一页的换入换出操作,非常简便,没有一大段那么沉重了,自然就解决了交换速度太慢的问题,其实不仅于此,是不是还解决了内存碎片的问题呢,把物理内存分成一页一页,用到的时候就一个个换入,这样下来就没有碎片了。
寻址原理
分页时解决了这些问题,那么它是如何寻址的呢?请看图
分页之后,虚拟地址包含页号和偏移量,页号作为页表的索引,页表包含物理页每页所在物理内存的基地址。这个基地址与页内偏移的组合就形成了物理内存地址。
内存分页之后每个页都是很小块,在进行内存交换的时候,使用效率过低的问就解决了,也不会出现大块交换卡顿,大块内存碎片的情况。
总结
本文从没有内存管理时候的三个问题,带你进入内存管理的世界,通过一个个例子,解决了内存管理的空间地址不隔离、不确定、使用效率过低的三个问题。期待更多的图解知识,请长期关注作者。
end
Hello,我是ConeZhang,本科毕业于某不知名双非末流一本,科班CS专业。本科做了四年iOS开发,写过无数iOS应用,拿过无数软件竞赛奖,也折腾过安卓开发,整过Spring全家桶,写过网站,搭过服务器。秋招拿到了微信、抖音等大厂offer,是一段从春招屡战屡败到秋招屡战屡胜的经历。如今在字节跳动抖音基础技术做全栈研发,啥都会点,啥也不会。欢迎大家点个关注长期持有我这只潜力股。在这里,不仅讲讲技术,还写点故事,分享点平平淡淡的编程人生。