重学操作系统(1)

511 阅读49分钟

笔记整理来自拉钩教育

课前必读 | 构建知识体系,可以这样做!

我认为,在学习中有一件非常重要的事情,那就是梳理知识体系,所以在进入操作系统课程的学习之前,我想先给你一份这门课程的知识体系(也是一份学习路径),然后再介绍一套我自己梳理知识体系的方法,帮助你更轻松地学好这门课。

课程内容&知识体系

我们先来看下这门课程的知识体系结构,分为 8 个模块,39 个课时,具体如下。

  • 模块一:(前置知识)计算机组成原理。 如果你对计算机的组成原理中涉及的比如内存、寄存器工作原理、CPU 指令、总线都是怎么工作的这些基本问题,没有搞清楚,大概率会影响你后续对操作系统的学习。因此,在课程开始前,我先来给你一份操作系统的前置知识,帮助你更好地理解后续内容。

  • 模块二:(初探)Linux 指令入门。 这个模块将介绍一些实用的知识,带你入门 Bash 编程,并通过日志分析、性能监控、集群管理等实战场景深入学习 Linux 指令。这些对于日常开发和运维人员来说,都会非常有帮助。

  • 模块三:(总纲)操作系统概述。 这部分帮助你了解操作系统的整体设计,介绍内核、用户空间等基本概念,还会介绍操作系统的分类,以及对比一下市面上的操作系统(如 Windows、Linux、Unix、Android 等),让你对整个操作系统生态能有一个整体的认识。

总的来说,模块四 ~ 模块七是我们这门课程的核心内容,也是面试的重点考区。设置这块内容的目的是借助操作系统的知识,帮你思考如何解决实战问题,比如我们反复提及的高并发、数据一致性、大数据存储和网络问题等。

  • 模块四:(面试重点)进程和线程。 我会针对大家在面试和工作中最常见的并发和数据同步问题,从进程原理、多线程编程、互斥和乐观锁、死锁和饥饿、调度算法、进程通信等多个方面,同时结合一些语言特性(比如 Java 的语言特性)讲解原理、思考方案及对策。

  • 模块五:(面试重点)内存管理。 这部分我们是从页表和 MMU、虚拟化、内存的分配和回收、缓存置换、逃逸分析、三色算法、生代算法等方面入手,帮助你了解内存的工作原理,应对高并发带来的内存使用问题。

  • 模块六:(面试重点)文件系统。 这部分内容我们将从两个方面入手,一方面是通过学习 Linux 的文件目录结构,了解 Linux 下不同的文件目录的功能和作用,帮助你把 Linux 用好;另一个方面,从文件系统的底层设计入手,帮助你了解文件系统的设计思路和原理,并且通过讲解数据库的文件系统,比如 MySQL 的 InnoDb、B+Tree 以及 Hadoop 的 HDFS,帮你把文件系统的知识应用到处理海量数据的领域。

  • 模块七:(面试重点)网络与安全。 这部分讲解面试中常见的互联网协议群、TCP 和 UDP 协议、Linux 的 I/O 模型、公私钥加密体系,以及一些最基本的计算机网络安全知识,帮助你理解操作系统和网络之间的交互,从而更好地利用操作系统知识设计业务系统的网络架构。

  • 模块八:(知识拓展)虚拟化和其他。 最后这部分,我们将从操作系统的角度学习容器化应用(比如 Kubernetes 和 Docker),还会深入讨论 Linux 架构及商业操作系统。这些知识一方面能够帮你和面试官产生更多的共鸣,另一方面还能帮你开拓视野、打开思路,看到未来的发展趋势。

接下来,我给大家梳理一下操作系统整体的知识框架,帮你扫除知识盲区。

从知识结构上来看,操作系统最核心的部分是进程,因为操作系统自己不能提供服务,它要想实现价值,就必须通过安装在系统中的应用程序。而安装好的应用程序,启动后就成了进程,所以说进程处在操作系统知识体系的核心。

了解了以上内容后,我们围绕进程继续梳理,可以发现:

  • 进程往往要同时做很多事情,比如浏览器同时要处理网络、又要处理鼠标、还要展示内容,因此有了多线程的概念。

  • 进程需要执行用的存储空间,比如需要存程序指令、需要堆栈存执行数据,因此需要内存。

  • 进程需要将一部分数据持久的存储下来,因此需要文件系统。

  • 进程需要和外界通信,其中一种途径就是网络。

  • 开发过程中我们希望进程可以单独部署,于是需要容器。

  • 操作系统内核本身也是一个程序,可以理解成一个进程,它同样是需要单独研究的。

所以,进程是核心,内核、多线程、内存、文件系统、网络、容器和虚拟是配套的能力。我们要想展开操作系统知识的学习,就要先从它的核心——进程入手,通过进程将操作系统的知识串联起来,然后逐一击破。

到这里,请你思考一个问题:进程本身是做什么的?

给你一些提示,进程是程序的执行副本,操作系统用进程来分配资源。这里说的资源,就是 CPU 的计算资源、内存和磁盘的存储资源、I/O 设备的使用权等等。所以我们为了更好地学习操作系统,就需要知道计算机是怎么工作的,因此就需要一门前置课——计算机组成原理,我将在模块一把这部分内容给你讲解清楚。

最后,操作系统通常提供 3 类用户接口:

  • 给程序员用的 API,比如用 C 语言去调用;

  • 给运维人员和管理员用的 Shell;

  • 给大众用户的图形界面。

通常用 API 是为了定制操作系统的能力,如果你从事云计算、运维开发、嵌入式方向,那么一定会用到 API 。因为用户界面制作成本最高,能力也最少,所以大部分运维人员和管理人员都用 Shell。从这个角度分析,认真学习 Shell 有助于你快速掌握操作系统的基本功能,而且又不会太难。

以上就是操作系统课程的知识体系结构,你也可以通过目录,快速了解这个课程的内容设置。

image.png

我是如何梳理知识体系的?

介绍完操作系统的知识体系,我还想借此再和你聊一聊我梳理知识体系的方法。

这是一套我运用多年的学习 方法,它可以帮你节省时间成本,快速精准地查询到你需要的资料。这个学习方法有点像顺藤摸瓜,我自己称之为:“追溯源头、回归本质”。经过这样的思考,可以帮助你把知识学扎实,从而逐渐形成自己的知识体系。

我记得有一个技术大牛曾经说过,程序员最重要的是搜索知识的能力,我非常赞同这个说法,此外,我认为如果你想要长远发展,还应同时具备用结构化的思维去构建知识体系的能力。因为知识成体系后,会形成关联记忆和整体的理解,这种经过深度思考和梳理过的知识才能转化为自己的储备。

下面请你跟我一起进入到场景中,跟着我的思路把你的大脑运转起来。假设,在工作的过程中,我遇到了一块不懂的知识,其中有一个技术名词我不了解它的作用,比如 ReentrantLockLock,那么我该如何解决自己的问题呢?

注意: 你也可以把它替换成任意一个陌生的或者你不理解的技术名词。

首先我会去查阅它的官方文档,然后发现了以下这些线索:

  • 构造函数上有个参数在配置锁的公平性;

  • ReentrantLockLock 是可重入的;

  • 功能类似 synchronized 关键字,但是更灵活;

  • 支持 lock、unlock、tryLock 等方法;

  • 底层是 AbstractQueuedSynchronizer。

接着,根据我获得的知识,追溯 synchronized 关键字,发现 ReentrantLockLock 都说自己的底层是 AbstractQueuedSynchronizer(AQS),我感觉到 AQS 应该是一个重要的东西。 然后我会去查资料验证我的猜测。这时候,我又得到了一个新的信息:发现AQS是用来实现信号量、条件变量以及其他锁的一个编程框架。

假设我还不知道信号量、条件变量和锁是什么,于是我通过搜索资料,发现这些名词通通指向一门科学,也就是操作系统。

接下来,我会去挑选一门讲操作系统的在线课程或者买一本书来查阅,经过查阅发现这些名词出现在进程和多线程这个部分。然后我翻阅了这两个章节的内容,发现了更多我不知道的知识,比如死锁和饥饿、信号量、竞争条件和临界区、互斥的实现,以及最底层的 CPU 指令。

经过以上过程的推导,我开始在脑海中梳理这些知识点,然后动笔画出了一幅基于思考过程的思维导图,将这些知识点串联起来,如下图所示:

image.png 注意,上图梳理出来的知识关系不一定对,但是你一定要敢于去画,这个梳理和探索的过程能够带动你主动思考,锻炼主动解决问题的能力。

输出思维导图后,我将开始学习上面那些超出我现阶段知识储备的内容,然后进行归类和整理。

这时候,我发现公平锁、可重入锁其实都是锁的一种实现,而 Java 中实现锁这个机制用的是 AQS,而 AQS 最基本的问题是要解决资源竞争的问题。

通过学习,我发现资源竞争的问题在操作系统里叫作竞争条件,解决方案是让临界区互斥。让临界区互斥可以用算法的实现,但是为了执行效率,更多的情况是利用 CPU 指令。Java 里用于实现互斥的原子操作 CAS,也是基于 CPU 指令的。

操作系统在解决了互斥问题的基础上,还提供了解决更复杂问题的数据结构,比如说信号量、竞争条件等;而程序语言也提供了数据结构,比如说可重入锁、公平锁。

经过一番探索,我终于弄明白了,原来实际应用场景中对锁有各种各样的需求,因此不仅仅需要信号量等数据结构,甚至还需要一个快速实现这种数据结构的框架,这个框架就是 AQS。我们可以用 AQS 实现 ReentrantLockLock 的功能。

image.png

通过上面的方法,我不仅仅可以把 ReentrantLockLockt 学透,而且顺藤摸瓜找到了所有关联的知识点,比如 AQS 和 CAS。比起理解最初的知识点,更重要的是我通过这种方法形成了自己的一个知识体系;而且,我会发现在这个知识体系中,操作系统是起到支撑作用的骨架

与此同时,我还认识到了计算机语言和操作系统之间的联系非常紧密,操作系统知识是学习计算机语言的根基。于是我开始制定学习计划,投入时间学习操作系统。我更偏爱做一次性的时间投入,以防止日后碎片化学习做多次投入,陷入时间黑洞,而这个嗜好让我受益良多。

01 | 计算机是什么:“如何把程序写好”这个问题是可计算的吗?

我记得自己在面试中遇到过这样一个问题:“可不可以计算一个人程序写得好不好?”

当时我也没有想明白“计算”这个词是什么意思。但事后分析来看,“计算”不就是写程序吗?

其实简单理解这个问题就是“可不可以用机器来判断人的程序写得好不好?”如果从这个角度考虑,我是可以和面试官论述一番的。

后面我查阅了资料,历史上有一个对计算机领域影响颇深的可计算理论,面试官说的“计算”应该就来源于这里。其实继续深挖还能找出很多涉及计算机本源的有趣的知识,比如图灵机、冯诺依曼模型;再比如说 CPU 的构成、程序如何执行、缓存的分级、总线的作用等。

上面提到的这些内容其实都属于操作系统的前置课程,我会利用第一章 4 个课时和大家探讨一下计算机组成原理,然后我们再正式进入操作系统的学习。其实学习就是这样,追溯源头,回到本质,才能挖掘兴趣、激发思考,否则就变成了死记硬背。接下来我们将从计算能源的角度入手,来展开今天的课程学习。

芯片:计算能源

我们知道第一次工业革命出现了蒸汽机,能源是煤炭。第二次工业革命出现了发电机,能源是电。20 世纪四五十年代,又发生了第三次科技革命,革命产物是计算机。而第四次科技革命,就发生在当下,出现了人工智能,能源是数据。

说到这里,你可能会有个疑问:第三次科技革命的能源是什么呢?

你的第一反应可能是电,但是细想又觉得不对。前两次工业革命都有带来能源变革,为什么第三次科技革命就没有了能源变革?其实,第三次科技革命的能源是一种数字能量,本质是计算。

下面我们来看一看这种数字能量是如何产生的。电能供给给芯片,芯片中的一种电子元件晶振(也就是石英晶体)通电后产生震荡,震荡会产生频率稳定的脉冲信号。通常这是一种高频的脉冲信号,每秒可达百万次。然后,我们通过谐振效应发放这个信号,形成方波。再通过电子元件调整这种脉冲的频率,把脉冲信号转换为我们需要的频率,这就形成了驱动芯片工作的时钟信号。这种信号的频率,我们也称作芯片的时钟频率。最后,时钟信号驱动着芯片工作,就像人体的脉搏一样,每一次脉冲到来,都让芯片的状态发生一次变化,用这种方法,最终存储器中的指令被一行行执行。指令被执行,其实就是数据被计算,这就是我说的计算能量。

芯片普及后,不仅给计算机和手机提供支持,它们还被安装到了航天设备、能源设备、医疗设备及通信设备中,甚至小到电灯、微波炉、咖啡机、热水器里面都有了芯片。有了芯片,设备通电后才可以计算,有了计算,这些设备才能够实现更加复杂而精确的功能。

摩尔定律:计算能力的发展

值得一提的是,历史上是先有计算机,后有的芯片。世界上第一个芯片,也被称作集成电路, 1958 年由美国德州仪器公司的工程师杰克·基尔比发明。而世界上第一台通用计算机 ENIAC 则是在 1946 年诞生于美国陆军弹道研究实验室。

看到这里你可能会有疑问,为什么是先发明计算机再发明芯片呢?

其实,这个道理就好比很多程序员先实现产品功能,再考虑封装和复用。ENIAC 中负责计算的模块和后来的芯片原理是一样的,都是利用电路实现逻辑运算。只不过在 20 世纪 40 年代人们还没有将这种能力抽象成一个独立的产品,而且也没有办法解决电路体积的问题,ENIAC的体积看上去就像一所学校那么大。

芯片的计算能力来源于芯片内部的集成电路,集成电路大大减小了电路的体积,所有的元件都是用同一块半导体材料制作而成,也就是把所有的电路都集成到了一个单一的硅片上。为了提高计算性能,集成电路越来越复杂,里面的电子元件也越来越多。从最早拥有 100 个左右晶体管的小型集成电路,发展到 21 世纪初,拥有上亿电子元件的巨大规模集成电路。

芯片的发展,带来了计算能力的飞跃,ENIAC 只能每秒计算 5000 次加法和 400 次乘法,到 1978 年 8086 芯片已经可以每秒计算百万次了。而今天随便一个芯片不但可以轻轻松松每秒计算数亿次,而且不只有一个核心,是多个核心都能达到这一量级的计算能力。

在当时那个年代,Intel 的创始人之一摩尔就观察到了这个现象,并提出了摩尔定律:当价格不变时,集成电路中可容纳的晶体管数目约每隔 18~24 个月就会增加一倍,性能也将提升一倍。这一定律揭示了信息技术发展的速度,但到今天,摩尔定律失效了。因为随着芯片越来越小,在尺寸和散热等方面已经挑战了人类的极限,芯片中无法再放入更多的电子元件了。

但是计算能力又开始以另一种方式发展,比如一个普普通通的 NVIDA 显卡中就拥有了几百个核心,这样就可以进行大量的并发计算;另外,一个分布式的大数据集群,里面就可能有上千个核心。

展望未来,计算能力还有更多的增长点,不仅有可以无限提高计算能力的量子计算机,还有利用光学元件替代晶体元件的光电集成电路。

可计算理论:图灵机

当然,在科学家们尝试发明计算机和芯片之前,他们必须回答一个问题,那就是计算或者程序可以用来做什么?比如:计算可不可以用来做饭?换一个更专业的说法,做饭可不可以被计算?

生活在数字时代的我们,用着导航、玩着游戏,本能地知道很多问题是可以被计算的,但是生活在 20 世纪初的科学家们,需要在没有计算机和芯片的时代就想清楚这些问题,并不是一件容易的事情。

公理化体系和不完备性定理

最早在 19 世纪初,德国著名数学家希尔伯特提出:这个世界可以建立一套完善的公理体系,由少数几个公理出发,推导出所有的定理和推论。这样就可以逐渐通过这种方法将世界上的万事万物都统一到一个体系中。

当然,这只是一个非常美好的设想,如果万事万物都可以用形式化(简单理解就是程序化规范化)的手段统一到一套体系中,也就意味着计算能力将被无限扩展,只要给定足够的时间和空间,计算机就可以完成任何工作。

但在不久后,美籍数学家哥德尔就提出了哥德尔不完备性定理,内容是:即便在完善的公理体系中仍然可以找到不能被证明也不能被证伪的命题。

这让我联想到,一说谎,鼻子就会变长的匹诺曹。如果他说“我说谎了”,那么他的鼻子应该变长还是变短呢?对于人类而言,这个问题可以理解,但是对于计算机来说这个问题是不可以被计算的。

正是因为世界上存在着大量的这种“公说公有理,婆说婆有理”的问题,才让大家认识到计算不能解决所有问题,所以:计算机能力也是有边界的。哥德尔的不完备性定理,让大家看到了世界上还有大量不可计算的问题

图灵机和可计算理论

于是人们意识到了需要一个理论,专门回答这样的问题:哪些问题可以被计算,哪些不可以被计算,这就是可计算性理论,该理论是计算机科学的理论基础之一。

1936 年,被誉为人工智能之父的阿兰·图灵提出了图灵机,它是一种不断执行指令的抽象计算机。之所以说抽象,是因为图灵并没有真的造出这台机器,而是把它当成理论去和大家探讨可计算问题。

图灵发现如果一个问题是可计算的,那么它的解决方案就必须可以被具化成一条条的指令,也就是可以使用图灵机处理。因此,不能使用图灵机处理的问题,都是不可计算的问题。

比如一个马达的控制程序是可计算的,因为控制过程是可以被抽象成一条条指令的(即可以写程序实现)。比如程序可以先读入传感器的数据,然后根据数据计算出下面要进行加速还是减速。

不可计算问题

但当图灵机遇到“素数是不是有无穷多个?”这样的问题时,事情就变得复杂了。虽然,我们可以通过有限的步骤计算出下一个素数。比如可以每次尝试一个更大的数字,然后通过一系列计算过程判断该数字是不是素数,直到找到一个更大的素数。古希腊数学家埃拉托斯特尼就发明了筛选出给定范围内所有素数的方法。

image.png

如上图所示,我们利用埃拉托斯特尼筛法找到的素数越来越多。但是,我们还是不能回答“素数是不是有无穷多个”这样的问题。因为要回答这样的问题,我们会不停地寻找下一个素数。如果素数是无穷的,那么我们的计算就是无穷无尽的,所以这样的问题不可计算。

停机问题

我们也无法实现用一个通用程序去判断另一个程序是否会停止。比如你用运行这段程序来检查一个程序是否会停止时,你会发现不能因为这个程序执行了 1 天,就判定它不会停止,也不能因为这个程序执行了 10 年,从而得出它不会停止的结论。这个问题放到图灵机领域,叫作停机问题,我们无法给出一个判断图灵机是否会停机的通用方法,因此停机问题是一个经典的不可计算问题。

计算能力的边界在哪里?

我们可以把世界上想解决的事情都称作问题,解决问题往往需要消耗芯片的计算能力,这通常称作时间开销,另外解决问题还需要消耗内存,称作空间开销。

问题的分类

世界上有一类问题,无论我们消耗多少时间和空间也无法解决,这类问题就包括“停机问题”,称作不可计算问题,我们无法用计算机精确地解决这类问题。世界上不可计算问题多,还是可计算问题多,也是一个不可计算问题,但直觉告诉我们一定是不可计算问题更多。

另外在可计算的问题中,有困难问题,也有简单问题,我们通常用复杂度来衡量,比如:

  • “求数组第 10 个元素”,计算这种问题,时间开销、空间开销都不会随着问题规模增长,我们记为 O(1);

  • “求数组中的最大值”,计算这种问题,时间开销会随着数组规模线性增大,记作 O(N),N 是问题的规模;

  • 还有像“求一个n*n矩阵的和”,如果n是规模,那么时间开销会随着问题规模的平方增长,我们称作 O(N2);

  • 当然也有更加复杂的数学模型,比如说O(N3)、O(N4)、O(N100)等。

P 问题 vs NP 问题

按照摩尔定律所说,人类的计算能力每 18~24 个月翻一倍,我们的计算能力在呈指数形式上升。因此,在所有可以计算的问题中,像 O(N1000)的问题,虽然现在的计算能力不够,但是相信在遥远的未来,我们会拥有能力解决。这种我们有能力解决的问题,统称为多项式时间( Polynomial time)问题。我们今天能解决的问题,都是多项式时间的问题,下面记为 P 类型的问题。

image.png

另外,还有一类问题复杂度本身也是指数形式的问题,比如 O(2N)的问题。这类型的问题随着规模 N 上升,时间开销的增长速度和人类计算能力增长速度持平甚至更快。因此虽然这类问题可以计算,但是当 N 较大时,因为计算能力不足,最终结果依然无法被解决。

由此可见,不是所有可以计算的问题都可以被解决,问题如果不能在多项式时间内找到答案,我们记为 NP 问题。

有一部分 NP 问题可以被转化为 P 问题,比如斐波那契数列求第 N 项,可以用缓存、动态规划等方式转化为 O(N) 的问题。但还有更多的 NP 问题,比如一个集合,找出和为零的子集,就没能找到一个合适的转换方法。其实说这么多,就是想告诉大家:如今还有很多问题无法解决,它的数量远远大于我们可以解决的问题,科学家、工程师们也只能望洋兴叹了。

人工智能

此外,包括停机问题、包括 NP 问题在内的很多问题,虽然不能解决,但可以努力让计算机的解决方案超过人类的水平,这就是人工智能。

比如下围棋,围棋盘是 19*19 的,共有 361!种情况,如果遍历 361!种情况,并进行打分,共有 10 的 170 次方种可能,因此,我们的计算能力是远远不足的。但是如果使用人工智能方法对可能出现的部分情况进行概率判断,在不追求绝对精确的情况下,人工智能就可以超过人类选手。

AlphaGo 战胜李世石就是利用了基于概率的不完全解法,这种解法已经可以超过部分人类职业选手了,也就是说计算机的解法已经超过了人类。当然,人类的强项在于理解和分析,人有两种思维,归纳和假设,这两种思维都是计算机无法计算的。机器用概率理解围棋,局部来说机器下得更好,但是人可以制造机器,因此,人的感悟更有意义,谈不上孰优孰劣。

针对这种解决问题的方法,20 世纪中人工智能之父图灵,提出图灵测试,就是在一次人机对话中,随机抽样一部分的实验者和机器对话,如果这部分实验者有较大的百分比判断对面是人而不是机器,那这台机器就通过了图灵测试。在围棋领域,可以说,AI 通过了图灵测试。但围棋的 AI 不能下象棋,这也是 AI 的一个劣势。所以广义的 AI 还没有出现,现在出现的是在某个专业领域的 AI。

总结

下面我们进行总结。本课时是一个理解操作系统知识必不可少的计算机原理引导课。

  • 我们学习了芯片,芯片将电能转化为计算能量,计算能量推动程序执行;

  • 着提到了摩尔定律,了解到我们的计算能力仍在飞速发展;

  • 还花了篇幅讲了图灵机,从而进一步认识了人工智能之父阿兰·图灵,图灵机具体的设计和构造,这将在02 课时程序的执行部分进一步讨论。

  • 最后普及了图灵测试和人工智能的基本概念,带你了解了计算机的能力边界。

下面我们回到最初的问题:“可不可以计算一个人程序写得好不好?”

这个问题可以这样来思考,如果把问题降级,变成:“可不可以计算一个人写的程序会不会停机?”

这个问题就如同停机问题,无法计算,因此这是一个不可计算的问题。但是我们通过设立规则,比如检查缩进、检查函数的复用情况、检查类的命名情况,给写程序的人更好的建议。另外,我们也可以通过 AI 技术,让机器在“程序写得好不好”这个问题的判定能力上,达到人类的水平,通过图灵测试。

综上,从绝对的对错角度去看,这是一个不可计算问题,因为它没有办法被完全解决;但是从图灵测试层面来看,虽然目前无法解决这个问题,但是我们有理由相信,在未来,计算机对这个问题的解决方案,是可以超过人类的。

思考题

最后再给你留一道查资料的思考题:可不可以构造一段程序证明停机问题无解?如果可以,请用自己熟悉的语言写出这段程序。

02 | 程序的执行:相比 32 位,64 位的优势是什么?(上)

本节课给你讲学习操作系统之前的一个前置知识:程序是如何执行的?

我们先来看一道常规的面试题:相比 32 位,64 位的优势是什么?

面试官考察这种类型的问题,主要是想看求职者是否有扎实的计算机基础,同时想知道求职者在工作中是否充满好奇,会主动学习、寻根问底,毕竟 32、64 位是经常出现在程序员视野的词汇,常见的东西都弄明白了,那说明这个人学习能力强。

其实 ,面试官在这里给你挖了一个陷阱,因为他没有说清楚 32、64 位指的是操作系统、是软件、还是 CPU?

  • 如果是软件,那么我们的数据库有 32 位和 64 位版本;

  • 如果是操作系统,那么在阿里云上选择 Centos 和 Debian 版本的时候,也会有 32/64 版本;

  • 如果是 CPU,那么有 32 位 CPU,也有 64 位 CPU。

接下来请你带着问题开始今天的课程学习,本课时的重点是带你学懂程序执行的原理。

图灵机的构造

想要学懂程序执行的原理,就要从图灵机说起了。它在计算机科学方面有两个巨大的贡献:

第一,它清楚地定义了计算机能力的边界,也就是可计算理论;

第二,它定义了计算机由哪些部分组成,程序又是如何执行的。

image.png

我们先来看一看图灵机的内部构造:

  1. 图灵机拥有一条无限长的纸带,纸带上是一个格子挨着一个格子,格子中可以写字符,你可以把纸带看作内存,而这些字符可以看作是内存中的数据或者程序。
  2. 图灵机有一个读写头,读写头可以读取任意格子上的字符,也可以改写任意格子的字符。
  3. 读写头上面的盒子里是一些精密的零件,包括图灵机的存储、控制单元和运算单元。

图灵机如何执行程序

下面我们来举一个例子,让大家弄清楚图灵机是如何工作的,比如我们要计算 11 + 15 的值,具体的运算步骤如下:

  • 首先,我们将“11、15、+” 分别写入纸带上的 3 个格子(现在纸带上的字符串是11、15、 +),然后将读写头先停在 11 对应的格子上。

image.png

  • 接下来,图灵机通过读写头读入 11 到它的存储设备中(这个存储设备也叫作图灵机的状态)。图灵机没有说读写头为什么可以识别纸带上的字符,而是假定读写头可以做到这点。

image.png

  • 然后读写头向右移动一个格,用同样的方法将 15 读入图灵机的状态中。现在图灵机的状态中有两个连续的数字,11 和 15。

image.png

  • 接下来重复上面的过程,会读到一个+号。下面我详细说一下这个运算流程:
  1. 读写头读到一个 + 号 ;
  2. 然后将 + 号传输给控制单元 ;
  3. 控制单元发现是一个 + 号,所以没有存入状态中。因为 + 号是一个我们预设的控制符(指令),它的作用是加和目前状态。因此,控制单元识别出是控制符,并通知运算单元工作;
  4. 运算单元从状态中读入 11、15 并进行计算,将结果 26 存储到状态;
  5. 运算单元将结果回传给控制单元;
  6. 控制单元将结果传输给读写头。

image.png

  • 读写头向右移动,将结果 26 写入纸带。

image.png 这样,我们就通过图灵机计算出了 11+15 的值。不知道你有没有发现,图灵机构造的这一台机器,主要功能就是读写纸带然后计算;纸带中有数据、也有控制字符(也就是指令),这个设计和我们今天的计算机是一样的。

图灵通过数学证明了,一个问题如果可以拆解成图灵机的可执行步骤,那问题就是可计算的。另一方面,图灵机定义了计算机的组成以及工作原理,但是没有给出具体的实现。

冯诺依曼模型

image.png 具体的实现是 1945 年冯诺依曼和其他几位科学家在著名的 101 页报告中提出的。报告遵循了图灵机的设计,并提出用电子元件构造计算机,约定了用二进制进行计算和存储,并且将计算机结构分成以下 5 个部分:

  1. 输入设备;
  2. 输出设备;
  3. 内存;
  4. 中央处理器;
  5. 总线。

这个模型也被称为冯诺依曼模型,下面我们具体来看看这 5 部分的作用。

