五分钟扫盲:程序在计算机中是如何运行起来的

497 阅读12分钟

🎓 尽人事,听天命。博主东南大学硕士在读,热爱健身和篮球,乐于分享技术相关的所见所得,关注公众号 @ 飞天小牛肉,第一时间获取文章更新,成长的路上我们一起进步

🎁 本文已收录于 「CS-Wiki」Gitee 官方推荐项目,现已累计 1.6k+ star,致力打造完善的后端知识体系,在技术的路上少走弯路,欢迎各位小伙伴前来交流学习

🍉 如果各位小伙伴春招秋招没有拿得出手的项目的话,可以参考我写的一个项目「开源社区系统 Echo」Gitee 官方推荐项目,目前已累计 600+ star,基于 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 并提供详细的开发文档和配套教程。公众号后台回复 Echo 可以获取配套教程,目前尚在更新中


本文并不会深入底层寄存器指令与硬件等内容,只是借此主题为后续内存管理章节作扫盲,并帮助各位对分层存储体系有一个整体的把握,以及一个程序在这样的存储体系上,究竟是如何运行起来的,这些存储器在这个过程又分别扮演了什么角色。

为什么设计了分层存储体系

其实早在操作系统系列开篇的那篇文章中,我就简单介绍过分层存储体系(Memory Hierarchy),先来回顾下(这里我参考的是《现代操作系统 — 第三版》这本书,其他有些资料可能会划分的更为细致,不过在理解本文的目的上,以下划分已经足够清晰了):

各位不妨想一想,为什么需要设计出这样一个分层的存储体系来供计算机使用呢

首先,有一个观点不用多说,每个人都希望自己的计算机拥有这样一个存储器:它容量无限大,CPU 访问它的速度无限快,并且能够永久性的存储数据(断电不会丢失数据),而且价格不能太高昂。

But,遗憾的是,现阶段的技术仍然满足不了我们的幻想。

为此,经过多年的探索,人们提出了分层存储体系的概念,把我们的幻想一个一个拆开对应到不同的存储器上。

在这个体系中,计算机拥有若干 KB 超级快速、超级昂贵且易失性的寄存器;若干 MB 快速、昂贵且易失性的高速缓存(cache);若干 GB 的速度与价格都适中、且同样易失性的内存;以及若干 TB 低速、廉价、非易失性的磁盘存储;另外还有诸如磁带等可移动的存储装置。

至于为什么这些存储器的造价不同,那就和它们的成本、材料、制作工艺等息息相关了。寄存器超级快速且超级昂贵的原因就是它和 CPU 的制作材料是相同的,所以 CPU 访问起来几乎是没有时延的。

另外,这里多提一嘴内存,关于内存的分类众说纷纭,我觉得各位对内存的概念有大体的把握就行,不必过度死扣细节。

内存也经常被人们称为主存和随机访问存储器(Random Access Memory,RAM),还有我们上文说到内存是易失性的,其实这都不是绝对正确的,只不过是通俗的说法并且被大众所认可。

现在许多计算机都已经在使用少量的非易失性随机访问存储器,也就是只读存储器(Read Only Memory,ROM),它在工厂中就被编程完毕,然后再也不能被修改。ROM 速度快并且造价低廉,很多开发商都会把用于启动计算机的引导加载模块存放在 ROM 中。

装入内存,让程序跑起来

这里我先开门见山的总结一下内存和磁盘的区别,也方便大家更好的理解下面的例子。通俗来说,内存决定了你的计算机能够同时流畅运行多少个应用程序,而磁盘决定了你的计算机能够下载安装多少个应用程序。

举个例子,这里面涉及到一个很重要的概念,各位认真看哈:

比方说你的计算机上安装了 WeChat,你双击了 WeChat 快捷方式,操作系统就会打开 WeChat 软件。

那么,各位有没有想过,在分层的存储体系上,WeChat 或者各种应用程序在你的计算机上究竟是怎么跑起来的呢?这些存储器在这个过程又分别扮演了什么角色呢?

首先,有一点你要明确,你安装的 WeChat 软件是保存在磁盘中的。软件安装的本质是什么?各位应该也都明白,就是将很多数据的集合存储到磁盘上。

双击 WeChat 快捷方式,操作系统就会知道你要运行这个软件,它会在磁盘中找到你安装的 WeChat 软件,将运行所需要的数据从磁盘中复制到内存里。注意这里!WeChat 不是直接磁盘中运行的,而是在内存中运行的。

至于原因,那当然是内存的读写速度比磁盘快得多。

所以,为了缓和磁盘之间的速度不匹配问题,程序执行前必须将硬盘上的数据复制到内存,CPU 才能够着手处理,这个过程就叫作载入内存(Load into Memory),完成这个过程需要一个不可或缺的程序:载入器或者说加载器(Loader)。

CPU 直接与内存进行交互,它会读取内存中的数据进行处理,并将结果返回并保存到内存。当然,如果你还需要将数据保存到磁盘,复制操作也会在内存和磁盘直接发生一次。

比如说,我们打开了某个 Word 文档,并输入了一些文字,虽然我们直观看到的已经发生变化了,但是磁盘中存储的文档仍然没有改变,它仍然是之前的数据,新增的文字只是暂时保存到了内存,只有我们手动保存了这篇文档比如 Ctrl + s 才会将修改保存到磁盘中。

而由于内存是易失性的,也就是说断电后数据就丢失了,所以如果你编辑完 Word 文档忘记保存或者断电导致关机了,那么你将永远无法找回这些内容(悲剧 😣)。

寄存器与高速缓存去哪里了

读到这里,各位肯定会有所困惑,上面不是说了还有高速缓存吗?还有寄存器吗?它们不是也用来存储的吗,在这个过程中它们去哪里了?

OK,从 CPU 的角度来说,内存是什么?就是一个笨逼,它仅仅是一个存放指令和数据的地方,计算机并不能在内存中完成计算功能。

若把 CPU 比作人类的灵魂,内存和磁盘就是人类的躯体。没有了 CPU,内存和磁盘就沦为一具行尸走肉。当然,没有了内存和磁盘也不行,毕竟 CPU 的存储能力非常可怜。

比如说我们要计算 a = b + c,必须将变量 a、b、c 从内存中读取到 CPU 内部才能进行加法运算,而在 CPU 中执行运算工作的部件,就是运算单元,可以说运算单元就是 CPU 的大脑。

我们不妨来看一下 CPU 的结构:

可以看出,寄存器(Register)和高速缓存都直接内嵌在 CPU 中

先说寄存器,其造价高昂且容量有限,功能涵括数学运算、控制程序的执行流程、标记 CPU 运行状态等。

有些同学可能认为寄存器就是一个部件,其实不然,现代的 CPU 基本都内置了几十个甚至上百个寄存器,根据功能的不同,这些寄存器也拥有不同的名字。

例如,EAX 寄存器通常被用在加法运算中,用来保存某个加数或运算结果;EIP 寄存器中存储的值是下一条指令的地址,CPU 执行完当前指令后,就会根据 EIP 的值去寻找下一条指令,也就是说如果我们改变 EIP 寄存器的值,也就会相应的改变程序的执行流程。

另外,寄存器存放的是二进制数据,我们常常说 32 位或者 64 位的 CPU,其实指的就是寄存器的位数。

再来看缓存,为啥 CPU 里面还要弄个缓存呢?那当然还是离不开读写速度的问题,虽然 CPU 访问内存的速度相比于磁盘来说已经足够快了,But,真要和高速缓存做个对比,那根本就不是一个数量级的,蚍蜉撼树罢了。

如果 CPU 每次都从内存中读取数据,会严重拖慢其运行速度,CPU 不得不浪费大量的时间来等待内存中数据操作的结束。为此,我们在 CPU 内部设置一个缓存,将使用频繁的数据暂时从内存中读取到缓存里来,这样,如果缓存命中,就直接从缓存中读取即可,不必访问内存了。

