你好,我是七牛云许式伟。

在上一篇中,我们把“构建一个应用程序”类比成“构建一座大厦”,并从宏观全局的视角剖析了应用程序这座大厦的构成。今天,我们将更加近距离地去解剖这座大厦的地基:冯·诺依曼体系结构。

解剖架构的关键点是什么?

在解剖之前,我想和你先谈谈“解剖学”:我们应该如何去分析架构设计中涉及的每一个零部件。换一句话说,当我们设计或分析一个零部件时,我们会关心哪些问题。

**第一个问题,是需求。**这个零部件的作用是什么?它能被用来做哪些事情?(某种意义上来说更重要的是)它不会被用来做哪些事情?

你可能会说,呀,这个问题很简单,既然我设计了这个零部件,自然知道它是用来干嘛的。但实质上这里真正艰难的是“为什么”:为何这个零件被设计成用来干这些事情的,而不是多干一点事情,或者为什么不是少干某些事情?

**第二个问题,是规格。**这个零部件接口是什么样的?它如何与其他零件连接在一起的?

规格是零部件的连接需求的抽象。符合规格的零部件可以有非常多种可能的实现方案,但是,一旦规格中某个条件不能满足了,它就无法正常完成与其他零件的连接,以达到预期的需求目标。

规格的约束条件会非常多样化,可能是外观(比如形状和颜色),可能是交互方式(比如用键盘、鼠标,或者语音和触摸屏),也可能是质量(比如硬度、耐热性等等)。

那么,冯·诺依曼体系结构的需求和规格又是什么样的呢?

为“解决一切的问题”而生

冯·诺依曼体系结构不但是应用程序这座大厦的地基,同时也是整个信息科技的地基。

当我们去审视整个信息科技时,仅把它形容为一座大厦显得如此不贴切,甚至你也不能用“一个城市”去形容它,事实上,它更像是一个无中生有的全新世界:在其中,有个体、有族群、有生态,还有喜怒哀乐。

冯·诺依曼体系结构的迷人之处在于,从需求来说,它想解决一切问题。解决一切可以用“计算”来解决的问题。

“计算”的边界在哪里?今天我们还没有人能够真正说得清。计算能不能解决“智能”的问题?通过计算能力,计算机是否终有一天可以获得和人类一样的智能?

今天人工智能热潮的兴起,证明对于这个问题我们很乐观:计算终将解决智能的问题。尽管我们不能确定什么时候能够达到,但是让人欣慰的是,我们一直在进步 —— 如果人类智能无法完成进一步的进化,那么我们就一直一直在前进,最终无限逼近甚至超越人类智能。

甚至有科幻小说家设想(例如在 Google 的“AlphaGo”大热后,霍炬和西乔创作的漫画“BetaCat”),计算机演进出超过人类的智能是生物进化的一个自然演进路径,它将取代人类成为新的食物链顶端,并最终基于其悠久的生命力,去完成人类有限生命无法实现的星际航行之路。

冯·诺依曼体系的规格

为了实现“解决一切可以用‘计算’来解决的问题”这个目标,冯·诺依曼引入了三类基础零部件:

  • 中央处理器;
  • 存储;
  • 输入输出设备。

首先我们来看看存储。它负责存放计算涉及的相关数据,作为计算的输入参数和输出结果。

我们日常见到的存储设备非常的多样化。比如:中央处理器自己内置的寄存器、内存、传统机械硬盘、USB 固态硬盘、光盘等等。

从中央处理器的角度,存储可简单分为两类:一类是内置支持的存储,通过常规的处理器指令可直接访问,比如寄存器、内存、计算机主板的 ROM。一类是外置存储,它们属于输入输出设备。中央处理器本身并不能直接读写其中的数据。

冯·诺依曼体系中涉及的“存储”,指的是中央处理器内置支持的存储。

我们再来看看输入输出设备。它是计算机开放性的体现,大大拓展了计算机的能力。每个设备通过一个端口与中央处理器连接。通过这个端口地址,中央处理器可以和设备进行数据交换。数据交换涉及的数据格式由设备定义,中央处理器并不理解。

但这并不影响设备的接入。设备数据交换的发起方(设备使用方)通常理解并可以解释所接收的数据含义。为了方便使用,设备厂商或操作系统厂商通常会提供设备相关的驱动程序,把设备数据交换的细节隐藏起来,设备的使用方只需要调用相关的接口函数就可以操作设备。

最后我们来看看中央处理器。它负责程序(指令序列)的执行。指令序列在哪里?也存放在存储里面。计算机加电启动后,中央处理器从一个固定的存储地址开始执行。

中央处理器支持的指令大体如下(我们在第一篇文章中也曾提到过):

  • 计算类,也就是支持我们大家都熟知的各类数学运算,如加减乘除、sin/cos 等等;
  • I/O 类,从存储读写数据,从输入输出设备读数据、写数据;
  • 指令跳转类,在满足特定条件下跳转到新的当前程序执行位置、调用自定义的函数。

和“解决一切可以用‘计算’来解决的问题”这个伟大的目标相比,冯·诺依曼体系的三类零部件的规格设计显得如此精简。

为什么这么简洁的规格设计,居然可以解决这么复杂的需求?

需求是怎么被满足的?

我们来设想一下:假如今天让我们从零开始设计一个叫电脑的东西,我们的目标是“解决一切可以用‘计算’来解决的问题”。

对于这么含糊的需求,如果你是“电脑”这个产品的主架构师,你会如何应对?

让我们来分析一下。

一方面,需求的变化点在于,要解决的问题是五花八门包罗万象的。如何以某种稳定但可扩展的架构来支持这样的变化?而另一方面,需求的稳定之处在于,电脑的核心能力是固定的,怎么表达电脑的核心能力?

电脑的核心能力是“计算”。什么是计算?计算就是对一个数据(输入)进行变换,变为另一个数据(输出)。在数学中我们把它叫“函数”。如下:

y = F(x)

这里 x、y 是数据。它们可能只是一个简单的数值,也可能是文本、图片、视频,各种我们对现实问题进行参数化建模后的测量值,当然也可能是多个输入数据。但无论它的逻辑含义为何,物理上都可以以一段连续的字节内容来表达。用 Go 的语法表达就是:

1
2

func F(x []byte) (y []byte)

那么 x、y 物理上在哪里?思路推理到这里,“存储”这个概念自然就产生了:存储,就是存放计算所要操作的数据的所在。

下面的问题是:一个具体的计算(也就是 F 函数)怎么表达?

这里的难点在于,F 对于电脑的架构师来说是未知的。那么,怎么设计一种系统架构让用户可以表达任意复杂的计算(函数)?

逻辑上来看,无论多复杂的自定义函数,都可以通过下面这些元素的组合来定义:

  • 内置函数,比如整数或小数运算(加减乘除、sin/cos 等);
  • 循环和条件分支;
  • 子函数(也是自定义函数)。

这样一来,对于任意的一个具体的计算(自定义函数)来说,都可以用一组指令序列来表达。

那么函数 F 物理上在哪里?以指令序列形式存放在存储里面。所以,存储不只存放计算所要操作的数据,也存放“计算”本身。

只是,存储里面存放的“计算”只是数据,需要有人理解并执行这些数据背后的计算行为,才变成真正意义的“计算”。这个执行者,就是中央处理器(CPU)。它支持很多计算指令,包括执行内置函数、循环和条件分支、执行子函数等。

所以,有了中央处理器 + 存储,就可以支持任意复杂的“计算”了。

只是如果电脑只有“中央处理器 + 存储”,那它就如同一个人只有头脑而没有四肢五官,尽管很可能很聪明,但是这种聪明无法展现出来,因为它没法和现实世界发生交互。

交互,抽象来看就是输入和输出。对人来说,输入靠的是五官:眼睛看、耳朵听、鼻子闻、舌头尝,以及肌肤接触产生的触觉。输出靠语言(说话)和各种动作,如微笑、眨眼、皱眉、手势等等。

对于电脑来说,输入输出的需求就更多了,不只是四肢五官,而可能是千肢万官。

从输入需求来说,可能采集静态图像、声音、视频;也可能采集结构化数据,如 GPS 位置、脉搏、心电图、温度、湿度等;还可能是用户控制指令如键盘按键、鼠标、触摸屏动作等。