内存

在冯诺依曼模型中,程序和数据被存储在一个被称作内存的线性排列存储区域。存储的数据单位是一个二进制位,英文是 bit。最小的存储单位叫作字节,也就是 8 位,英文是 byte,每一个字节都对应一个内存地址。内存地址由 0 开始编号,比如第 1 个地址是 0,第 2 个地址是 1, 然后自增排列,最后一个地址是内存中的字节数减 1。

我们通常说的内存都是随机存取器,也就是读取任何一个地址数据的速度是一样的,写入任何一个地址数据的速度也是一样的。

CPU

冯诺依曼模型中 CPU 负责控制和计算。为了方便计算较大的数值,CPU 每次可以计算多个字节的数据。

  • 如果 CPU 每次可以计算 4 个 byte,那么我们称作 32 位 CPU;
  • 如果 CPU 每次可以计算 8 个 byte,那么我们称作 64 位 CPU。

这里的 32 和 64,称作 CPU 的位宽。

为什么 CPU 要这样设计呢? 因为一个 byte 最大的表示范围就是 0~255。比如要计算 20000*50,就超出了byte 最大的表示范围了。因此,CPU 需要支持多个 byte 一起计算。当然,CPU 位数越大,可以计算的数值就越大。但是在现实生活中不一定需要计算这么大的数值。比如说 32 位 CPU 能计算的最大整数是 4294967295,这已经非常大了。

控制单元和逻辑运算单元

CPU 中有一个控制单元专门负责控制 CPU 工作;还有逻辑运算单元专门负责计算。具体的工作原理我们在指令部分给大家分析。

寄存器

CPU 要进行计算,比如最简单的加和两个数字时,因为 CPU 离内存太远,所以需要一种离自己近的存储来存储将要被计算的数字。这种存储就是寄存器。寄存器就在 CPU 里,控制单元和逻辑运算单元非常近,因此速度很快。

  • 寄存器中有一部分是可供用户编程用的,比如用来存加和指令的两个参数,是通用寄存器。
  • 还有一部分寄存器有特殊的用途,叫作特殊寄存器。比如程序指针,就是一个特殊寄存器。它存储了 CPU 要执行的下一条指令所在的内存地址。注意,程序指针不是存储了下一条要执行的指令,此时指令还在内存中,程序指针只是存储了下一条指令的地址。
  • 下一条要执行的指令,会从内存读入到另一个特殊的寄存器中,这个寄存器叫作指令寄存器。指令被执行完成之前,指令都存储在这里。

总线

CPU 和内存以及其他设备之间,也需要通信,因此我们用一种特殊的设备进行控制,就是总线。总线分成 3 种:

  • 一种是地址总线,专门用来指定 CPU 将要操作的内存地址。
  • 还有一种是数据总线,用来读写内存中的数据。

当 CPU 需要读写内存的时候,先要通过地址总线来指定内存地址,再通过数据总线来传输数据。

  • 最后一种总线叫作控制总线,用来发送和接收关键信号,比如后面我们会学到的中断信号,还有设备复位、就绪等信号,都是通过控制总线传输。同样的,CPU 需要对这些信号进行响应,这也需要控制总线。

输入、输出设备

输入设备向计算机输入数据,计算机经过计算,将结果通过输出设备向外界传达。如果输入设备、输出设备想要和 CPU 进行交互,比如说用户按键需要 CPU 响应,这时候就需要用到控制总线。

到这里,相信你已经对冯诺依曼模型的构造有了一定的了解。这里我再强调几个问题:

1. 线路位宽问题

第一个问题是,你可能会好奇数据如何通过线路传递。其实是通过操作电压,低电压是 0,高电压是 1。

如果只有一条线路,每次只能传递 1 个信号,因为你必须在 0,1 中选一个。比如你构造高高低低这样的信号,其实就是 1100,相当于你传了一个数字 10 过去。大家注意,这种传递是相当慢的,因为你需要传递 4 次。

这种一个 bit 一个 bit 发送的方式,我们叫作串行。如果希望每次多传一些数据,就需要增加线路,也就是需要并行。

如果只有 1 条地址总线,那每次只能表示 0-1 两种情况,所以只能操作 2 个内存地址;如果有 10 条地址总线,一次就可以表示 2^10 种情况,也就是可以操作 1024 个内存地址;如果你希望操作 4G 的内存,那么就需要 32 条线,因为 2^32 是 4G。

到这里,你可能会问,那我串行发送行不行?当然也不是不行,只是速度会很慢,因为每多增加一条线路速度就会翻倍。

2. 64 位和 32 位的计算

第二个问题是,CPU 的位宽会对计算造成什么影响?

我们来看一个具体场景:要用 32 位宽的 CPU,加和两个 64 位的数字。

32 位宽的 CPU 控制 40 位宽的地址总线、数据总线工作会非常麻烦,需要双方制定协议。 因此通常 32 位宽 CPU 最多操作 32 位宽的地址总线和数据总线。

因此必须把两个 64 位数字拆成 2 个 32 位数字来计算,这样就需要一个算法,比如用像小时候做加法竖式一样,先加和两个低位的 32 位数字,算出进位,然后加和两个高位的 32 位数字,最后再加上进位。

而 64 位的 CPU 就可以一次读入 64 位的数字,同时 64 位的 CPU 内部的逻辑计算单元,也支持 64 位的数字进行计算。但是你千万不要仅仅因为位宽的区别,就认为 64 位 CPU 性能比 32 位高很多。

要知道大部分应用不需要计算超过 32 位的数字,比如你做一个电商网站,用户的金额通常是 10 万以下的,而 32 位有符号整数,最大可以到 20 亿。所以这样的计算在 32 位还是 64 位中没有什么区别。