所以很显然,缓存容量越大的 CPU 其性能就越好。当然,毕竟造价高昂,搞不起超大容量的缓存,大小有限,所以对于缓存中数据取舍的种种算法又是一门值得深究的话题。

另外,缓存的概念其实并不仅仅局限于此。只要存在大量的资源可以划分成小的部分,那么,这些资源中的某些部分就会比其他部分更频繁的得到使用,使用缓存就可以带来性能上的改善。在操作系统中,除了 CPU 中的缓存,内存和硬盘之间其实也有缓存的概念,也就是下面我们将要讨论的虚拟内存。

虚拟内存和局部性原理

虚拟内存(Virtual Memory)、虚拟存储器,这俩其实是一个东西,只不过网络上各种博客说法不一样,没有统一起来,容易让萌新摸不着头脑。

上文说的种种只是在运行一个 WeChat 程序的情况下,各位不妨想一下,如果你的电脑内存只有 4G,你同时运行了 WeChat、QQ、网易云音乐、Chrome 等等很多应用程序,假设它们加起来一共需要 5G 的内存空间,也就意味着需要从磁盘复制 5G 的数据到只有 4G 存储空间的内存,显然,这是不可能的。

为此,操作系统中引入了虚拟内存的概念。当程序运行需要的存储空间大于内存的容量时,会将内存中暂时不用的数据写回磁盘,当需要这些数据时再从磁盘中重新读取,而内存中存放的数据也就是所谓的热点数据。这样,磁盘中就会有一部分空间用来存放内存中暂时不用的数据,这一部分空间就叫做虚拟内存。我们上面所说的情况就需要在磁盘上分配(5 - 4 = 1G)的虚拟内存。

这样,虚拟内存中 "虚拟" 二字的含义也就不言而喻了。只有 4G 内存,但是可以正常运行占用内存超过 4G 的应用程序,在用户看来他似乎拥有了一个比实际内存大得多的内存。当然,实际的物理内存大小并没有发生改变,只是在逻辑上进行了扩充。

可以这么理解,引入虚拟内存的概念后,在内存和磁盘之间,内存就充当了缓存的作用。CPU 优先从内存中获取数据,如果命中,就不需要去访问磁盘。这里和我们上文所说的 CPU 中的缓存和内存之间的关系是不是一模一样?

当然,我们为什么可以这样做?这些数据为啥就能被调入缓存/内存呢?虚拟内存存在的理论支撑是什么?这就是著名的局部性原理。局部性原理包含两种,时间局部性和空间局部性:

1)时间局部性原理:如果 CPU 执行了程序中的某条指令,那么不久后这条指令很有可能再次执行;如果某个数据被访问过,不久之后该数据很可能再次被访问;

2)空间局部性原理:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问,因为很多数据在内存中都是连续存放的,并且程序的指令也是顺序地在内存中存放的。

🎉 关注公众号 | 飞天小牛肉,即时获取更新

  • 博主东南大学硕士在读,利用课余时间运营一个公众号『 飞天小牛肉 』,2020/12/29 日开通,专注分享计算机基础(数据结构 + 算法 + 计算机网络 + 数据库 + 操作系统 + Linux)、Java 技术栈等相关原创技术好文。本公众号的目的就是让大家可以快速掌握重点知识,有的放矢。希望大家多多支持哦,和小牛肉一起成长 😃
  • 并推荐个人维护的开源教程类项目: CS-Wiki(Gitee 推荐项目,现已累计 1.6k+ star), 致力打造完善的后端知识体系,在技术的路上少走弯路,欢迎各位小伙伴前来交流学习 ~ 😊
  • 如果各位小伙伴春招秋招没有拿得出手的项目的话,可以参考我写的一个项目「开源社区系统 Echo」Gitee 官方推荐项目,目前已累计 600+ star,基于 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 并提供详细的开发文档和配套教程。公众号后台回复 Echo 可以获取配套教程,目前尚在更新中。