其实学习80C51单片机的时候已经学过了一些相关知识,觉得还算有趣,我们通常使用的计算机原来是怎么构成的,就是真实感受汇编语言不会编,记得大作业那个红绿灯搞了我很久,当时搞出来的时候我热泪盈眶。
上面是闲话,还是当越深入学习,越会发现计算机组成的重要性。
比如知道js里面0.1 + 0.2 是不等于0.3的,为什么呢?这就牵扯到计算机组成原理中浮点数的表示方法,以及浮点数的加减运算,当时学Verilog的大作业,对于浮点数来说,乘除法反而好些,加减法写了半天。
又例如从键盘输入a+b这个指令,如何通过cpu的调度输出到屏幕上呢?这就涉及到冯诺依曼体系,如果你是编程人员,都不清楚数据从键盘到屏幕的基本流向,输入、输出设备、中央处理器是做什么的都不知道,真的该下定决心补一补这些基础。
1.计算机的基本组成
1.1 冯·诺依曼计算机的特点
- 计算机由运算器、存储器、控制器、输入设备和输出设备组成
- 指令由操作码(表示操作的性质)和地址码(表示操作数在存储器中的位置)组成
- 指令和数据皆用二进制表示
- 指令和数据以同等地位存放于存储器内,并可按地址寻访
- 指令在存储器内按顺序存放,通常是顺序执行的。但也可根据运算结果或设定的条件,改变执行顺序
- 机器以运算器为中心,输入输出设备与存储器之间的数据传送通过运算器完成
1.2 计算机硬件框图
现代计算机可认为由三大部分组成:CPU、I/O 设备及主存储器(Main Memory,MM),如下图所示:

- M⋅M(Main Memory):主存储器,也就是常说的内存,与CPU直接交换信息。此外还有辅存,如硬盘、U盘等;
- ALU(Arithmetic Logic Unit):算数逻辑运算单元;
- CU(Contro Unit):控制单元,解释存储器中的指令,并发出各种命令执行指令。
1.3 细化的计算机组成框图
为了形象地了解计算机的工作过程,对现代计算机的组成框图进行细化:

1.3.1 运算器
运算器最少包含 3 个寄存器(现代计算机处理器内部往往设有通用寄存器组,如 ARM Cortex-A8 处理器,有 40 个 32bit 的寄存器(32 个通用寄存器,7 个状态寄存器,1 个程序计数器(PC,Program Counter)))和一个算数逻辑运算单元(ALU)。
- ACC(Accumulator):累加器
- MQ(Multiple-Quotient register):乘商寄存器
- X:操作数寄存器
1.3.2 控制器
控制器是计算机的神经中枢,由它指挥各部件自动、协调地运行。具体而言:
- 读取指令:命令存储器读出一条指令
- 分析指令:指出该指令需要完成什么操作,并按寻址特征指出操作数的地址
- 执行指令:根据操作数所在地址以及指令的操作码,完成某种操作
控制器由程序计数器(Program Counter,PC)、指令寄存器(Instruction Register,IR)和控制单元(CU)组成:
- PC:存放当前欲执行指令的地址,与主存的 MAR(Memory Address Register,存储器地址寄存器)有一条直接通路,且具备自动 +1 的功能,即可自动形成下一条指令的地址
- IR:存放当前的指令,其内容来自主存的 MDR。IR 中的操作码 (OP(IR)) 送至CU,用来分析指令。其地址码 (Ad(IR)) 作为操作数的地址,送至存储器的 MAR
- CU:分析当前指令所需完成的操作,并发出各种微操作序列,用以控制所有被控对象
1.3.3 主存储器
主存储器(简称主存或内存)包括存储体M、各种逻辑部件和控制电路等。存储体由许多存储单元组成,每个存储单元又包含很多存储元件,每个存储元件可以存储一位二进制代码 0 或 1。可见一个存储单元可存储一串二进制代码,称这串二进制代码为一个存储字,这串二进制代码的位数称为存储字长。存储字长可以是 8 位、16 位、32 位等。一个存储字可代表一个二进制数、一串十六进制字符、两个 ASCII 码或者一条指令。
每个存储单元都有自己的地址,主存的工作方式就是按照存储单元的地址实现对存储字各位的读写,而 MAR、MDR 则用来实现按地址访问:
- MAR(Memory Address Register):存储器地址寄存器,存放欲访问的存储单元的地址。其位数对应存储单元的个数,如 MAR 是 32 位,则共有 2^32 = 4 * 1024 * 1024 * 1024 (1024个记作 1K) 个存储单元
- MDR(Memory Data Register):存储器数据寄存器,存放从存储体读出的数据或准备写入存储体的数据(可以是代码,也可以是指令),其位数与存储字长相等
当然,要想实现一个完整的读/写操作,CPU还需要给主存发送各种控制信号,如读命令、写命令、地址译码驱动信号等。随着硬件电路的发展,主存都制成大规模集成电路的芯片,而将MAR、MDR 集成在 CPU 芯片中。
早期计算机的存储字长一般与机器的指令字长、数据字长相等,故访问一次主存便可取一条指令或一个数据。随着计算机应用范围的不断扩大,往往要求计算机的指令字长、数据字长是可变的。为了适应指令字长和数据字长的可变性,其长度不再由存储字长确定,而由字节的个数来表示。比如 4 字节的指令字长就是 32bit,2 字节的指令字长就是 16bit。至此,指令字长、数据字长和存储字长不必相等,但都必须是字节的整数倍。
下图为32位架构的ARM存储器组织结构,其基本数据类型有:

- Byte:字节,8位;
- HalfWord:半字,16位(半字必须与2字节边界对齐);
- Word:字,32位(字必须与4字节边界对齐);
- Double World(Cortex-A支持):双字,64位(双字必须与8字节边界对齐)。
2. 计算机的工作原理
首先,计算机最基本的5大组成部分如下图,分别为:输入设备(比如键盘、鼠标), 存储器(比如内存、硬盘), 运算器(CPU, 控制器(CPU)(两者合称中央处理器), 输出设备(显示器)。

工作原理如下
2.1 控制器 —> 控制输入设备 —-> 指令流向内存
当我们输入数据的时候,cpu里的控制器会让输入设备把这些指令存储到存储器(内存)上。

2.2 控制器分析指令 —> 控制存储器 —> 把数据送到运算器
控制器分析指令之后, 此时让存储器把数据发送到运算器里(控制器和运算器都在cpu里面)
这里需要注意,存储器既能存储数据,还能存储指令

2.3 控制器控制运算器做数据的运算 并且将运算结果返回存储器

2.4 控制器控制存储器将结果返回给输出设备

3.计算机硬件的主要技术指标
3.1 机器字长
机器字长是指计算机进行一次整数运算所能处理的二进制数据的位数(整数运算即定点整数运算)。因为计算机中数的表示有定点数和浮点数之分,定点数又有定点整数和定点小数之分,这里所说的整数运算即定点整数运算。机器字长也就是运算器进行定点数运算的字长,即通用寄存器的位数。
主存字长一般等于机器字长,不等的情况下,一般是主存储器字长小于机器字长。例如机器字长是 32 位,主存储器字长可以是 32 位,也可以是 16 位。
Windows 64 位操作系统是针对 64 位机器字长的 CPU 设计的,目前 64 位架构实现技术主要有 AMD64、Intel EM64T 等。
3.2 存储容量
主存容量 = 存储单元数 * 存储字长
比如,若 MAR 是 32 位,则存储单元个数为: 2^32 = 4 * 1024 * 1024 * 1024个。若存储字长为 8 位,则存储容量 = 4 * 1024 * 1024 * 1024 * 8 bit,即 4G(4 Gigabyte) 。
3.3 运算速度
单位时间内执行的指令平均条数,单位 MIPS(Million Instruction Per Second)。
4.CPU及其工作过程
CPU中比较重要的两个部件是运算器和控制器,我们先来看看运算器的主要作用
4.1 运算器主要部件

如上图,运算器里最重要的部件是ALU,中文叫算术逻辑单元,用来进行算术和逻辑运算的。其它的MQ,ACC这些我们不用管了,是一些寄存器。
4.2 控制器主要部件

控制器中最重要的部件是CU(控制单元),只要是分析指令,给出控制信号。
IR(指令寄存器),存放当前需要执行的指令
PC存放的指令的地址。
4.3 举例 - 取数指令执行过程
首先,是取指令的过程如下

- 第一步,
PC,也就是存放指令地址的地方,我们要知道下一条指令是什么,就必须去存储器拿,CPU才知道接下来做什么。PC去了存储器的MAR拿要执行的指令地址,MAR(存储器里专门存指令地址的地方) - 第二步和第三步,
MAR去存储体内拿到指令之后,将指令地址放入MDR(存储器里专门存数据的地方) - 第四步
MDR里的数据返回到IR里面,IR是存放指令的地方,我们把刚才从存储体里拿的指令放在这里
然后,分析指令,执行指令的过程如下

- 第五步,
IR将指令放入CU中,来分析指令,比如说分析出是一个取数指令,接着就要执行指令了(这里取数指令,其实就是一个地址码,按着这个地址去存储体取数据) - 第六步,第七步
IR就会接着去找存储体里的MAR(存储地址的地方),MAR就根据取数指令里的地址吗去存储体里去数据 - 第八步,取出的数据返回给
MDR(存放数据的地方) - 第九步,
MDR里的数据放到运算器的寄存器里,这里的取指令的过程结束了。
来个插曲,我们知道数据在内存里是二进制存着,也就是0和1, 0和1怎么用表示呢?
我们拿其中一种存储0和1的方式来说明
- 电容是否有电荷,有电荷代表1,无电荷代表0
- 如下图


5.计算机编程语言
5.1 机器语言
存放一个数的指令,例如下图

我们来看二进制代码 0000,0000,000000010000
- 其中第一个
0000,表示的是汇编语言里的LOAD,也就是加载,加载什么呢 - 加载地址
000000010000上的数据到第二个0000(寄存器的位置)。
5.2 汇编语言

LOAD A, 16意思是将存储体内的16号单元数据,放到寄存器地址A中 ADD C, A, B意思是将寄存器里的A,B数据相加,得到C STORE C, 17意思是将寄存器里的数据存到存储体17号单元内
5.3 高级语言

高级语言是不是很简单,就一个a+b,你都不用去考虑寄存器,存储体这些事。

最初的计算机并没有微指令系统。由于 M0、M1 都是实际存在的,为了区分,这里分为微程序机器、传统机器。
将高级语言翻译成机器语言的程序叫做翻译程序,翻译程序分为编译程序与解释程序两种类型:
- 编译程序:一次性将高级语言全部翻译成机器语言,直接借助
编译器,将高级语言转换为二进制代码,比如c,这样c运行起来就特别快,因为编译后是机器语言,直接就能在系统上跑,但问题是,编译的速度可能会比较慢。 - 解释程序:翻译一句执行一句,重新执行需要再次翻译,比如
js,是将代码翻译一行成机器语言(中间可能会先翻译为汇编代码或者字节码),解释一行,执行一行
需要注意的是,按照第一种将大量的高级代码翻译为机器语言,这其中就有很大的空间给编译器做代码优化,解释性语言就很难做这种优化,但是在v8引擎中,js还是要被优化的,在编译阶段(代码分编译和执行两个阶段)会对代码做一些优化,编译后立即执行的方式通常被称为 JIT (Just In Time) Comipler。
6.进制转换
6.1 二进制如何转化为十进制
例如2进制101.1如何转化为10进制。(有些同学觉得可以用parseInt('101.1', 2),这个是不行的,因为parseInt返回整数)
转化方法如下:

上图的规则是什么呢?
二进制的每个数去乘以2的相应次方,注意小数点后是乘以它的负相应次方。 再举一个例子你就明白了,
二进制1101转为十进制

6.2 十进制整数转为二进制
JS里面可以用toString(2)这个方法来转换。如果要用通用的方法,例如:将十进制数(29)转换成二进制数, 算法如下:
- 把给定的十进制数29除以2,商为14,所得的余数1是二进制数的最低位的数码
- 再将14除以2,商为7,余数为0
- 再将7除以2,商为3,余数为1,再将3除以2,商为1,余数为1
- 再将1除以2,商为0,余数为1是二进制数的最高位的数码

其结果为:11101
6.3 十进制小数转为二进制
方式是采用“乘2取整,顺序排列”法。具体做法是:
- 用2乘十进制小数,可以得到积,将积的整数部分取出-
- 再用2乘余下的小数部分,又得到一个积,再将积的整数部分取出-
- 如此进行,直到积中的小数部分为零,或者达到所要求的精度为止
我们具体举一个例子
如: 十进制 0.25 转为二进制
0.25 * 2 = 0.5取出整数部分:00.5 * 2 = 1.0取出整数部分1
即十进制0.25的二进制为 0.01 ( 第一次所得到为最高位,最后一次得到为最低位)
此时我们可以试试十进制0.1和0.2如何转为二进制
1 | 0.1(十进制) = 0.0001100110011001(二进制) |
接下来看0.2
1 | 0.2化二进制是 |
所以0.1和0.2都无法完美转化为二进制,所以它们相加当然不是0.3了
7.定点数和浮点数
首先,什么是定点数呢?
7.1 定点数

如上图,举例纯整数的二进制1011和-1011,如果是整数,符号位用0表示,如果是负数符号为用1表示

同理,纯小数表示举例如下:

那如果不是纯小数或者纯整数,该怎么表示呢?
比如10.1, 可以乘以一个比例因子,将10.1 ---> 101 比例因子是10, 或者10.1 ---> 0.101比例因子是100
定点数很简单,接下来我们介绍浮点数,在JS里面,数字都是用双精度的浮点数,所以学习浮点数对我们理解JS的数字有帮助。
7.2 浮点数
那么浮点数怎么表示呢?

上面是十进制的科学计数法,从中我们需要了解几个概念,一个是尾数,基数和阶码
尾数必须是纯小数,所以上图中1.2345不满足尾数的格式,需要改成0.12345基数,在二进制里面是2阶码就是多少次方
所以浮点数的通用表示格式如下:

- S代表尾数
- r代表基数
- j代表阶码
这里需要注意的是,浮点数的加减运算,并不是像我们上面介绍的那样简单,会经过以下几个步骤完成

这些名词大家感兴趣的话,可以去网上查询,我们只要了解到浮点数加减运算很麻烦就行了,但如果你要做一个浮点数运算的库,你肯定是要完全掌握的。
8.局部性原理和cache

在虚拟存储系统中,程序员的编程地址范围与虚拟存储器的地址空间相对应。例如机器指令地址码是 32 位,那么虚拟存储器的存储单元个数可达 2^32 = 4 * 1024 * 1024 * 1024 个,若存储字长为 8 位,则存储容量为 4 Gegabyte,这可能比主存实际的存储单元个数多得多。这类指令地址码称为虚拟地址或逻辑地址,主存的实际地址称为物理地址。虚拟地址到物理地址的转换由操作系统负责实现,比如 Windows 操作系统通过页目和页表来实现虚拟地址到物理地址的转换。若虚拟地址指向的内容在主存,则可被 CPU 直接使用,否则必须先传到主存,然后才能被 CPU 访问。
选择下图

(说明一下,MDR和MAR虽然逻辑上属于主存,但是在电路实现的时候,MDR和MAR离CPU比较近)
上图是在执行一串代码,可以理解为js的for循环
1 | const n = 1000; |
我们可以发现
- 数组的数据有时候在内存是连续存储的
- 如果我们要取数据,比如从内存取出a[0]的数据需要1000ns(ns是纳秒的意思),那么取出a[0]到a[7]就需要1000 * 8 = 8000 ns
- 如果我们cpu发现这是取数组数据,那么我就把就近的数据块a[0]到a[7]全部存到缓存上多好,这样只需要取一次数据,消耗1000ns
cache就是局部性原理的一个应用

空间局部性:在最近的未来要用到的信息(指令和数据),很可能与现在正在使用的信息在存储空间上是邻近的时间局部性:在最近的未来要用到的信息,很可能是现在正在使用的信息

可以看到cache一次性取了a[0]到a[9]存储体上的数据,只需要1000ns,因为cache是高速存储器,跟cpu交互速度就比cpu跟主存交互速度快很多。
9.I/O设备的演变
I/O是什么呢?
1 | 输入/输出(Input /Output ,简称I/O),指的是一切操作、程序或设备与计算机之间发生的数据传输过程。 |
比如文件读写操作,就是典型的I/O操作。接下来我们看一下I/O设备的演进过程

在早期的计算机里,CPU如何知道I/O设备已经完成任务呢?比如说怎么知道I/O设备已经读取完一个文件的数据呢?CPU会不断查询I/O设备是否已经准备好。这时,CPU就处于等待状态。也就是CPU工作的时候,I/O系统是不工作的,I/O系统工作,CPU是不工作。
接着看第二阶段

- 为了解决第一阶段
CPU要等待I/O设备,串行的工作方式,所有I/O设备通过I/O总线来跟CPU打交道,一旦某个I/O设备完成任务,就会以中断请求的方式,通过I/O总线,告诉CPU,我已经准备好了。 - 但是对于
高速外设,它们完成任务的速度很快,所以会频繁中断CPU, 为了解决这个问题,高速外设跟主存之间用一条直接数据通路,DMA总线连接,CPU只需要安排开始高速外设做什么,剩下的就不用管了,这样就可以防止频繁中断CPU。
最后来看一下第三阶段

第三阶段,CPU通过通道控制部件来管理I/O设备,CPU不需要帮它安排任务,只需要简单的发出启动和停止类似的命令,通道部件就会自动的安排相应的I/O设备工作
10.系统总线
同一时刻只能有一个部件向总线发送信息,但是可以有多个部件接受信息,因为总线是各部件共享的。
10.1 片内总线
芯片内部的连线,比如寄存器之间、寄存器与 ALU 之间的连线等。
10.2 系统总线
数据总线:双向传输,其条数称为数据总线宽度。数据总线宽度与机器字长、存储字长有关。比如总线宽度是 8 位,指令字长为 16 位,那么 CPU 取出一条指令,就需要访问两次主存。
地址总线:单向传输,指出数据总线上的源数据或目的数据所在存储单元的地址。地址总线的宽度与存储单元个数有关,比如 32 位的地址总线,可编址按字节寻址的存储单元个数为 2^32 = 4 * 1024 * 1024 * 1024 ,即 4 Gigabyte。
从存储器读一个字的数据时,首先由 CPU 将其地址经 MAR 通过地址总线送至主存,然后向主存发读命令。主存接到读命令后,将对应数据读出后,经数据总线送至 MDR。向存储器写一个字的数据时,CPU 先将目的地址经 MDR 通过地址总线送至主存,并将数据送至 MDR,然后向主存发写命令。主存接到写命令后,便可以将 MDR 中的数据经数据总线写至目的地址:

- 控制总线:决策总线使用权,用来发出各种控制信号。I/O 设备通过控制总线向 CPU 发出总线请求,CPU 通过控制总线向 I/O 设备发出读写命令。
- 通信总线:用于计算机系统之间或计算机系统与其它系统(如控制仪表、移动通讯等)之间通信。