计算机内部探秘:程序执行_计算机内部探秘

程序执行
上面我们知道了存储和加法实现,但是这个还不是程序,那么一个程序是怎么在计算机内部执行的呢?
有了前面的讲解,你能猜到,还不是一堆电路在飞速干活,这个过程好机械。这么想就对了,计算机就是一个又笨又快的机器。通过简单的门电路基本的功能(加法移位逻辑运算)组合成威力无比的现代计算机。
你可能厌倦了在这么基本的物理层面学习计算机编程,但是稍安勿躁,虽然这很啰嗦,但是值得的,我们最后花点时间从底层看看现代伟大的发明计算机是如何执行的。
现代计算机的理论模型是Turing图灵机,由Alan Turing在1936年的一篇论文中提出。你可以从这个网址找到原始论文:www.cs.virginia.edu/~robins/Turing_Paper_1936.pdf。
图灵机定义:M=(Q,Ε,Γ,δ,q0,B,F)
其中,Q是有限状态集,Ε是输入字母表,Γ是带符号集,δ:是动作函数(L表示读写头向左移动一格,R表示读写头向右移动一格),q0(q0∈Q)是初始状态,B(B∈Γ)表示空白符,F(F⊆Q)是终止状态集。
哎呀,许多人看着这个就头大了,我在这里写出来也只是想让大家知道Turing机是得到了严格数学证明的东西。
Alan Turing是英国数学家,他奠定了理论计算机的基础,提出了判定机器是否具有智能的图灵测试,是计算机科学之父和人工智能之父。二战期间,他破译了纳粹德国密码,为胜利做出了巨大贡献。
因为Turing是同性恋,为当时的社会所不容,被法庭宣判有罪,遂于1954年服毒自杀。2013年12月24日,英国女王Elizabeth II伊丽莎白二世赦免1952年因同性恋行为被定罪的Turing。之后Turing的家人联合50万人签名继续请愿,最终在2017年1月31日,Alan Turing法案生效,约49,000位因同性恋定罪者被赦免。作为对他的敬意,你将在2021年的50英镑钞票上看到图灵的头像。在人类走向文明和包容的历史进程中,也有我们的计算机科学之父流下的血。

计算机内部探秘:程序执行_计算机内部探秘_02

(Alan Turing,1912.6.23-1954.6.7,图片来源:维基百科)


我们来看看Turing机形象的表示:

计算机内部探秘:程序执行_计算机内部探秘_03

这个概念机器简单到出人意外,更加出人意外的是这台机器就表示了人的最大可计算能力!我们从小就被告知科技的进步是无止尽的,人类认识世界的能力也是没有穷尽的。但是科学告诉我们,我们必须认识到有些东西是人类不可认识到的。数学和逻辑学大师哥德尔在1931年发表的不完备定理就证明了这点。
看一下Turing机的组成部分:

计算机内部探秘:程序执行_计算机内部探秘_04

