前端开发人员中,有相当大比例的同学不是科班出来的,所以对于基本的科班必修课,例如:计算机组成原理、操作系统、计算机网络、数据结构和算法等知识接触不多。
当你越深入学习,越会发现这些知识的重要性。
比如大家都知道js里面0.1 + 0.2 是不等于0.3的,为什么呢?这就牵扯到计算机组成原理中浮点数的表示方法,以及浮点数的加减运算(正文会有白话版解答)。
又例如从键盘输入a+b这个指令,如何通过cpu的调度输出到屏幕上呢?这就涉及到冯诺依曼体系,如果你是编程人员,都不清楚数据从键盘到屏幕的基本流向,那是时候看看这篇'十全大补文'了
本文是一篇计算机组成原理最基本的入门文章,我觉得前端没有必要那么深入这个专题,掌握基本的计算机组成原理的常识即可。
1、计算机的工作原理
首先,计算机最基本的5大组成部分如下图,分别为:输入设备(比如键盘), 存储器(比如内存), 运算器(cpu), 控制器(cpu), 输出设备(显示器)。
![bbc073f6c9d61a443d7be97d402c3ba0.png](https://img-blog.csdnimg.cn/img_convert/bbc073f6c9d61a443d7be97d402c3ba0.png)
工作原理如下
1.1 控制器 ---> 控制输入设备 ----> 指令流向内存
当我们输入数据的时候,cpu里的控制器会让输入设备把这些指令存储到存储器(内存)上。
![18b35e329c9de61fb39904b62ffb2cda.png](https://img-blog.csdnimg.cn/img_convert/18b35e329c9de61fb39904b62ffb2cda.png)
1.2 控制器分析指令 ---> 控制存储器 ---> 把数据送到运算器
控制器分析指令之后, 此时让存储器把数据发送到运算器里(控制器和运算器都在cpu里面)
这里需要注意,存储器既能存储数据,还能存储指令
![29241be8004f7f7846e352cc294c8f05.png](https://img-blog.csdnimg.cn/img_convert/29241be8004f7f7846e352cc294c8f05.png)
1.3 控制器控制运算器做数据的运算 并且将运算结果返回存储器
![984cb54523587e608b7434ce07c6dcc5.png](https://img-blog.csdnimg.cn/img_convert/984cb54523587e608b7434ce07c6dcc5.png)
1.4 控制器控制存储器将结果返回给输出设备
![4f2d49e0328990047397e8f06295cf57.png](https://img-blog.csdnimg.cn/img_convert/4f2d49e0328990047397e8f06295cf57.png)
从接下来,我们更近一步,看看计算机内部,CPU是怎么跟存储器交互的。
2、CPU及其工作过程
CPU中比较重要的两个部件是运算器和控制器,我们先来看看运算器的主要作用
2.1 运算器主要部件
![5dde64de27c6d704c0a5af3d01f08014.png](https://img-blog.csdnimg.cn/img_convert/5dde64de27c6d704c0a5af3d01f08014.png)
如上图,运算器里最重要的部件是ALU,中文叫算术逻辑单元,用来进行算术和逻辑运算的。其它的MQ,ACC这些我们不用管了,是一些寄存器。
2.2 控制器主要部件
![72f1760421493e37fbc331d7f6cffe80.png](https://img-blog.csdnimg.cn/img_convert/72f1760421493e37fbc331d7f6cffe80.png)
控制器中最重要的部件是CU(控制单元),只要是分析指令,给出控制信号。
IR(指令寄存器),存放当前需要执行的指令
PC存放的指令的地址。
2.3 举例 - 取数指令执行过程
首先,是取指令的过程如下
![d72bb56fec2f6f3e0c6e5d13671e6039.png](https://img-blog.csdnimg.cn/img_convert/d72bb56fec2f6f3e0c6e5d13671e6039.png)
第一步,PC,也就是存放指令地址的地方,我们要知道下一条指令是什么,就必须去存储器拿,CPU才知道接下来做什么。PC去了存储器的MAR拿要执行的指令地址,MAR(存储器里专门存指令地址的地方)
第二步和第三步,MAR去存储体内拿到指令之后,将指令地址放入MDR(存储器里专门存数据的地方)
第四步MDR里的数据返回到IR里面,IR是存放指令的地方,我们把刚才从存储体里拿的指令放在这里
然后,分析指令,执行指令的过程如下
![916e8136a4b0af37964446c0d34940ad.png](https://img-blog.csdnimg.cn/img_convert/916e8136a4b0af37964446c0d34940ad.png)
第五步, IR将指令放入CU中,来分析指令,比如说分析出是一个取数指令,接着就要执行指令了(这里取数指令,其实就是一个地址码,按着这个地址去存储体取数据)
第六步,第七步 IR就会接着去找存储体里的MAR(存储地址的地方),MAR就根据取数指令里的地址吗去存储体里去数据
第八步,取出的数据返回给MDR(存放数据的地方)
第九步,MDR里的数据放到运算器的寄存器里,这里的取指令的过程结束了。
来个插曲,我们知道数据在内存里是二进制存着,也就是0和1, 0和1怎么用表示呢?我们拿其中一种存储0和1的方式来说明
电容是否有电荷,有电荷代表1,无电荷代表0
如下图
![3c627d65bcff0639625a818ca5e0b612.png](https://img-blog.csdnimg.cn/img_convert/3c627d65bcff0639625a818ca5e0b612.png)
![04b6c5bbf87d4f5639bf3d7d53d8afbf.png](https://img-blog.csdnimg.cn/img_convert/04b6c5bbf87d4f5639bf3d7d53d8afbf.png)
3、计算机编程语言
我们看看机器语言,怎么表示存放一个数的指令,例如下图
![c79781a324ad692781da77b0fef33971.png](https://img-blog.csdnimg.cn/img_convert/c79781a324ad692781da77b0fef33971.png)
我们来看二进制代码 0000,0000,000000010000
其中第一个0000,表示的是汇编语言里的LOAD,也就是加载,加载什么呢
加载地址000000010000上的数据到第二个0000(寄存器的位置)。
接下来,我们看看如果是汇编语言怎么表示
![39c5cd86dacce30b0bbbee3f4de88c38.png](https://img-blog.csdnimg.cn/img_convert/39c5cd86dacce30b0bbbee3f4de88c38.png)
LOAD A, 16意思是将存储体内的16号单元数据,放到寄存器地址A中 ADD C, A, B意思是将寄存器里的A,B数据相加,得到C 。STORE C, 17意思是将寄存器里的数据存到存储体17号单元内
最后,我们看看怎么用高级语言表示
![11404f8a60ea9c6b41a94f8a430b85a6.png](https://img-blog.csdnimg.cn/img_convert/11404f8a60ea9c6b41a94f8a430b85a6.png)
高级语言是不是很简单,就一个a+b,你都不用去考虑寄存器,存储体这些事。
这部分的总结
高级语言一般有两种方式转换为机器语言
一种是直接借助编译器,将高级语言转换为二进制代码,比如c,这样c运行起来就特别快,因为编译后是机器语言,直接就能在系统上跑,但问题是,编译的速度可能会比较慢。
一种是解释性的,比如 js,是将代码翻译一行成机器语言(中间可能会先翻译为汇编代码或者字节码),解释一行,执行一行
需要注意的是,按照第一种将大量的高级代码翻译为机器语言,这其中就有很大的空间给编译器做代码优化,解释性语言就很难做这种优化,但是在v8引擎中,js还是要被优化的,在编译阶段(代码分编译和执行两个阶段)会对代码做一些优化,编译后立即执行的方式通常被称为 JIT (Just In Time) Comipler。
4、进制转换
接下来4.3这个小节会解释为什么0.1 + 0.2 等于0.3
4.1 二进制如何转化为十进制
例如2进制101.1如何转化为10进制。(有些同学觉得可以用parseInt('101.1', 2),这个是不行的,因为parseInt返回整数)
转化方法如下:
![19632cdd64f8db94869786528a6c2ab7.png](https://img-blog.csdnimg.cn/img_convert/19632cdd64f8db94869786528a6c2ab7.png)
上图的规则是什么呢?
二进制的每个数去乘以2的相应次方,注意小数点后是乘以它的负相应次方。再举一个例子你就明白了,二进制1101转为十进制
![21ce832a98bc20957df0a825dbec1936.png](https://img-blog.csdnimg.cn/img_convert/21ce832a98bc20957df0a825dbec1936.png)
4.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
4.3 十进制小数转为二进制
方式是采用“乘2取整,顺序排列”法。具体做法是:
用2乘十进制小数,可以得到积,将积的整数部分取出-
再用2乘余下的小数部分,又得到一个积,再将积的整数部分取出-
如此进行,直到积中的小数部分为零,或者达到所要求的精度为止 我们具体举一个例子
如: 十进制 0.25 转为二进制
0.25 * 2 = 0.5 取出整数部分:0
0.5 * 2 = 1.0 取出整数部分1 即十进制0.25的二进制为 0.01 ( 第一次所得到为最高位,最后一次得到为最低位) 此时我们可以试试十进制0.1和0.2如何转为二进制
0.1(十进制) = 0.0001100110011001(二进制)
十进制数0.1转二进制计算过程:
0.1*2=0.2……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.2”接着计算。
0.2*2=0.4……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.4”接着计算。
0.4*2=0.8……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.8”接着计算。
0.8*2=1.6……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.6”接着计算。
0.6*2=1.2……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.2”接着计算。
0.2*2=0.4……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.4”接着计算。
0.4*2=0.8……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.8”接着计算。
0.8*2=1.6……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.6”接着计算。
0.6*2=1.2……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.2”接着计算。
0.2*2=0.4……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.4”接着计算。
0.4*2=0.8……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.2”接着计算。
0.8*2=1.6……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.2”接着计算。
……
……
所以,得到的整数依次是:“0”,“0”,“0”,“1”,“1”,“0”,“0”,“1”,“1”,“0”,“0”,“1”……。
由此,大家肯定能看出来,整数部分出现了无限循环。
接下来看0.2
0.2化二进制是
0.2*2=0.4,整数位为0
0.4*2=0.8,整数位为0
0.8*2=1.6,整数位为1,去掉整数位得0.6
0.6*2=1.2,整数位为1,去掉整数位得0.2
0.2*2=0.4,整数位为0
0.4*2=0.8.整数位为0
就这样推下去!小数*2整,一直下去就行
这个数整不断
0.0011001
所以0.1和0.2都无法完美转化为二进制,所以它们相加当然不是0.3了
5、定点数和浮点数
首先,什么是定点数呢?
5.1 定点数
![75f073578be1e78bcd15a838d0e8da38.png](https://img-blog.csdnimg.cn/img_convert/75f073578be1e78bcd15a838d0e8da38.png)
如上图,举例纯整数的二进制1011和-1011,如果是整数,符号位用0表示,如果是负数符号为用1表示
![b4101d4d7ec32da82d51dcaf6f94c60b.png](https://img-blog.csdnimg.cn/img_convert/b4101d4d7ec32da82d51dcaf6f94c60b.png)
同理,纯小数表示举例如下:
![e95af4afc4c86e3f7212bb2abcee1730.png](https://img-blog.csdnimg.cn/img_convert/e95af4afc4c86e3f7212bb2abcee1730.png)
那如果不是纯小数或者纯整数,该怎么表示呢?
比如10.1, 可以乘以一个比例因子,将10.1 ---> 101 比例因子是10, 或者10.1 ---> 0.101比例因子是100
定点数很简单,接下来我们介绍浮点数,再JS里面,数字都是用双精度的浮点数,所以学习浮点数对我们理解JS的数字有帮助。
5.2 浮点数
浮点数怎么表示呢?
![7464d3367139551de5303e91eec9264b.png](https://img-blog.csdnimg.cn/img_convert/7464d3367139551de5303e91eec9264b.png)
上面是十进制的科学计数法,从中我们需要了解几个概念,一个是尾数,基数和阶码
尾数必须是纯小数,所以上图中1.2345不满足尾数的格式,需要改成0.12345
基数,在二进制里面是2
阶码就是多少次方 所以浮点数的通用表示格式如下:
![4dc868e9bc00aa48b7d1efed02d4f54c.png](https://img-blog.csdnimg.cn/img_convert/4dc868e9bc00aa48b7d1efed02d4f54c.png)
S代表尾数
r代表基数
j代表阶码
这里需要注意的是,浮点数的加减运算,并不是像我们上面介绍的那样简单,会经过以下几个步骤完成
![da73757e782c8b7eb02bf293c9bfc4e2.png](https://img-blog.csdnimg.cn/img_convert/da73757e782c8b7eb02bf293c9bfc4e2.png)
这些名词大家感兴趣的话,可以去网上查询,我们只要了解到浮点数加减运算很麻烦就行了,但如果你要做一个浮点数运算的库,你肯定是要完全掌握的。
6、局部性原理和catche(缓存)
先看下图
![1aae1be76dc20f5d704599ec9e67f9f2.png](https://img-blog.csdnimg.cn/img_convert/1aae1be76dc20f5d704599ec9e67f9f2.png)
(说明一下,MDR和MAR虽然逻辑上属于主存,但是在电路实现的时候,MDR和MAR离CPU比较近) 上图是在执行一串代码,可以理解为js的for循环
const n = 1000;
const a = [1, 2, 3, 4, 5, 6, 7]
for(let i =0; i < n; i++) {
a[i] = a[i] + 2
}
我们可以发现
数组的数据有时候在内存是连续存储的
如果我们要取数据,比如从内存取出a[0]的数据需要1000ns(ns是纳秒的意思),那么取出a[0]到a[7]就需要1000 * 8 = 8000 ns
如果我们cpu发现这是取数组数据,那么我就把就近的数据块a[0]到a[7]全部存到缓存上多好,这样只需要取一次数据,消耗1000ns
cahce就是局部性原理的一个应用
![6acf899b0bbbae266bd9fe28b39e8882.png](https://img-blog.csdnimg.cn/img_convert/6acf899b0bbbae266bd9fe28b39e8882.png)
空间局部性:在最近的未来要用到的信息(指令和数据),很可能与现在正在使用的信息在存储空间上是邻近的
时间局部性:在最近的未来要用到的信息,很可能是现在正在使用的信息
![9dbde1fb178f3b909738e2ab4a8a7007.png](https://img-blog.csdnimg.cn/img_convert/9dbde1fb178f3b909738e2ab4a8a7007.png)
可以看到cache一次性取了a[0]到a[9]存储体上的数据,只需要1000ns,因为Cache是高速存储器,跟cpu交互速度就比cpu跟主存交互速度快很多。
接下里,进入最后一节(略过对总线知识的学习),I/O设备的演变
7、I/O设备的演变
I/O是什么呢?
输入/输出(Input /Output ,简称I/O),指的是一切操作、程序或设备与计算机之间发生的数据传输过程。
比如文件读写操作,就是典型的I/O操作。接下来我们看一下I/O设备的演进过程
![6dd69bff36ae04dbfa48befc4a4fc3d3.png](https://img-blog.csdnimg.cn/img_convert/6dd69bff36ae04dbfa48befc4a4fc3d3.png)
在早期的计算机里,cpu如何知道I/O设备已经完成任务呢?比如说怎么知道I/O设备已经读取完一个文件的数据呢?CPU会不断查询I/O设备是否已经准备好。这时,cpu就处于等待状态。也就是cpu工作的时候,I/O系统是不工作的,I/O系统工作,cpu是不工作。接着看第二阶段
![1a804ec2b3a9133d092d8f44bf35e33c.png](https://img-blog.csdnimg.cn/img_convert/1a804ec2b3a9133d092d8f44bf35e33c.png)
为了解决第一阶段CPU要等待I/O设备,串行的工作方式,所有I/O设备通过I/O总线来跟CPU打交道,一旦某个I/O设备完成任务,就会以中断请求的方式,通过I/O总线,告诉CPU,我已经准备好了。
但是对于高速外设,它们完成任务的速度很快,所以会频繁中断CPU, 为了解决这个问题,高速外设跟主存之间用一条直接数据通路,DMA总线连接,CPU只需要安排开始高速外设做什么,剩下的就不用管了,这样就可以防止频繁中断CPU。最后来看一下第三阶段
![dc648785740886aacc53c296aea0993c.png](https://img-blog.csdnimg.cn/img_convert/dc648785740886aacc53c296aea0993c.png)
第三阶段,CPU通过通道控制部件来管理I/O设备,CPU不需要帮它安排任务,只需要简单的发出启动和停止类似的命令,通道部件就会自动的安排相应的I/O设备工作
本文完结,希望大家点个赞,比心💗。
源码
最后, 送人玫瑰,手留余香,觉得有收获的朋友可以点赞,关注一波 ,我们组建了高级前端交流群,如果您热爱技术,想一起讨论技术,交流进步,不管是面试题,工作中的问题,难点热点都可以在交流群交流,为了拿到大Offer,邀请您进群,入群就送前端精选100本电子书以及 阿里面试前端精选资料 添加 下方小助手二维码或者扫描二维码 就可以进群。让我们一起学习进步.
推荐阅读
(点击标题可跳转阅读)
[面试必问]-你不知道的 React Hooks 那些糟心事
[面试必问]-全网最全 React16.0-16.8 特性总结
[架构分享]- 微前端qiankun+docker+nginx自动化部署
[架构分享]-石墨文档 Websocket 百万长连接技术实践
觉得本文对你有帮助?请分享给更多人
关注「React中文社区」加星标,每天进步
点个赞👍🏻,顺便点个 在看 支持下我吧