本文爲極客時間徐文浩老師 - 深刻淺出計算機組成原理學習筆記html
現代計算機的基本組成部分其實主要由三部分組成:CPU,內存,主板。java
你撰寫的程序,打開的任何PC端應用。都要加載到內存中才能運行,存放在內存中的程序及其數據須要被CPU讀取,CPU計算完以後還要把對應的數據寫回到內存。主板的做用就是承載兩者,由於他們不能互相嵌入到對方中。程序員
CPU讀取內存中的二進制指令,而後譯碼,經過控制信號操做對應的運算原件以及存儲單元進行操做。算法
英特爾創始人之一 戈登丶摩爾 曾說:當價格不變時,集成電路上可容納的元器件的數目每隔18-24個月便會增長一倍,性能也將提高一倍。編程
咱們如今所使用的機器既叫圖靈機也叫馮諾依曼機,二者是不一樣的計算機抽象。馮諾依曼側重於硬件抽象,圖靈機則側重於計算抽象。數組
起源於馮諾依曼發表的一片文章即「第一份草案」,文中描述了他心目中的一臺計算機應該長什麼樣。進而確立了當代計算機的體系結構,即:運算器,控制器,存儲器,輸入設備,輸出設備五部分。緩存
他認爲現代計算機應該是一個「可編程」的計算機,是一個「可存儲」的計算機。即也叫存儲程序計算機。由於過去的計算機電路是焊死在電路板的,如同如今的計算器,若是要作其餘的操做,那麼就須要從新焊接電路,它是不可編程的計算機。再後來的計算機是「插拔式」的計算機,須要用什麼程序必須得插入該程序的組件纔可。這樣程序沒法保存在計算機上,每次使用的時候還須要插拔的方式才能夠。它是不可存儲的計算機。bash
圖靈是一位數學家,他並無考慮計算機的硬件基礎,而是隻考慮了計算機在數學計算模型上的可行性。即只思考了做爲一個「計算機」,他應該作的工做,和怎麼工做。圖靈只思考了計算機的計算模型,及計算機所謂的「計算」的理論邏輯的實現方法。服務器
圖靈機能夠看作由一條兩端可無限延長的帶子,它有一個讀寫頭,以及一組控制讀寫頭工做的命令組成,是一種抽象的計算模型,即將人們使用紙筆進行的數學運算由一個虛擬的機器代替。多線程
它證實了通用計算理論,確定了計算機實現的可能性。同時它給出了計算機應有的主要架構,引入了讀寫,算法,程序語言的概念。極大的突破了過去的計算機的設計理念。
其實圖靈機本質上是狀態機,計算機理論模型,馮諾依曼體系則更像是圖靈機的具體物理實現。包括運算,控制,存儲,輸入,輸出五個部分。馮諾依曼體系相對以前的計算機最大的創新在於程序和數據的存儲。今後實現機器的內部編程。圖靈機的紙帶應對馮諾依曼體系中的存儲,讀寫頭對應輸入輸出規則及(讀取一個符號後,作了什麼)運算,紙帶怎麼移動則對應着控制。
理論上圖靈機能夠模擬人類的全部計算過程,不管複雜與否。
芯片組
芯片組是主板的核心組成部分,聯繫CPU及其餘周邊設備的運做。主板上最重要的芯片組就是南北橋芯片組。
南北橋芯片組->主板上的兩個主要芯片組,靠上方的叫北橋,下方的叫南橋。北橋負責與CPU通訊,而且連接告訴設備(內存,顯卡),而且與(I/O操做)南橋通訊,南橋負責與低俗設備(硬盤/外部IO設備,USB等設備)通訊,而且與北橋通訊。
總線
主板的芯片組和總線解決了CPU和內存通訊的問題(北橋),芯片組控制數據傳輸的流轉(從哪來,到哪兒去),總線則是實際數據傳輸的高速公路。
CPU的好壞決定 -> 主頻高,緩存大,核心數多。CPU通常安裝在主板的CPU插槽中。
數據通路:其實就是鏈接了整個運算器與控制器,方便咱們程序的運轉和計算,並最終組成了CPU。
CPU通常被叫作超大規模集成電路,由一個個晶體管組合而成,CPU的計算過程其實就是讓晶體管中的「開關」信號不斷的去「打開」和「關閉」。來組合完成各類運算和功能。這裏的「打開」及「關閉」操做的快慢就是由CPU主頻來影響。
控制器
一條條指令執行的控制過程,就是由計算機五大組件之一的控制器來控制的。
CPU即中央處理器,GPU即圖形處理器,如今的電腦,大部分GPU都集成在了CPU中也叫集成顯卡,後來本來的GPU即屬於北橋的內存控制器等做爲一支獨立的芯片封裝到了CPU基板上。因此後來的及其的主板上沒有南北橋之分了,只剩下了PCH芯片即過去的南橋。
固然若是你的PC機要運行一些大型遊戲,或者有一些對GPU要求較高的工做的話,也能夠配置獨立的GPU卡到主板上。
過去:
要看一臺PC機的具體CPU核數以及線程數能夠經過任務管理器界面看到,也能夠經過計算機右鍵屬性的設備管理器中看到(僅能看到線程數)。或者經過以下命令看到
wmic
cpu get *
-----------對應屬性
NumberOfCores
NumberLogicProcessors
複製代碼
CPU 的主頻即內核工做的時鐘頻率,一般所說的***CPU是多少兆赫的,這裏所謂的兆赫就是描述的CPU主頻,CPU型號後面跟着的2.4 GHZ
即主頻的數字描述。
主頻並不直接表明CPU的運算速度,因此也會有CPU主頻高可是CPU的運算速度慢的狀況,主頻僅是CPU性能表現的一方面。
Java 中的全部線程均在JVM進程中,CPU調度的是進程中的線程。
CPU線程數和Java線程數並無直接關係,CPU採用分片機制執行線程,給每一個線程劃分很小的時間顆粒去執行,可是真正的項目中,一個程序要作不少的的操做,讀寫磁盤、數據邏輯處理、出於業務需求必要的休眠等等操做,當程序正在執行的線程進入到I/O操做的時候,線程隨之進入阻塞狀態,此時CPU會作上下文切換,以便處理其餘線程的任務;當I/O操做完成後,CPU會收到一個來自硬盤的中斷信號,並進入中斷處理例程,手頭正在執行的線程則可能所以被打斷,回到 ready 隊列。而先前因 I/O 而阻塞等待的線程隨着 I/O 的完成也再次回到 就緒隊列,這時 CPU 在進行線程調度的時候則可能會選擇它來執行。
參考:
線程是操做系統最小的調度單位,進程則是操做系統資源分配的對小單位。
進程:進程是操做系統分配資源的基本單位,每隔進程擁有虛擬後的獨立的內存空間,存儲空間,CPU資源。各類PC端應用均是一個獨立的進程。 線程:是CPU調度的基本單位,贊成進程的各個線程共享進程內部的資源,線程間的通信遠小於進程間的。由於(各個線程共享進程內部的資源)。因此在多線程併發的狀況下,須要額外關注對於共享資源的保護問題,尤爲是全局變量。
Intel的超線程技術,目的是爲了更充分地利用一個單核CPU的資源。CPU在執行一條機器指令時,並不會徹底地利用全部的CPU資源,並且實際上,是有大量資源被閒置着的。 超線程技術容許兩個線程同時不衝突地使用CPU中的資源。好比一條整數運算指令只會用到整數運算單元,此時浮點運算單元就空閒了,若使用了超線程技術,且另外一個線程恰好此時要執行一個浮點運算指令,CPU就容許屬於兩個不一樣線程的整數運算指令和浮點運算指令同時執行,這是真的並行。 我不瞭解其它的硬件多線程技術是怎麼樣的,但單就超線程技術而言,它是能夠實現真正的並行的。但這也並不意味着兩個線程在同一個CPU中一直均可以並行執行,只是剛好碰到兩個線程當前要執行的指令不使用相同的CPU資源時才能夠真正地並行執行。
本質上是一個物理核在跑一個線城時,同時利用閒置的晶體管跑其餘指令,這樣就能夠提高效能。
參考:
計算機的兩個核心指標:性能,功耗。具體的體現則是響應時間和吞吐率。響應時間即單位任務執行運算的快慢,吞吐量即單位時間處理任務的多少。
程序運行時間: 程序在用戶態運行指令的時間+內核態運行指令的時間。
但受線程調度的影響,CPU在同一時間會有不少的Task在執行,不是隻執行特定程序的指令,而且同一臺計算機可能CPU滿載執行,也能會降頻執行。而且程序運行時間也會受到相應的主板和內存的影響。
程序的CPU執行時間 = CPU時鐘週期數 * 時鐘週期時間 - 能夠當作處理每一個Task所需時間。
好比Intel Core - i7 - 7700HQ 2.8GHZ
,這裏的2.8GHZ
粗淺理解即CPU在一秒裏能夠執行的簡單指令數是2.8G條。準確說即CPU的一個「鐘錶」可以識別出來的最小時間間隔。
**時鐘週期時間:**在CPU內部,和咱們戴的電子石英錶相似,有一個叫晶體振盪器的東西簡稱「晶振」,晶振的每一次「滴答」即電子石英錶的時鐘週期時間(晶振時間)。在2.8GHZ
主頻的CPU上,這個時鐘週期時間就是1/2.8GHZ
。CPU就是按照這個「時鐘」提示的時間來進行本身的操做,主頻越高意味着這個表走的越快,CPU也就「被逼」着走的也快,CPU越快散熱壓力固然也越大。
這裏能夠得出,晶振時間與CPU執行固定指令耗時成正比,越小耗時越少。
CPU時鐘週期數 = 指令數 * 每條指令的平均時鐘週期數(CPI)。 - 能夠當作共有多少個Task。
這裏說了每條指令的平均時鐘週期數,因此咱們就知道不一樣的指令執行時間是不一樣的,即所花費的時鐘週期數是不一樣的,可能別人的Task簡單花1秒鐘就能作完,你的Task比較複雜須要5秒才行。具體到計算機,乘法的時鐘週期數就要多於加法。不過現代的CPU經過流水線技術可使得單個命令的執行須要的CPU時鐘週期數更少了。
一個程序包含多條語句,一條語句可能對應多條指令,一條CPU指令可能須要多個CPU時鐘週期才能完成。
程序的CPU執行時間: 指令數 * 每條指令的平均時鐘週期數(CPI) * 時鐘週期時間
由上面的公式咱們知道,若是想要減小程序的CPU執行時間的話那麼就要從以上三點着手。可是指令數是由不一樣編譯器所決定的,時鐘週期時間則是由CPU主頻的高低來決定的,而每條指令的平均時鐘週期數咱們則能夠經過流水線技術來優化。
CPU功耗 ~= 1/2 * 負載電容 * 電壓的平方 * 開關平率 * 晶體管數量
製程:
納米制程,以14nm爲例,其製程是指在芯片中,線最小能夠作到14納米的尺寸,縮小晶體管能夠減小耗電量(晶體管必定的單位面積中),同時能夠提高信號量在電路間的傳輸速度,縮小製程後,晶體管之間的電容也會更低,從而提高他們之間的開關頻率。可知功耗與電容成正比,因此傳輸速度更快,還更省電。
阿姆達爾定律:
並行優化,並非全部的問題均可以經過並行去優化。
優化後的執行時間 = 受優化影響的執行時間/加速倍數(並行處理數) + 不受影響的執行時間
彙編器是一種工具程序,用於將彙編語言源程序轉換爲機器語言。機器語言是一種數字語言, 專門設計成能被計算機處理器(CPU)理解。全部 x86 處理器都理解共同的機器語言。
彙編語言包含用短助記符如 ADD、MOV、SUB 和 CALL 書寫的語句。彙編語言與機器語言是一對一的關係:每一條彙編語言指令對應一條機器語言指令。 這就意味着不一樣型號的處理器若是所使用的機器語言不一樣的話,那麼他們的彙編語言也毫不相同。
高級語言如 Python、C++ 和 Java 與彙編語言和機器語言的關係是一對多。好比,C++的一條語句就會擴展爲多條彙編指令或機器指令。 一種語言,若是它的源程序可以在各類各樣的計算機系統中進行編譯和運行,那麼這種語言被稱爲是可移植的。
彙編語言不是可移植的,由於它是爲特定處理器系列設計的。目前普遍使用的有多種不一樣的彙編語言,每一種都基於一個處理器系列。 對於一些廣爲人知的處理器系列如 Motorola 68×00、x8六、SUN Sparc、Vax 和 IBM-370,彙編語言指令會直接與該計算機體系結構相匹配,或者在執行時用一種被稱爲微代碼解釋器的處理器內置程序來進行轉換。
要讓一段C語言程序在一個 Linux 操做系統上跑起來,咱們須要把整個程序翻譯成一個彙編語言的程序,這個過程咱們通常叫編譯成彙編代碼。針對彙編代碼,咱們能夠再用匯編器翻譯成 機器碼。這些機器碼由「0」和「1組成的機器語言表示。這一條條機器碼,就是一條條的計算機指令。這樣一串串的 16 進制數字,就是咱們 CPU 可以真正認識的計算機指令。爲了讀起來方便,咱們通常把對應的二進制數,用 16 進製表示
解釋型語言,是經過解釋器在程序運行的時候逐句翻譯,而 Java 這樣使用虛擬機的語言,則是由虛擬機對編譯出來的中間代碼進行解釋,或者即時編譯(JIT)成爲機器碼來最終執行。
咱們平常用的 Intel CPU,有 2000 條左右的 CPU 指令。常見的指令能夠分紅五大類:
來看一段彙編代碼:
#include <time.h>
#include <stdlib.h>
int main()
{
srand(time(NULL));
int r = rand() % 2;
int a = 10;
if (r == 0)
{
a = 1;
} else {
a = 2;
}
==================
if (r == 0)
3b: 83 7d fc 00 cmp DWORD PTR [rbp-0x4],0x0
3f: 75 09 jne 4a <main+0x4a>
{
a = 1;
41: c7 45 f8 01 00 00 00 mov DWORD PTR [rbp-0x8],0x1
48: eb 07 jmp 51 <main+0x51>
}
else
{
a = 2;
4a: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
51: b8 00 00 00 00 mov eax,0x0
}
複製代碼
該段代碼的具體釋義可參考深刻淺出計算機組成原理第六節。
機器語言,彙編語言,編譯器:
過去編寫程序是經過紙帶打孔的方式,那麼就只能經過「0,1」機器碼來進行程序的編寫,後來進不出了彙編語言,彙編語言是一種更接近人類語言的語言,用匯編器能夠將彙編語言轉爲機器語言。彙編器則至關於翻譯機的存在,能夠根據具體的彙編指令轉爲計算機能識別的二進制碼,即各CPU開發商提供的機器碼。
咱們知道當下所流行的各類彙編語言都是與處理器所一對一的,即當初彙編語言的設計人員在編寫彙編語言的時候是經過CPU開發商提供的指令集手冊來對應開發定義的彙編語言,而各類型號不一樣的CPU所獨有的指令集則是燒錄到了CPU中。
參考:
能夠先讀:
基本概念:
寄存器 寄存器是中央處理器內的組成部分。寄存器是有限存貯容量的高速存貯部件,它們可用來暫存指令、數據和地址。在中央處理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序計數器(PC)。
各種存儲器按照到cpu距離由近到遠(訪存速度由高到低)排列分別是寄存器,緩存,主存,輔存。
其餘優質解答:
邏輯上,咱們能夠認爲,CPU 其實就是由一堆寄存器組成的。而寄存器就是 CPU 內部,由多個觸發器或者鎖存器組成的簡單電路。
N 個觸發器或者鎖存器,就能夠組成一個 N 位(Bit)的寄存器,可以保存 N 位的數據。比方說,咱們用的 64 位 Intel 服務器,寄存器就是 64 位的。
一個 CPU 裏面會有不少種不一樣功能的寄存器。其中有三個比較特殊的
CPU 裏面還有更多用來存儲數據和內存地址的寄存器。這樣的寄存器一般一類裏面不止一個。咱們一般根據存放的數據內容來給它們取名字,好比整數寄存器、浮點數寄存器、向量寄存器和地址寄存器等等。有些寄存器既能夠存放數據,又能存放地址,咱們就叫它通用寄存器。
一個程序執行的時候,CPU 會根據 PC 寄存器裏的地址,從內存裏面把須要執行的指令讀取到指令寄存器裏面執行,而後根據指令長度自增,開始順序讀取下一條指令。一個程序的一條條指令,在內存裏面是連續保存的,也會一條條順序加載。
而有些特殊指令,好比 J 類指令,也就是跳轉指令,會修改 PC 寄存器裏面的地址值。這樣,下一條要執行的指令就不是從內存裏面順序加載的了。事實上,這些跳轉指令的存在,也是咱們能夠在寫程序的時候,使用 if…else
條件語句和 while/for
循環語句的緣由。
CPU從PC寄存器中取地址,找到地址對應的內存位子,取出其中指令送入指令寄存器執行,而後指令自增,重複操做。因此只要程序在內存中是連續存儲的,就會順序執行這也是馮諾依曼體系的理念。而實際上跳轉指令就是當前指令修改了當前PC寄存器中所保存的下一條指令的地址,從而實現了跳轉。固然各個寄存器其實是由數電中的一個一個門電路組合出來的。
參考:
計算機中的數在內存中都是以二進制形式進行存儲的,用位運算就是直接對整數在內存中的二進制位進行操做,所以其執行效率很是高,在程序中儘可能使用位運算進行操做,這會大大提升程序的性能。固然可讀性纔是首要保證的目標。
位操做符
&
與運算 兩個位都是 1 時,結果才爲 1,不然爲 01 0 0 1 1
&
1 1 0 0 1
------------------------------
1 0 0 0 1
複製代碼
|
或運算 兩個位都是 0 時,結果才爲 0,不然爲 11 0 0 1 1
|
1 1 0 0 1
------------------------------
1 1 0 1 1
複製代碼
^
異或運算,兩個位相同則爲 0,不一樣則爲 11 0 0 1 1
^
1 1 0 0 1
-----------------------------
0 1 0 1 0
複製代碼
~
取反運算,0 則變爲 1,1 則變爲 0~ 1 0 0 1 1
-----------------------------
0 1 1 0 0
複製代碼
<<
左移運算,向左進行移位操做,高位丟棄,低位補 0int a = 8;
a << 3;
移位前:0000 0000 0000 0000 0000 0000 0000 1000
移位後:0000 0000 0000 0000 0000 0000 0100 0000
複製代碼
>>
右移運算,向右進行移位操做,對無符號數,高位補 0,對於有符號數,高位補符號位unsigned int a = 8;
a >> 3;
移位前:0000 0000 0000 0000 0000 0000 0000 1000
移位後:0000 0000 0000 0000 0000 0000 0000 0001
int a = -8;
a >> 3;
移位前:1111 1111 1111 1111 1111 1111 1111 1000
移位前:1111 1111 1111 1111 1111 1111 1111 1111
複製代碼
參考自:
在真實的程序裏,壓棧的不僅有函數調用完成後的返回地址。好比函數 A 在調用 B 的時候,須要傳輸一些參數數據,這些參數數據在寄存器不夠用的時候也會被壓入棧中。整個函數 A 所佔用的全部內存空間,就是函數 A 的棧幀。
實際的程序棧佈局,頂和底與咱們的乒乓球桶相比是倒過來的。底在最上面,頂在最下面,這樣的佈局是由於棧底的內存地址是在一開始就固定的(內存地址偏大的那一邊)。而一層層壓棧以後,棧頂的內存地址是在逐漸變小而不是變大。(這裏理解這句話,須要明白棧是固定大小的,想象一個乒乓球筒,反向往裏面填球)
觸發的StackOverFlow常見觸發方式:函數的遞歸調用,在棧中聲明一個很是佔內存的變量(巨大數組)。
程序運行常見的優化方案:
把一個實際調用的函數產生的指令,直接插入到調用該函數的位置,來替換對應的函數調用指令。這種方案在若是被調用的函數中沒有調用其餘函數的狀況下,仍是可行的。這是一個常見的編譯器進行自動優化的場景,叫函數內聯。
這裏編譯器優化的具體痛點並不是簡單的少了一些指令的執行,而是函數頻繁進出棧所花費時間的開銷,由於相對於寄存器來講,內存是十分慢的。因此讓CPU反覆操做內存的話,開銷仍是很大的。因此上述文本着重提示了被調用函數沒有調用其餘函數的狀況下,由於若是有調用的話,一是寄存器內存可能開銷不夠,二是仍是有操做主存的瓶頸在。
C 語言的文件在編譯後會生成以.o
爲尾綴的彙編語言文件,如 add_lib.o
以及 link_example.o
並非一個可執行文件而是目標文件。只有經過連接器把多個目標文件以及調用的各類函數庫連接起來,咱們才能獲得一個可執行文件。
C 語言代碼 - 彙編代碼 - 機器碼 這個過程,在咱們的計算機上進行的時候是由兩部分組成的。
由上咱們能夠得知程序最終是經過裝載器加載程序及數據到內存而後變成指令和數據的,因此其實咱們生成的可執行代碼也並不只僅是一條條的指令。
可執行代碼和目標代碼長得差很少,可是長了不少。由於在Linux下,可執行文件和目標文件所使用的都是一種叫ELF
的文件格式,中文名字叫可執行與可連接文件格式,這裏面不只存放了編譯成的彙編指令,還保留了不少別的數據。
如函數名稱add
、main
等,以及定義的全局能夠訪問的變量名稱,都存放在ELF格式文件裏。這些名字和它們對應的地址,在 ELF 文件裏面,存儲在一個叫做符號表的位置裏。符號表至關於一個地址簿,把名字和地址關聯了起來。
執行流程: 連接器會掃描全部輸入的目標文件,而後把全部符號表裏的信息收集起來,構成一個全局的符號表。而後再根據重定位表,把全部不肯定要跳轉地址的代碼,根據符號表裏面存儲的地址,進行一次修正。最後,把全部的目標文件的對應段進行一次合併,變成了最終的可執行代碼。這也是爲何,可執行文件裏面的函數調用的地址都是正確的。
main 函數裏調用 add 的跳轉地址,再也不是下一條指令的地址了,而是 add 函數的入口地址了,這就是 EFL 格式和連接器的功勞。
因此一些文件即使是在同一計算機同一CPU上的不一樣操做系統上可能會出現一個可執行而一個不可執行的狀況。根本緣由在於不一樣OS的裝載器所對應的能解析的文件格式也是不一樣的。Linux的裝載器只能裝載EFL的文件格式,而Windows是PE的。
這裏Java實現跨平臺的機制則是:Java是經過實現不一樣平臺上的虛擬機,而後即時翻譯javac生成的中間代碼來作到跨平臺的。跨平臺的工做被虛擬機開發人員來解決了(如同彙編)。
解決辦法:
分段: 在內存中劃分一段連續的內存空間,分配給裝載的程序,把連續的內存空間和指令指向的內存地址進行映射。
其中指令裏用到的內存地址叫做虛擬內存地址,實際內存硬件裏面的物理空間叫作物理內存地址。程序員只須要關心虛擬內存地址就好了。因此咱們只須要維護虛擬內存到物理內存的映射關係的起始地址和對應的空間大小就能夠了。
問題:內存碎片
解決辦法:
內存交換。即先把內存中某個程序所佔用的內存寫到硬盤上,而後再從硬盤上讀回內存中,只不過讀回來的時候要緊貼上一個應用所佔用內存空間的後面,造成連續的內存佔用。
問題:性能瓶頸,內存碎片和內存交換的空間太大,硬盤的讀寫速度太慢
解決辦法:
內存分頁。原理是少出現一些內存碎片。另外,當須要進行內存交換的時候,讓須要交換寫入或者從磁盤裝載的數據更少一點。和分段這樣分配一整段連續的空間給到程序相比,分頁是把整個物理內存空間切成一段段固定尺寸的大小。 。而對應的程序所須要佔用的虛擬內存空間,也會一樣切成一段段固定尺寸的大小。 這樣一個連續而且尺寸固定的內存空間,就是頁。通常頁遠小於程序大小隻有幾KB。
因爲內存空間都是預先劃分好的,也就沒有了不能使用的碎片,而只有被釋放出來的不少 4KB 的頁。即便內存空間不夠,須要讓現有的、正在運行的其餘程序,經過內存交換釋放出一些內存的頁出來,一次性寫入磁盤的也只有少數的一個頁或者幾個頁,不會花太多時間,讓整個機器被內存交換的過程給卡住。
分頁的方式使得咱們在加載程序的時候,再也不須要一次性都把程序加載到物理內存中。咱們徹底能夠在進行虛擬內存和物理內存的頁之間的映射以後,並不真的把頁加載到物理內存裏,而是隻在程序運行中,須要用到對應虛擬內存頁裏面的指令和數據時,再加載到物理內存裏面去。
虛擬內存是指一段地址,可是沒有加載到物理內存裏的時候其實就是放在硬盤上。
虛擬內存,內存交換,內存分頁三者結合下,其實運行一個應用程序須要用的必要內存是不多的,也是爲何咱們優先的內存能夠運行比咱們內存大不少的應用的緣由。
JVM也是一個可執行程序,同其餘程序同樣依賴於操做系統的內存管理和裝載程序,它能夠按本身的方式去規劃它自身的內存空間給就Java程序使用而無需考慮怎麼映射到物理內存這些。這是承載他的操做系統須要作的事情,每一個應用程序都有固定使用的內存空間的限度。
上文提到在使用鏈接器進行代碼合併的時候,這裏的連接是指靜態連接,相應的,也有對應的動態連接。咱們知道程序在進行裝載的時候同一份代碼若是多個程序都靜態鏈接了一遍那麼內存中將會有多分一樣的代碼佔用內存,這對內存耗費也是很是大的。
既然是共享代碼,那麼內存中只要裝載一份便可。在程序連接的時候咱們連接到該共享庫的內存地址便可,不一樣系統下,共享庫的文件尾綴不一樣。Windows是.dll
,Linux下是.so
。
共享庫文件代碼要求:
編譯出來的共享庫文件的指令代碼,是地址無關的。 緣由是不一樣程序若是都用同一份共享代碼庫的話,不一樣程序該代碼的虛擬地址是不一樣的,雖然物理地址上是相同的,可是對於該共享代碼庫的虛擬地址和物理地址的映射就沒法維護了。
其中利用重定位表的代碼就是與地址相關的代碼。利用重定位表的代碼在程序連接的時候,就把函數調用後要跳轉訪問的地址肯定下來了,這意味着,若是這個函數加載到一個不一樣的內存地址,跳轉就會失敗。
相對地址: 動態代碼庫中的數據和指令的虛擬地址都是經過相對地址的方式互相訪問的。各類指令中使用到的內存地址,給出的不是一個絕對的地址空間,而是一個相對於當前指令偏移量的內存地址。由於整個共享庫是放在一段連續的虛擬內存地址中的,不管裝載到哪一段地址,不一樣指令之間的相對地址都是不變的。
須要注意的是:雖然共享庫的代碼部分的物理內存是共享的,可是數據部分是各個動態連接它的應用程序裏面各加載一份的。
全局偏移表(GOT): GOT表位於共享庫的數據段裏。因此使用動態連接的各個程序在共享庫中生成各自的GOT,每一個程序的GOT都不一樣。 而 GOT 表裏的數據,則是在加載一個個共享庫的時候寫進去的。因此若是當前運行程序的共享庫指令須要用到外部的變量和函數地址的話,都會查詢 GOT,來找到當前運行程序的虛擬內存地址。
不一樣的進程,調用一樣的共享庫,各自 GOT 裏面指向最終加載的動態連接庫裏面的虛擬內存地址是不一樣的(由於各應用程序調用該函數的虛擬內存地址是不一樣的)。
雖然不一樣的程序調用的一樣的動態庫,而各自的數據部分的內存地址是獨立的,調用的又都是同一個動態庫,可是不須要去修改動態庫裏面的代碼所使用的地址,而是各個程序各自維護好本身的 GOT,可以找到對應的動態庫就行了。
像動態連接這樣經過修改「地址數據」來進行間接跳轉,去調用一開始不能肯定位置代碼的思路,在Java中,相似多態的實現。
以下代碼:
public class DynamicCode {// 動態代碼庫
private HashMap<String, Object> data; // 各種私有的數據部分 - 其中有一項是GOT
public static void main(strs[] args) {// 公用代碼部分
}
}
複製代碼
二進制 -> 十進制: 把從右到左的第 N 位,乘上一個 2 的 N 次方,而後加起來。N 從 0 開始記位。
示例:
0011
=====
0×2^3 + 0×2^2 + 1×2^1 + 1×2^0
複製代碼
十進制 -> 二進制: 短除法。也就是,把十進制數除以 2 的餘數,做爲最右邊的一位。而後用商繼續除以 2,把對應的餘數緊靠着剛纔餘數的右側,這樣遞歸迭代,直到商爲 0 就能夠了。而後餘數序列從下到上組成的序列就是該整數的二進制表示。
首先須要明白:在計算機中,數字都是用補碼來存儲的,而對於補碼的表示方式一個字節(8bit)的數字,規定1000 0000就是-128。並且對於正數而言,反碼,補碼是其原碼自己。
原碼: 0001 在原碼中就表示爲 +1。而 1001 最左側的第一位是 1,因此它就表示 -1。這個其實就是整數的原碼錶示法。
原碼錶示法的問題
反碼: 爲了解決「正負相加不等於0」的問題,在「原碼」的基礎上,人們發明了「反碼」。「反碼」表示方式是用來處理負數的,符號位置不變,其他位置相反。
這樣正負兩數相加不爲0的狀況就解決了
反碼錶示法的問題:
補碼:一樣是針對"負數"作處理的,從原來"反碼"的基礎上 +1。在補一位1的時候,要丟掉最高位(好比1111)。
這樣就解決了+0和-0同時存在的問題,另外"正負數相加等於0"的問題,一樣獲得知足。同時還多了一位數 -8。
用原碼的話,一個字節能夠表示的範圍是:-127~127,用補碼的話表示的範圍是:-128~127.
二進制負數的補碼,等於該負數取反碼再加1,也等於其正數按位取反再加1。
正數的反碼是其自己 負數的反碼是在其原碼的基礎上, 符號位不變,其他各個位取反。
重點:
說了那麼多,只是描述一下三者的區別及由來。由於咱們從一開始就說了,計算機中是按補碼來存儲數據的,因此咱們只要想辦法快速搞清楚一個計算機中的二進制數的十進制是多少。
咱們仍然經過最左側第一位的0和1,來判斷這個數的正負。可是,咱們再也不把這一位當成單獨的符號位,在剩下幾位計算出的十進制前加上正負號,而是在計算整個二進制值的時候,在左側最高位前面加個負號。
好比,一個 4 位的二進制補碼數值 1011,轉換成十進制,就是 -1×2^3 + 0×2^2 + 1×2^1 + 1×2^0 = -5。若是最高位是 1,這個數必然是負數;最高位是 0,必然是正數。而且,只有 0000 表示 0,1000 在這樣的狀況下表示 -8。一個 4 位的二進制數,能夠表示從 -8 到 7 這 16 個數,不會浪費一位。
參考:
ASCII(American Standard Code for Interchange,美國信息交換標準代碼: 最先計算機只須要使用英文字符,加上數字和一些特殊符號,而後用8位的二進制,就能表示咱們平常須要的全部字符了,這個就是ASCII碼。
ASCII 碼就比如一個字典,用 8 位二進制中的 128 個不一樣的數,映射到 128 個不一樣的字符裏。好比,小寫字母 a 在 ASCII 裏面,就是第 97 個,也就是二進制的 0110 0001,對應的十六進制表示就是 61。而大寫字母 A,就是第 65 個,也就是二進制的 0100 0001,對應的十六進制表示就是 41。
須要注意的是:
在 ASCII 碼裏面,數字 9 再也不像整數表示法裏同樣,用 0000 1001 來表示,而是用 0011 1001 來表示。字符串 「15」 也不是用 0000 1111 這 8 位來表示,而是變成兩個字符 1 和 5 連續放在一塊兒,也就是 0011 0001 和 0011 0101,須要用兩個 8 位來表示。 兩個 8 位的緣由是,由於 4 位最高只能表示到(-8 - 7)。
咱們能夠看到,最大的 32 位整數,就是 2147483647。若是用整數表示法,只須要 32 位就能表示了。可是若是用字符串來表示,一共有 10 個字符,每一個字符用 8 位的話,須要整整 80 位。比起整數表示法,要多佔不少空間。因此這也是爲何咱們在存儲數據的時候要經過二進制序列化的方式來存儲。
Unicode: 其實就是一個字符集,包含了 150 種語言的 14 萬個不一樣的字符。
字符編碼則是對於字符集裏的這些字符,怎麼一一用二進制表示出來的一個字典。咱們上面說的 Unicode,就能夠用 UTF-八、UTF-16,乃至 UTF-32 來進行編碼,存儲成二進制。因此,有了 Unicode,其實咱們能夠用不止 UTF-8 一種編碼形式,只要別人知道這套編碼規則,就能夠正常傳輸、顯示這段代碼。
一樣的文本,採用不一樣的編碼存儲下來。若是另一個程序,用一種不一樣的編碼方式來進行解碼和展現,就會出現亂碼。
須要注意的是,若是咱們程序中使用了一些或者說存儲了一些不經常使用的古老字符集,那麼可能Unicode字符集中並不存在這樣的字符,那麼Unicode 會統一把這些字符記錄爲 U+FFFD 這個編碼。若是用 UTF-8 的格式存儲下來,就是\xef\xbf\xbf。
參考:
計算機不用十進制而用二進制緣由以下:
電磁關係及繼電器的由來可參考繼電器。
電信號在傳遞的時候,因爲電線過長會致使電阻過大此時對電壓要求會變大或者說用電器會出現無響應的狀態。因此在進行遠距離信息傳遞的時候爲了不電路過長這種狀況,發明了繼電器(電驛)。繼電器能夠方便咱們的電信號進行傳導,或者根據須要組成咱們想要的「與」,「或」,「非」等的邏輯電路。
「與」電路的話至關於咱們在電路上串聯兩個開關,當兩個開關都打開,電路才接通。「或」至關於咱們在輸入端通兩條電路到輸出端,任意一條電路是打開狀態,那麼到輸出端的電路都是聯通的。「非」至關於從開關默認關掉,只有通電有了磁場以後打開,換成默認是打開通電的,只有通電以後才關閉,咱們就獲得了一個計算機中的「非」操做。輸出端開和關正好和輸入端相反。
這三種基本邏輯電路實現起來都比較簡單,若是要作複雜的工做的話則須要更多的邏輯電路經過分層,組合的方式來實現。
結論:咱們經過電路的「開」和「關」,來表示「1」和「0」。就像晶體管在不一樣的狀況下,表現爲導電的「1」和絕緣的「0」的狀態。
這些基本的邏輯電路,也叫門電路。 一方面,咱們能夠經過繼電器或者中繼,進行長距離的信號傳輸。另外一方面,咱們也能夠經過設置不一樣的線路和開關狀態,實現更多不一樣的信號表示和處理方式,這些線路的鏈接方式其實就是咱們在數字電路中所說的門電路。而這些門電路,也是咱們建立 CPU 和內存的基本邏輯單元。咱們的各類對於計算機二進制的「0」和「1」的操做,其實就是來自於門電路,叫做組合邏輯電路。
所謂門電路在數字電路中,所謂「門」就是隻能實現基本邏輯關係的電路。最基本的邏輯關係是與,或,非,最基本的邏輯門是與門,或門和非門。以下是最基本的門電路,其餘複雜的門電路都是由這些門電路組合而成。他們是構成現代計算機硬件的「積木」。
半加器
能夠看到基礎門電路,輸入都是兩個單獨的 bit,輸出是一個單獨的 bit。若是咱們要對 2 個 8 位的數,計算與、或、非這樣的簡單邏輯運算,其實很容易。只要連續擺放 8 個開關,來表明一個 8 位數。這樣的兩組開關,從左到右,上下單個的位開關之間,都統一用「與門」或者「或門」連起來,就是兩個 8 位數的 AND 或者 OR 的運算了。
要想實現一個加法器,各二進制位的計算邏輯以下:
能夠看到每位的輸入輸出關係對應着基本門電路中的異或門的邏輯。因此,其實異或門就是一個最簡單的整數加法,所須要使用的基本門電路。 但須要注意的是,若是當兩個輸入位都是1的話,咱們還須要考慮進1位的狀況。因此這就用到了基礎門電路中的與門。
因此,經過一個異或門計算出個位,經過一個與門計算出是否進位,咱們就經過電路算出了一個一位數的加法。因而,後來就把這兩個門電路進行打包,叫他爲半加器。
全加器
半加器只能解決個位的運算,二,四,八位的輸入狀況與個位的並不同。由於二位除了一個加數和被加數以外,還須要加上來自個位的進位信號,一共須要三個數進行相加,才能獲得結果。可是基本的門電路以及組合而成的半加器輸入內容都是兩位的。其實解決辦法很簡單,即經過兩個半加器和一個或門就能組合成一個全加器。
如圖W的輸出即爲二位的值。有了全加器理論上兩個8位數的加法運算就能夠實現了:
能夠看到的是,個位和其餘高位不一樣,個位只須要一個半加器便可。而最高位即最左側的一位表示的是咱們的加法是否溢出了。整個電路中有這樣一個信號來表示咱們所作的加法運算是否溢出了,能夠給到硬件層面的其它標誌位中,來讓計算機知曉這樣算溢出了,以便獲得計算機硬件層面的支持。
算術邏輯單元(ALU):是中央處理器的執行單元,是全部中央處理器的核心組成部分,由與門和或門構成的算術邏輯單元,主要功能是進行二進制的算術運算,如加減乘數(不包括整數除法)。
13 * 9 = 117
的二進制轉化表:
實際二進制數據在進行乘法運算的時候,退化成了位移和加法。由於是二進制乘法,因此乘數的各位和被乘數的乘積不是所有爲0就是把被乘數複製一份下來。須要注意的是乘數的每位進行一次乘積運算以後,下一次的運算結果就須要向高位移動一位。最後這些結果相加起來便可
二進制的乘法運算具體放到電路中的話,也並不須要引入任何新的、更復雜的電路,仍然用最基礎的電路便可,只要用不一樣的接線方式,就可以實現一個基本的乘法。最簡單的實現思路就是,咱們只要根據乘數從個位一直到高位經過一個門電路來控制每位的輸出信號,來判斷和被乘數的結果是所有爲0輸出仍是把被乘數複製一份輸出,並將結果存儲並累加到某個寄存器上便可。
先拿乘數最右側的個位乘以被乘數,而後把結果存入到寄存器中,而後,把被乘數左移一位,把乘數右移一位,仍然用乘數的個位去乘以被乘數,而後把結果加到剛纔的寄存器上。反覆重複這一步驟,直到二者分別不能再左移和右移位置。這樣,乘數和被乘數其實僅僅須要簡單的加法器(結果的累加),一個能夠支持其左移一位的電路和一個右移一位的電路,以及一個開關(判斷乘數的每位和被乘數乘積的結果是複製仍是0)就能完成整個乘法。 如圖所示
這裏的控制測試,其實就是經過一個時鐘信號,來控制左移、右移以及從新計算乘法和加法的時機。
13 * 9 的具體豎列圖
由上圖的分解示意圖,能夠發現其實所謂的位移+加法
。並非徹底獨立的,乘數的最高位在進行乘法運算以前任然須要低位的運算完才能夠。因此咱們用的是 4 位數,因此要進行 4 組「位移 + 加法」的操做。並且這 4 組操做還不能同時進行。由於 下一組的加法要依賴上一組的加法後的計算結果,下一組的位移也要依賴上一組的位移的結果。這樣,整個算法是「順序」的,每一組加法或者位移的運算都須要必定的時間,及必定的等待時間。
若是要優化整個乘法器的運算,能夠看到影響執行速度的緣由有以下幾點:
位移+加法
運算都具備強關聯及前後關係。解決辦法就是把咱們的電路進行展開,首先針對第一點,咱們上面所看到的豎列圖分析出所謂的每組位移+加法
的強關聯關係及前後關係是由於咱們人分析,但其實對於計算機的電路而言,當相加的兩個數是肯定的,那高位是否會進位其實也是肯定的。也就是說,對於計算機的電路而言,高位和地位能夠同時出結果,電路是自然並行的,也就不存在所謂的強關聯關係。同時對應的第三點的門延時也就只有一組加法進行運算的門延時存在了,即3T的門延時。
能夠看到其實乘法器的實現方式共有兩種:
定點數的表示方法:用 4 個比特來表示 0~9 的整數,那麼 32 個比特就能夠表示 8 個這樣的整數。而後咱們把最右邊的 2 個 0~9 的整數,當成小數部分;把左邊 6 個 0~9 的整數,當成整數部分。這樣,咱們就能夠用 32 個比特,來表示從 0 到 999999.99 這樣 1 億個實數了。
用二進制來表示十進制的編碼方式,叫做BCD 編碼。
缺點:能表示數值過小,本來32位的數值表示方法,能表示的數值的最大值是42億。用BCD編碼的話最大隻能表示到100w。
浮點數彌補了定點數表示方式在表達數值上缺陷。浮點數使用科學計數法的方式來進行數值的表示。 浮點數的科學計數法的表示有一個IEEE 754的標準,它定義了兩個基本的格式。一個是用 32 比特表示單精度的浮點數,也就是咱們經常說的 float 或者 float32 類型。另一個是用 64 比特表示雙精度的浮點數,也就是咱們平時說的 double 或者 float64 類型。
根據國際標準IEEE 754,任意一個二進制浮點數V能夠表示成下面的形式:
例如:
IEEE 754規定,對於32位的浮點數,最高的1位是符號位s,接着的8位是指數E,剩下的23位爲有效數字M。 對於64位的浮點數,最高的1位是符號位S,接着的11位是指數E,剩下的52位爲有效數字M。
由於E爲8位,因此它的取值範圍爲0~255。因爲科學計數法中的E是能夠出現負數的,因此IEEE 754規定,E的真實值必須再加上一個中間數,對於8位的E,這個中間數是127;對於11位的E,這個中間數是1023。
好比,2^10的E是10,因此保存成32位浮點數時,必須保存成10+127=137,即10001001。
而後,指數E還能夠再分紅三種狀況:
在這樣的浮點數表示下,不考慮符號的話,浮點數可以表示的最小的數和最大的數,差很少是 1.17 * 10^-38
和 3.40 * 10^38
,表示的數值範圍就大不少了。此時f爲23個0,e爲-126 和 f爲23個1,e爲127。
正是由於這個數對應的小數點的位置是「浮動」的,它才被稱爲浮點數。隨着指數位 e 的值的不一樣,小數點的位置也在變更。對應的,前面的 BCD 編碼的實數,就是小數點固定在某一位的方式,咱們也就把它稱爲定點數。
0.1~0.9 這 9 個數,其中只有 0.5 可以被精確地表示成二進制的浮點數。而其餘的都只是一個近似的表達。
參考: