计算机系统003 - 硬件体系
前一篇中粗略讲述了二进制加法运算的过程,其中假定数据是从寄存器直接装载到加法器两端,加法器产生的结果也同样保存在另一寄存器中,而没有在意数据是如何从外界传递到了寄存器里,也并未给出寄存器中结果将以何种方式呈现给加法操作的真正用户。
因此本章将从计算机体系结构入手,介绍自然语言与机器语言间的共识。
1. 问题表现形式
当我们希望求助他人帮忙解决某一问题时,首先要做的是用双方共同支持的自然语言(如普通话、英语)来描述问题,等待对方提出解决问题的方法或是协同完成。然而如果希望将某一任务交付给计算机来完成,那么很抱歉,计算机并不会思考,他只是流程的接收和执行者,也就是说,如何形成流程,仍然需要我们来完成。
也可能有人会讲,不是啊,当我打开计算机,双击一个视频文件,计算机就会帮我播放,我并没有告诉它如何播放,用什么播放吧。但你回过头来看看这段文字,就可以发现其中还是含有通用流程,和你要寻找食物必须打开冰箱一样。至于视频播放的真正过程,后面会讲到,此处先给一个概念。
所有的信息都是以电信号存储于存储器中,读取后得到的是二进制字节流,至于该字节流描述成什么具体内容需要依赖上下文,以及解释器。比如同样一句“不好吧”使用不同语气,对于不同接收者而言可能获取到不同的含义。同样,一串字节流,使用音频播放器可能被解析为声音,使用notepad可能被处理为文本,甚至如果想象力再丰富一点,也可以假象成这是一个文字解密游戏。
因此通常会在字节流中埋下一些记号,标明数据类型,这些称为元数据。在元数据基础上,不同视频格式会有不同数据组合方式,通过枚举不同格式尝试解析视频文件,如可以匹配为某格式,就按照该格式进行播放;如不能,则报错不能正确播放某文件。
综上所述,要想托管任务给计算机执行,那么计算机必须有两个充分条件:
- 接收任务描述:支持以程序(流程)方式表述处理过程
- 具有运行能力:能够执行程序
计算机能够执行的任务归根结底是数学计算,而对于如何进行统一、可行的任务描述,人们做了大量尝试,直到图灵机概念的出现。
1.1 图灵机
图灵为了给出计算机的清晰数学描述,他观察人们计算式采用的方法和行为,然后将其抽象成一种统一的表达机制。图灵将人们用纸笔进行数学运算的过程抽象成下列两种简单动作:
- 在纸上写上或擦除某个符号
- 把注意力从纸的一个位置移动到另一个位置
为了模拟人的运算过程,图灵造出一台假想的机器,该机器按照读取规则读取纸带上数值,然后进行计算。
这台机器主要由如下几个部分组成:
- 一条无限长的纸带TAPE
- 一个读写头HEAD
- 一套控制规则TABLE
- 一个状态寄存器
简而言之就是TABLE中规则控制读写头HEAD在无限长纸带TAPE上读写,根据读写内容进行运算,并选择是否保存结果至纸带特定位置中。如运行过程中发生状态变化,则保留在状态寄存器内。
1.2 七层转换
图灵机是思想指导,回归到实践而言,对于任务描述的方法可表达为七层转换。《计算机系统概论》1.7节中提出,要控制电子器件按照我们的意图工作,需经历如下七个完整过程:
-
6 问题
通常我们采用“自然语言”来描述问题,如英语、汉语等,存在的问题是有很多语言组成部分具有二义性,而计算机指令很可能因为无法确定语句的准确含义而采取错误措施,导致错误结果。因此,剩下的几层转换,均可以视为消除二义性的步骤。 -
5 算法
算法描述的特定是流程化、步骤清晰,并确保该流程能终止。具体如下:- 确定性,表明每个操作步骤的描述是清晰的、可定义的
- 可计算性,表示每一步的描述都可被计算机执行
- 有限性,表示过程是会终止的
-
4 语言(开发语言)
有了人类可以理解的计算方法,还需要转达计算机如何计算。计算机只能识别“机械语言”,这种语言具有严格的顺序方式,以便让计算机顺序地执行指令序列。这一层的语言通常称为开发语言,如C/C++,Java,Python等,由于开发语言需要程序员编写,因此仍具有一定可读性。 -
3 机器(ISA)结构
然而到开发语言一层仍然不够,因为开发语言可能是面向多种硬件环境的,为了在特定的硬件平台上也能够顺利执行,还要做一次翻译,即将程序由开发语言翻译成为特定计算机的指令集。该部分功能通常由“编译器”或“解释器”来完成。
指令集结构实际上就是程序和计算机硬件之间接口的一个完整定义。
-
2 微结构
有了接口,就可以顺利完成任务描述,而真正要调动硬件的运行能力,还需要将各接口进行实现。比如很多处理器都使用了ISA X86,但具体实现却可以各不相同。 -
1 电路
微结构的概念最终要落实到一组组简单的逻辑电路,同样一个功能,如加法器,可以使用多种实现方式,不同的实现方式也同样带来了性能和成本上的差异。需要设计者从全局上进行衡量取舍。 -
0 器件
最后,相同的逻辑电路也仍然可以使用器件技术来实现,如材料、规格等等。
综上所述,从人类自然语言表述的问题到最终执行的单元器件,需要做七层转换。对于为什么要分层以及为什么是七层,通常来讲,高层通常是低层的抽象结果,意在忽略部分无关细节,提供分析问题的更高层次的视野。不过到目前为止表述的方法都是理论性的,接下来就看一下历史中实践的结果。
2. 冯·诺依曼结构
冯·诺依曼结构是一种将程序指令存储器和数据存储器合并在一起的电脑设计概念结构,它用于实现通用图灵机和一种相对于并行计算的序列式结构参考模型。
接下来我们对冯·诺依曼结构进行细化,如下图所示。
从图中可以看出,冯·诺依曼结构主要有如下5个组成部分:
-
内存
内存存放程序,访问内存的第一步,是想内存提供被访问内存单元的地址。以读写操作为例:- 读:将访问内存单元的地址放入MAR;发送读信号通知内存;内存将该单元中存放的数据传送至MDR
- 写:将访问内存单元的地址放入MAR;将要写入的数据放入MDR,发送写信号通知内存
-
处理单元
ALU所能处理的量化大小通常称为计算机的字长,如X86,X64。设计中通常会在ALU附近配置少量存储器,以便存放最近生成的中间计算结果。 -
输入
通过计算机解决问题的前提是能够将问题描述成电信号,转换层次可参考1.2小节。 -
输出
计算结果要具有可读性就必须通过设备进行输出,或是显示器、或是音响等可将电信号转换为自然语言的设备。 -
控制单元
控制单元负责指令的有序执行,其中具有两个特殊寄存器。- 指令寄存器(IR),保存当前执行的指令
- PC寄存器,指示下一条待处理的指令,也可视为“指令指针”
哈佛结构
虽然冯·诺依曼结构是现代计算机的主要结构,但我们也必须知道,还有一个结构叫做哈佛结构。它和冯·诺依曼结构之间最大的差异在于数据在内存中排列方式。冯·诺依曼结构中允许指令和数据混合存储在同一存储模块中,而哈佛结构必须使用独立的存储模块来分别存储指令和数据。
2.1 硬件实现(0 器件,1 电路)
本系列的第一篇中主要介绍了电学知识,为的就是在理解计算机时能够形成完整通路,而不是只能触及操作系统这一层,视底层硬件为黑盒,不知所以然。从前面我们知道硬件只能识别电信号,无论是组合逻辑电路还是时序逻辑电路,输出的产生终归需要输入的参与,而在托管任务时,任务就是我们需要的输入。
然而我们不可能在托管任务后还要守在一旁切换电信号以提供准确输入换取输出,既不高效也不可理智。因此最好能将任务描述使用一种能够最终转换为电信号的语言进行描述,而硬件在收到任务描述后,根据其中规则和数据自动完成计算,产出结果。同时为了保证任务描述的精确性,对于电信号我们进行了一次抽象,将信号幅度抽象成有无,对应为0和1,即所谓的二进制。
2.2 指令结构及实现
冯·诺依曼结构在现有基础上提出计算过程应该是:程序和数据都是以bit流的方式存放在计算机内存中,程序在控制单元的控制下,依次完成指令的读取和执行。
二进制在历史悠久的人类自然语言面前是过于苍白的,为了能够将二进制位和人类想表达的操作对应起来,这一次,抽象的任务落在了自然语言上。既然计算机实质上是负责计算任务,因此可以不直接参考自然语言,而是选择抽象数学运算的基本规则(如加减乘除),形成指令。
目前通用计算机中指令是其执行的最小单位,指令本身本身由操作码和操作数组成。操作码标明其行为,操作数标明行为作用对象。
指令集架构还定义了其他内容,现存的指令集结构也各不相同,如Intel和AMD系的。但通常一条指令包含内容如下:
指令的处理过程是在控制单元控制下一步步完成的,这里执行的步骤顺序称为指令周期,每一步称为节拍,通常一个指令周期包含6个节拍,分别如下:
-
取指令FETCH
从内存中读取下一条待执行的指令,并将其装入控制单元的指令寄存器IR中。- 将PC寄存器的内容装入MAR寄存器
- 该地址对应内存单元的内容(即下一条指令)被装入MDR
- 控制单元将MDR内容装入IR寄存器
- PC寄存器内容自增
-
译码DECODE
译码操作的任务是分析、检查指令的类型,并确定对应微结构操作细节。
换句人话就是将几个信号通过们电路扩展为多个单个信号,代表多个模式,而译码器后的器件可根据不同模式执行预设的不同操作。 -
地址计算EVALUATE ADDRESS
如操作数不是立即数(如123),而是寄存器地址等涉及寻址的表达方式时,需要进行地址计算以得到操作数的真正地址。
寻址方式通常有如下几种:
立即寻址 | 操作数为立即数 | MOV BX,8080H |
---|---|---|
寄存器寻址 | 操作数为寄存器 | MOV BX,AX |
直接寻址 | 操作数为地址 | MOV AX,[1234H] |
寄存器间接寻址 | 操作数为SI/DI/BX/BP之一 | MOV BX,[DI] |
寄存器相对寻址 | 操作数为SI/DI/BX/BP之一,加上偏移量 | MOV BX,[DI+100H] |
基址加变址寻址 | 操作数是BX/BP之一,加上SI/DI之一 | MOV BX,[BX+DI] |
相对基址加变址寻址 | 操作数是BX/BP之一,加上SI/DI之一,加上偏移量 | MOV BX,[BX+DI+100H] |
-
取操作数FETCH OPERAND
读取指令处理所需要的源操作数,分为两个步骤:- 将地址计算节拍中结果值装入MAR
- 从MDR中获取读自内存的源操作数
-
执行EXECUTE
负责指令的执行操作,不同操作码在该节拍的操作也各不相同。 -
存放结果STORE RESULT
将执行结果写入目的寄存器,该节拍结束后,循环进入第一个取指令节拍,由于PC寄存器在连续空间内指令上移动,因此也称为顺序执行。有顺序执行自然就有非顺序执行,这里不去深究跳转的具体实现,只要知道,满足条件后需要跳转前,将会修改PC寄存器中内容,跳转完成后,继续顺序执行连续指令,直到下一次跳转的来临。
2.3 从问题到开发语言
终于现在有了指令集,它定义了处理器可以执行的所有操作,而我们剩下要做的就是将问题描述成一条条顺序或条件控制的跳转执行指令。对于简单的、小规模的任务,通过编写汇编函数,通常也能实现功能,除了效率略低,移植性不佳(平台支持的指令集可能不同)等。但对于复杂、大规模的任务,使用指令集语言就略显力不从心了,毕竟所有操作如MOV等都需要小心翼翼地不停叮嘱,这时候人们又想到了抽象。
虽然指令集接口各家、各平台互不相同,那能不能想出一套更高层次的,不在意平台差异的开发语言来描述问题。这样一来,还可以顺便把一些繁琐的、啰嗦的地址处理指令模块化,将任务描述这件事情从细节中解脱?答案是肯定的,现在我们有了更贴近底层的C/C++,主打一次编译处处运行的Java,甚至不要编译纯靠解释器在运行时逐字逐句翻译的Python等脚本语言,这些都让我们能够更关注问题本身,而不是事无巨细的实现。
当然从开发语言到指令集并不是省略了那些繁琐的细节,而是通过编译器、解释器、虚拟化运行环境等模块代劳。软件开发中有一句话和东北烧烤哲学类似,没有什么问题是一顿烧烤不能解决的,如果有,那就两顿;同样没有什么问题是一个中间层不能解决的,如果有,那就两个。
3. 总结
到目前为止,我们阐述了从问题本身出发,形成任务描述(指令),并由电路执行运算。这些都是整体的概念,或者说,对于实现细节,都是做了较多的抽象,为的是从宏观上有所了解。后续将对体系中各组成部分进行分别深入,力求浅出,再不济也要知道一些关键概念。
这里或者本系列也均不会进行任务无谓的语言之争,本系列文章寻求的是问题的解决方法、思维,而不是各实现细节之间的差异。
追寻知识融会贯通后的快感。