前端開發人員中,有相當大比例的同學不是科班出來的,所以對於基本的科班必修課
,例如:計算機組成原理
、作業系統
、計算機網路
、資料結構和演算法
等知識接觸不多。
當你越深入學習,越會發現這些知識的重要性。
比如大家都知道js裡面0.1 + 0.2
是不等於0.3
的,為什麼呢?這就牽扯到計算機組成原理中浮點數
的表示方法,以及浮點數
的加減運算(正文會有白話版解答)。
又例如從鍵盤輸入a+b
這個指令,如何通過cpu
的排程輸出到螢幕上
呢?這就涉及到馮諾依曼體系
,如果你是程式設計人員,都不清楚資料從鍵盤到螢幕的基本流向
,那是時候看看這篇'十全大補文'了
本文是一篇計算機組成原理
最基本的入門文章,我覺得前端沒有必要那麼深入這個專題,掌握基本的計算機組成原理
的常識即可。
1、計算機的工作原理
首先,計算機最基本的5大組成部分如下圖,分別為:輸入裝置
(比如鍵盤), 儲存器
(比如記憶體), 運算器
(cpu), 控制器
(cpu), 輸出裝置
(顯示器)。
工作原理如下
1.1 控制器 ---> 控制輸入裝置 ----> 指令流向記憶體
當我們輸入資料的時候,cpu裡的控制器
會讓輸入裝置
把這些指令儲存到儲存器
(記憶體)上。
1.2 控制器分析指令 ---> 控制儲存器 ---> 把資料送到運算器
控制器分析指令之後, 此時讓儲存器
把資料傳送到運算器
裡(控制器
和運算器
都在cpu
裡面)
這裡需要注意,儲存器
既能儲存資料
,還能儲存指令
1.3 控制器控制運算器做資料的運算 並且將運算結果返回儲存器
1.4 控制器控制儲存器將結果返回給輸出裝置
從接下來,我們更近一步,看看計算機內部,CPU是怎麼跟儲存器互動的。
2、CPU及其工作過程
CPU中比較重要的兩個部件是運算器
和控制器
,我們先來看看運算器的主要作用
2.1 運算器主要部件
如上圖,運算器裡最重要的部件是ALU
,中文叫算術邏輯單元
,用來進行算術
和邏輯運算
的。其它的MQ
,ACC
這些我們不用管了,是一些暫存器
。
2.2 控制器主要部件
控制器中最重要的部件是CU
(控制單元),只要是分析指令
,給出控制訊號
。
IR
(指令暫存器),存放當前需要執行的指令
PC
存放的指令的地址。
2.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
- 如下圖
3、計算機程式語言
我們看看機器語言,怎麼表示存放一個數的指令,例如下圖
我們來看二進位制程式碼0000,0000,000000010000
- 其中第一個
0000
,表示的是組合語言裡的LOAD
,也就是載入,載入什麼呢 - 載入地址
000000010000
上的資料到第二個0000
(暫存器的位置)。
接下來,我們看看如果是組合語言
怎麼表示
LOAD A, 16
意思是將儲存體內的16號單後設資料,放到暫存器地址A中
ADD C, A, B
意思是將暫存器裡的A,B資料相加,得到C
STORE C, 17
意思是將暫存器裡的資料存到儲存體17號單元內
最後,我們看看怎麼用高階語言
表示
高階語言是不是很簡單,就一個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
返回整數)
轉化方法如下:
上圖的規則是什麼呢?
二進位制
的每個數去乘以2
的相應次方,注意小數點後是乘以它的負相應次方
。
再舉一個例子你就明白了,
二進位制1101
轉為十進位制
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是二進位制數的最高位的數碼
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 定點數
如上圖,舉例純整數的二進位制1011
和-1011
,如果是整數
,符號位用0
表示,如果是負數
符號為用1
表示
同理,純小數表示舉例如下:
那如果不是純小數
或者純整數
,該怎麼表示呢?
比如10.1
, 可以乘以一個比例因子,將10.1 ---> 101
比例因子是10
, 或者10.1 ---> 0.101
比例因子是100
定點數很簡單,接下來我們介紹浮點數,再JS裡面,數字都是用雙精度的浮點數
,所以學習浮點數對我們理解JS的數字有幫助。
5.2 浮點數
浮點數怎麼表示呢?
上面是十進位制
的科學計數法,從中我們需要了解幾個概念,一個是尾數
,基數
和階碼
尾數
必須是純小數,所以上圖中1.2345
不滿足尾數的格式,需要改成0.12345
基數
,在二進位制裡面是2
階碼
就是多少次方
所以浮點數
的通用
表示格式如下:
- S代表尾數
- r代表基數
- j代表階碼
這裡需要注意的是,浮點數的加減運算,並不是像我們上面介紹的那樣簡單,會經過以下幾個步驟完成
這些名詞大家感興趣的話,可以去網上查詢,我們只要瞭解到浮點數加減運算
很麻煩就行了,但如果你要做一個浮點數運算的庫,你肯定是要完全掌握的。
6、區域性性原理和catche(快取)
先看下圖
(說明一下,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
就是區域性性原理
的一個應用
空間區域性性
:在最近的未來要用到的資訊(指令
和資料
),很可能與現在正在使用的資訊在儲存空間
上是鄰近的時間區域性性
:在最近的未來要用到的資訊,很可能是現在正在使用的資訊
可以看到cache
一次性取了a[0]
到a[9]
儲存體上的資料,只需要1000ns
,因為Cache
是高速儲存器
,跟cpu
互動速度就比cpu
跟主存
互動速度快很多。
接下里,進入最後一節(略過對匯流排知識的學習),I/O裝置的演變
7、I/O裝置的演變
I/O是什麼呢?
輸入/輸出(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裝置工作
本文完結,希望大家點個贊,比心?。