从输出需求来说,可能是向屏幕输出信息;也可能是播放声音;还可能是执行某项动作,如交通灯开关、汽车马达转动、打印机打印等。

但不管是什么样交互用途的器官(设备),我们要做的只是定义好统一的数据交换协议。这个数据交换机制,和网络上两台电脑通过互联网,需要通过某种数据交换协议进行通讯,需求上没有实质性的差别。

也就是说,除了纯正的“计算”能力外,中央处理器还要有“数据交换”能力(或者叫 IO 能力)。最终,电脑可以被看做由“中央处理器 + 存储 + 一系列的输入输出设备” 构成。如下图:

尽管输入输出设备引入的最初灵感可能是来自于“交互”,但是当我们去审视输入输出设备到底是什么的时候,我们很自然发现,它能够做的不单单是交互。

比如常见的外置存储如机械硬盘、光盘等,它们也是输入输出设备,但并不是用于交互,而是显著提升了电脑处理的数据体量。

输入输出设备从根本上解决的问题是什么?

是电脑无限可能的扩展能力。

最重要的一点,输入输出设备和电脑是完全异构的。输入输出设备对电脑来说就只是实现了某项能力的黑盒子。

这个黑盒子内部如何?没有规定。它可以只是一个原始的数字化的元器件,也可以是另一台冯·诺依曼架构的电脑,还可以是完全不同架构的电脑,比如 GPU 电脑、量子计算机。

你可以发现,引入了输入输出设备的电脑,不再只能做狭义上的“计算”(也就是数学意义上的计算),如果我们把交互能力也看做一种计算能力的话,电脑理论上能够解决的“计算”问题变得无所不包。

架构思维上我们学习到什么?

架构的第一步是需求分析。从需求分析角度来说,关键要抓住需求的稳定点和变化点。需求的稳定点,往往是系统的核心价值点;而需求的变化点,则往往需要相应去做开放性设计。

对于“电脑”这个产品而言,需求的稳定点是电脑的“计算”能力。需求的变化点,一是用户“计算”需求的多样性,二是用户交互方式的多样性。

电脑的“计算”能力,最终体现为中央处理器的指令集,这是需求相对稳定的部分。

用户“计算”需求的多样性,最终是通过在存储中的指令序列实现。计算机加电启动后,中央处理器并不是按自己固有的“计算”过程进行,而是从一个固定的存储地址加载指令序列执行。

通常,这个固定的存储地址指向计算机主板的 ROM 上的一段启动程序(BIOS)。这段启动程序通常包含以下这些内容。

  • 存储设备的驱动程序,用以识别常规的外置存储设备,比如硬盘、光驱、U 盘。
  • 基础外部设备的驱动程序,比如键盘、鼠标、显示器(显卡)。
  • 设备和启动配置的基础管理能力。
  • 在外置存储上执行程序的能力(中央处理器只支持在内存上执行程序,当然它也为在外置存储执行程序提供了一些支持,比如内存页缺失的中断处理)。
  • 将执行权转移到外置存储(第一次安装操作系统的时候可能是光驱甚至是网络存储,平常通常是硬盘)上的操作系统启动程序。这样,操作系统就开始干活了。

这样一来,“计算”需求的多样性只需要通过调整计算机主板上的 BIOS 程序,乃至外置存储中的操作系统启动程序就可以实现,而不必去修改中央处理器本身。

用户交互方式的多样性,则通过定义外部设备与中央处理器的数据交换协议实现。

当我们把所有的变化点从电脑的最核心部件中央处理器剥离后,中央处理器的需求变得极其稳定,可独立作为产品进行其核心价值的演进。

结语

总结一下,今天,我们近距离地去解剖了整个信息世界地基:冯·诺依曼体系结构。

冯·诺依曼体系结构的不凡之处在于,它想“解决一切可以用‘计算’来解决的问题”。

为了实现这个目标,冯·诺依曼引入了三类基础零部件:中央处理器、存储、输入输出设备。所有计算机都可以看做由“中央处理器 + 存储 + 一系列的输入输出设备”构成。

为了方便理解,我在尝试用 Go 语言模拟来实现冯·诺依曼架构体系的电脑:

如果你对此感兴趣,欢迎 fork 并对其进行修改迭代。

如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。