1 一个无限长的存储带,带子有一个个连续的存储格子组成,每个格子可以存储一个数字或符号;
2 一个读写头,读写头可以在存储带上左右移动,并可以读、修改存储格上的数字或符号;
3 内部状态存储器,该存储器可以记录图灵机的当前状态,并且有一种特殊状态为停机状态;
4 控制程序指令,指令可以根据当前状态以及当前读写头所指的格子上的符号来确定读写头下一步的动作(左移还是右移),并改变状态存储器的值,令机器进入一个新的状态或保持状态不变。
按照Turing的理论模型,我们设计计算机的时候要怎么做呢?历史上有很多种尝试,最后是伟大的数学家物理学家Von Neumann冯诺伊曼在1945年写了一个报告《First Draft of a Report on the EDVAC》进行了规定,后来都按照他的规定来做的,所以现代计算机体系也称位Von Neumann冯诺伊曼体系结构。他写的这份报告总共有101页,史称101报告(原文见http://archive.org/download/firstdraftofrepo00vonn/firstdraftofrepo00vonn.pdf)。

计算机内部探秘:程序执行_计算机内部探秘_05

(John Von Neumann,1903.12.28-1957.2.8,图片来源:维基百科)

计算机内部探秘:程序执行_计算机内部探秘_06

(101报告)


在这个报告里,提出了现代计算机的结构:

计算机内部探秘:程序执行_计算机内部探秘_07

冯诺依曼体系结构规定:
(1)程序和数据都用二进制数表示;
(2)采用存储程序方式,指令和数据统一存储在同一个存储器中;
(3)顺序执行程序;
(4)计算机由运算器、控制器、存储器、输入和输出设备五部分组成。各个部分之间用总线(Bus)连接,用于传递数据。
这个结构有一个特点是程序与数据都是一样地存放在存储器里面的(我们前面提到过,运算本身页是0/1表示的,数据也是0/1表示的,它们从形式上具有同一性)。
我们现在说的CPU就是由控制器+算术逻辑单元+寄存器组成的。

计算机内部探秘:程序执行_计算机内部探秘_08

1946年造出来的第一台计算机是这个样子:

计算机内部探秘:程序执行_计算机内部探秘_09

(ENIAC,第一台数字电子计算机,1946)


好,我们现在用一台简单的计算机来演练一番:

计算机内部探秘:程序执行_计算机内部探秘_10

我们的这台极简计算机有一个控制器,它的作用从指定的地址读指令,指令地址存在PC中,指令存在IR中。读完这个地址后,PC就就+1,准备读下一条地址。控制器按照指令进行控制。
这台机器有一个算术逻辑单元ALU,它负责几个算术逻辑操作,如加减与或非异或移位等等基本运算。操作的数据对象放在寄存器中。
这台机器有16个寄存器,用于存放运算过程中的数据。
这台机器有存储器,保存程序和输入的数据,通过8位地址确定指令和数据的位置。控制器从存储器读取指令,加载数据也回存数据。
起连接作用的总线我没有画出来。
我们通过代码把这些功能描述出来,如加载数据,功能是从存储器的某个位置加载数据到寄存器,我们记为LOAD  R0  M40,解释为从地址为40的内存中加载数据到0号寄存器中。如加法,我们记为 ADD  R0  R1  R2,解释为把1号寄存器的数据和2号寄存器的数据相加,结果放到0号寄存器中。
这台机器还要能停下来,所以我们需要给指令集增加一条特殊指令HALT。
为了便于统一处理,我们给这个操作编码,后面的操作数也规定统一的格式。比如我们规定一条指令由16位组成,分成四段,每段4位,第一段位指令段,后面三段表示操作数地址。好,我们把这台机器能做的事情列一个表格,叫指令集:

 

指令

编码

数据1

数据2

数据3

描述

 

HALT

0

     

停机

 

LOAD

1

0

40

把存储器40地址里的数据加载到寄存器0中

 

STORE

2

41

2

把寄存器1中的数据存到存储器41地址里

 

ADD

3

2

0

1

把寄存器0中的数据和寄存器1中的数据相加,存放到寄存器2中

SUB

4

2

0

1

把寄存器0中的数据和寄存器1中的数据相减,存放到寄存器2中

MOVE

5

1

0

 

把寄存器0中的数据移到寄存器1中

AND

6

2

0

1

把寄存器0中的数据和寄存器1中的数据进行逻辑与操作,存放到寄存器2中

OR

7

2

0

1

把寄存器0中的数据和寄存器1中的数据进行逻辑或操作,存放到寄存器2中

NOT

8

1

0

 

把寄存器0中的数据进行逻辑非操作,存放到寄存器1中

XOR

9

2

0

1

把寄存器0中的数据和寄存器1中的数据进行逻辑异或操作,存放到寄存器2中

LSHIFT

A

0

n

 

把寄存器0中的数据左移n次

RSHIFT

B

0

n

 

把寄存器0中的数据右移n次

JUMP

C

n

 

把当前PC指令地址设为n

我们试着构建的这台计算机能执行12条指令。有些指令用不到三个数据段,可以默认给0。
有了这个我们可以编程了。我们以Python的一条语句c=a+b为例。
程序写成:
指令1,加载数据a(假设内存地址为40)到寄存器0
指令2,加载数据b(假设内存地址为41)到寄存器1
指令3,把寄存器0和寄存器1的数据相加,存到寄存器2
指令4,把寄存器2的数据回存内存地址42中
指令5,停机
代码表示为:
LOAD R0 40
LOAD R1 41
ADD R2 R0 R1
STORE 42 R2
HALT
用十六进制表示为:
1040
1141
3201
2422
0000
把这个程序装进计算机,按照冯诺依曼体系结构,也是装在内存中的,我们可以规定内存区域的开头64个地址为程序保留(十六进制地址编号为00-3F)。
先不管程序和数据怎么装进去的,在装载好之后,内存里是这个样子:

计算机内部探秘:程序执行_计算机内部探秘_11

程序执行过程:
CPU初始化状态,PC设为00.
控制器读取00地址的指令,讲1040读取到IR中,PC加1编程01
控制器讲IR中的指令1040译码为M40->R0(将40地址的数据加载到0号寄存器)。
控制器执行指令。
执行完这条指令后,我们的这台计算机变成下面的样子:

计算机内部探秘:程序执行_计算机内部探秘_12

以上三条为一个机器执行周期(取指令-译码-执行指令),就这么不断地执行下去,直到遇到HALT指令。
这就是现代计算机的执行过程。学到这一步,我们终于看到了庐山真面目。之前我们学了那么多基础,进行铺垫,一步一步辛苦叠加,“为伊消得人憔悴”,但是功夫不负有心人,最后在这里我们触摸到了编程知识的硬核,“众里寻他千百度,慕然回首,那人却在,灯火阑珊处。”
希望你在这里领略到了现代计算的精确之美和秩序之美。
对这方面的入门书籍,我推荐你看看Charles Petzold的名著《Code》。
这台机器能跑起来了,但是你看到了它缺少输入设备,我们没办法把程序和数据给它输入进去。我们不仔细探讨了,从原理上,这台机器已经可以运行了。怎么把程序和数据输入进去是另一个课题,实际上只要把机电设备转换成电子信号的办法都可以,如键盘鼠标,纸带。
下面是穿孔纸带:

计算机内部探秘:程序执行_计算机内部探秘_13

穿孔纸带的原理说明:

计算机内部探秘:程序执行_计算机内部探秘_14

穿孔纸带上每个数据孔位上面有一根金属针,下面有容器,里面装着水银。
按下压板时,纸带上有孔的地方,针通过,与水银接触,电路通,没有孔的地方,针被挡住了,电路不通。这就把纸带上的01(有孔无孔)转成了电信号。
那么程序和数据其实就是纸带上的孔表示的,如规定有孔的地方为1,没有空的地方为0。几十年以前的程序员就是这么编程序的,用一个东西给纸带打孔(比起第一代程序员钻进计算机里连线已经进步很多了)。Debug可费劲了,需要用纸片把打错的孔糊起来。那个时候的程序员,还具有传统工匠的样子。
别小看这些,阿波罗登月,控制程序就是用了很多卷纸的。阿波罗登月计划中,有一个职位叫“重量控制官”,是控制搭载物体的重量的,太重的东西不能上。这个重量控制官就挨个部门跑,收集物品重量记在小本子上,跑到程序部门,那个时候的人们对程序还没有什么概念,就问“你们部门搭载的程序有多重?”,程序员说没有重量,这个官员不信,他头脑里凡是东西都会有重量的。过了三天,重量控制官又来了,这次手里抱了一大卷纸,跟程序员们说,你们的程序我称过了,35磅。这些程序员用可怜的眼神看着重量控制官说“纸带不是程序,上面打的孔才是。”
现在我们了解了冯诺依曼体系结构如何执行程序,这个体系有超过七十年的时间了,我相信这个体系撑不了一百年,未来会如何发展?
在未来的一二十年里,我觉得会将CPU和存储器统一起来。现在这个体系结构的性能瓶颈在于总线结构,数据老是要通过总线在存储器和CPU之间传递,当多CPU多核和海量数据构成主流需求后(如人工智能),这个瓶颈会异常明显。
更加远的未来呢?留给你自己去想象。
Google的著名专家Ray Kurzweil写过一本很有影响力的书《The Singularity is Near》(奇点临近)。
对计算机内部的结构和执行过程我们就讲到此为止,这是编程的书,我们可以跳出物理结构了,只关注概念逻辑,后面,我们只在算法和Python语言的层面来继续编写各种程序。
好,欢迎来到虚拟逻辑世界。