还有一点要注意,32 位宽的 CPU 没办法控制超过 32 位的地址总线、数据总线工作。比如说你有一条 40 位的地址总线(其实就是 40 条线),32 位的 CPU 没有办法一次给 40 个信号,因为它最多只有 32 位的寄存器。因此 32 位宽的 CPU 最多操作 2^32 个内存地址,也就是 4G 内存地址。

总结

关于计算机组成和指令部分,我们就先学到这里。这节课我们通过图灵机和冯诺依曼模型学习了计算机的组成、CPU 的工作原理等。此外,我们还顺带讨论了 32 位和 64 位的区别,现在,你可以回答 64 位和 32 位比较有哪些优势了吗

请你先自己思考这个问题的答案,再来 03 课时查看详细的分析过程。

你可以把你的思考、思路或者课后总结写在留言区,这样可以帮助你产生更多的思考,也是构建知识体系的一部分。经过长期的积累,相信你会得到意想不到的收获。如果你觉得今天的内容对你有所启发,欢迎分享给身边的朋友。期待看到你的思考!

03 | 程序的执行:相比 32 位,64 位的优势是什么?(下)

在 02 课时中我们学习了计算机的组成原理,还分析了一些你在工作中可能会遇到的问题。本课时,我们继续深入学习程序执行部分,进一步讨论程序在冯诺依曼模型上如何执行。

程序的执行过程

当 CPU 执行程序的时候:

1.首先,CPU 读取 PC 指针指向的指令,将它导入指令寄存器。具体来说,完成读取指令这件事情有 3 个步骤:

步骤 1:CPU 的控制单元操作地址总线指定需要访问的内存地址(简单理解,就是把 PC 指针中的值拷贝到地址总线中)。

步骤 2:CPU 通知内存设备准备数据(内存设备准备好了,就通过数据总线将数据传送给 CPU)。

步骤 3:CPU 收到内存传来的数据后,将这个数据存入指令寄 存器。

完成以上 3 步,CPU 成功读取了 PC 指针指向指令,存入了指令寄存器。

2.然后,CPU 分析指令寄存器中的指令,确定指令的类型和参数。

3.如果是计算类型的指令,那么就交给逻辑运算单元计算;如果是存储类型的指令,那么由控制单元执行。

4.PC 指针自增,并准备获取下一条指令。

比如在 32 位的机器上,指令是 32 位 4 个字节,需要 4 个内存地址存储,因此 PC 指针会自增 4。

image.png 了解了程序的执行过程后,我还有一些问题想和大家一起讨论:

  1. 内存虽然是一个随机存取器,但是我们通常不会把指令和数据存在一起,这是为了安全起见。具体的原因我会在模块四进程部分展开讲解,欢迎大家在本课时的留言区讨论起来,我会结合你们留言的内容做后续的课程设计。
  2. 程序指针也是一个寄存器,64 位的 CPU 会提供 64 位的寄存器,这样就可以使用更多内存地址。特别要说明的是,64 位的寄存器可以寻址的范围非常大,但是也会受到地址总线条数的限制。比如和 64 位 CPU 配套工作的地址总线只有 40 条,那么可以寻址的范围就只有 1T,也就是 240。
  3. 从 PC 指针读取指令、到执行、再到下一条指令,构成了一个循环,这个不断循环的过程叫作CPU 的指令周期,下面我们会详细讲解这个概念。

详解 a = 11 + 15 的执行过程

上面我们了解了基本的程序执行过程,接下来我们来看看如果用冯诺依曼模型执行a=11+15是一个怎样的过程。

我们再 Review 下这个问题:程序员写的程序a=11+15是字符串,CPU 不能执行字符串,只能执行指令。所以这里需要用到一种特殊的程序——编译器。编译器的核心能力是翻译,它把一种程序翻译成另一种程序语言。

这里,我们需要编译器将程序员写的程序翻译成 CPU 认识的指令(指令我们认为是一种低级语言,我们平时书写的是高级语言)。你可以先跟我完整地学完操作系统,再去深入了解编译原理的内容。

下面我们来详细阐述 a=11+15 的执行过程:

1.编译器通过分析,发现 11 和 15 是数据,因此编译好的程序启动时,会在内存中开辟出一个专门的区域存这样的常数,这个专门用来存储常数的区域,就是数据段,如下图所示:

  • 11 被存储到了地址 0x100;
  • 15 被存储到了地址 0x104;

image.png

2.编译器将a=11+15转换成了 4 条指令,程序启动后,这些指令被导入了一个专门用来存储指令的区域,也就是正文段。如上图所示,这 4 条指令被存储到了 0x200-0x20c 的区域中:

0x200 位置的 load 指令将地址 0x100 中的数据 11 导入寄存器 R0;

0x204 位置的 load 指令将地址 0x104 中的数据 15 导入寄存器 R1;

0x208 位置的 add 指令将寄存器 R0 和 R1 中的值相加,存入寄存器 R2;

0x20c 位置的 store 指令将寄存器 R2 中的值存回数据区域中的 0x1108 位置。

3.具体执行的时候,PC 指针先指向 0x200 位置,然后依次执行这 4 条指令。

这里还有几个问题要说明一下:

  1. 变量 a 实际上是内存中的一个地址,a 是给程序员的助记符。
  2. 为什么 0x200 中代表加载数据到寄存器的指令是 0x8c000100,我们会在下面详细讨论。
  3. 不知道细心的同学是否发现,在上面的例子中,我们每次操作 4 个地址,也就是 32 位,这是因为我们在用 32 位宽的 CPU 举例。在 32 位宽的 CPU 中,指令也是 32 位的。但是数据可以小于 32 位,比如可以加和两个 8 位的字节。
  4. 关于数据段和正文段的内容,会在模块四进程和线程部分继续讲解。

指令

接下来我会带你具体分析指令的执行过程。

在上面的例子中,load 指令将内存中的数据导入寄存器,我们写成了 16 进制:0x8c000100,拆分成二进制就是:

这里大家还是看下图,需要看一下才能明白。

image.png

  • 最左边的 6 位,叫作操作码,英文是 OpCode,100011 代表 load 指令;
  • 中间的 4 位 0000是寄存器的编号,这里代表寄存器 R0;
  • 后面的 22 位代表要读取的地址,也就是 0x100。

所以我们是把操作码、寄存器的编号、要读取的地址合并到了一个 32 位的指令中。

我们再来看一条求加法运算的 add 指令,16 进制表示是 0x08048000,换算成二进制就是:

image.png

  • 最左边的 6 位是指令编码,代表指令 add;
  • 紧接着的 4 位 0000 代表寄存器 R0;
  • 然后再接着的 4 位 0001 代表寄存器 R1;
  • 再接着的 4 位 0010 代表寄存器 R2;
  • 最后剩下的 14 位没有被使用。

构造指令的过程,叫作指令的编码,通常由编译器完成;解析指令的过程,叫作指令的解码,由 CPU 完成。由此可见 CPU 内部有一个循环:

  1. 首先 CPU 通过 PC 指针读取对应内存地址的指令,我们将这个步骤叫作 Fetch,就是获取的意思。
  2. CPU 对指令进行解码,我们将这个部分叫作 Decode。
  3. CPU 执行指令,我们将这个部分叫作 Execution。
  4. CPU 将结果存回寄存器或者将寄存器存入内存,我们将这个步骤叫作 Store。

image.png 上面 4 个步骤,我们叫作 CPU 的指令周期。CPU 的工作就是一个周期接着一个周期,周而复始。

指令的类型

通过上面的例子,你会发现不同类型(不同 OpCode)的指令、参数个数、每个参数的位宽,都不一样。而参数可以是以下这三种类型:

  1. 寄存器;
  2. 内存地址;
  3. 数值(一般是整数和浮点)。

当然,无论是寄存器、内存地址还是数值,它们都是数字。

指令从功能角度来划分,大概有以下 5 类:

  1. I/O 类型的指令,比如处理和内存间数据交换的指令 store/load 等;再比如将一个内存地址的数据转移到另一个内存地址的 mov 指令。
  2. 计算类型的指令,最多只能处理两个寄存器,比如加减乘除、位运算、比较大小等。
  3. 跳转类型的指令,用处就是修改 PC 指针。比如编程中大家经常会遇到需要条件判断+跳转的逻辑,比如 if-else,swtich-case、函数调用等。
  4. 信号类型的指令,比如发送中断的指令 trap。
  5. 闲置 CPU 的指令 nop,一般 CPU 都有这样一条指令,执行后 CPU 会空转一个周期。

指令还有一个分法,就是寻址模式,比如同样是求和指令,可能会有 2 个版本:

  1. 将两个寄存器的值相加的 add 指令。

  2. 将一个寄存器和一个整数相加的 addi 指令。 另外,同样是加载内存中的数据到寄存器的 load 指令也有不同的寻址模式:

  3. 比如直接加载一个内存地址中的数据到寄存器的指令la,叫作直接寻址。

  4. 直接将一个数值导入寄存器的指令li,叫作寄存器寻址。

  5. 将一个寄存器中的数值作为地址,然后再去加载这个地址中数据的指令lw,叫作间接寻址。

因此寻址模式是从指令如何获取数据的角度,对指令的一种分类,目的是给编写指令的人更多选择

了解了指令的类型后,我再强调几个细节问题:

  1. 关于寻址模式和所有的指令,只要你不是嵌入式开发人员,就不需要记忆,理解即可。
  2. 不同 CPU 的指令和寄存器名称都不一样,因此这些名称也不需要你记忆。
  3. 有几个寄存器在所有 CPU 里名字都一样,比如 PC 指针、指令寄存器等。

指令的执行速度

之前我们提到过 CPU 是用石英晶体产生的脉冲转化为时钟信号驱动的,每一次时钟信号高低电平的转换就是一个周期,我们称为时钟周期。CPU 的主频,说的就是时钟信号的频率。比如一个 1GHz 的 CPU,说的是时钟信号的频率是 1G。

到这里你可能会有疑问:是不是每个时钟周期都可以执行一条指令?其实,不是的,多数指令不能在一个时钟周期完成,通常需要 2 个、4 个、6 个时钟周期。

总结

接下来我们来做一个总结。这节课我们深入讨论了指令和指令的分类。接下来,我们来看一看在 02 课时中留下的问题:64 位和 32 位比较有哪些优势

还是老规矩,请你先自己思考这个问题的答案,写在留言区,然后再来看我接下来的分析。

【解析】 其实,这个问题需要分类讨论。

  1. 如果说的是 64 位宽 CPU,那么有 2 个优势。

优势 1:64 位 CPU 可以执行更大数字的运算,这个优势在普通应用上不明显,但是对于数值计算较多的应用就非常明显。

优势 2:64 位 CPU 可以寻址更大的内存空间

  1. 如果 32 位/64 位说的是程序,那么说的是指令是 64 位还是 32 位的。32 位指令在 64 位机器上执行,困难不大,可以兼容。 如果是 64 位指令,在 32 位机器上执行就困难了。因为 32 位指令在 64 位机器执行的时候,需要的是一套兼容机制;但是 64 位指令在 32 位机器上执行,32 位的寄存器都存不下指令的参数。

  2. 操作系统也是一种程序,如果是 64 位操作系统,也就是操作系统中程序的指令都是 64 位指令,因此不能安装在 32 位机器上。

思考题

最后再给你出一道思考题:CPU 中有没有求对数的指令?如果没有那么程序如何去计算?

04 | 构造复杂的程序:将一个递归函数转成非递归函数的通用方法

05 | 存储器分级:L1 Cache 比内存和 SSD 快多少倍?

加餐 | 练习题详解(一)