深入理解計算機系統_第一章_計算機系統漫遊

深入,並且廣泛
				-沉默犀牛

寫在前面

今天是2018/12/14,還有一週我的實習期就結束了,發現自己的基礎特別薄弱,選了幾本提升基礎的書籍,《深入理解計算機系統》是我將要看的第一本書,特別開這個系列文章,記錄所學所得所想,也用作檢驗自己學習進度的指標之一。
PS:大部分是摘抄書中原話,[]方括號中的內容是自己所想所得。

計算機系統漫遊

所有的計算機都有相似的硬件和軟件結構,它們又執行着相似的功能。這本書就是爲了那些希望深入瞭解這些組件如何工作以及這些組件是如何影響程序的正確性和性能的程序員而寫的。

以下將以一個hello程序的生命週期來開始對系統的學習——從它被程序員創建開始,到在系統上運行,輸出簡單的消息,然後終止。我們將沿着這個生命週期,簡要的介紹一些逐步出現的關鍵概念,專業術語和組成部分。

#include <stdio.h>
int main()
{
	printf("hello,world \n");
	return 0;
}

[以前從未考慮過類似於此的問題,大學時只是簡單的編譯一下看到輸出結果就很開心了,現在來了解一下它的生命週期。]

信息就是位+上下文

hello程序的生命週期是從一個源程序(或者說源文件)開始的,即程序員通過編輯器創建並保存的文本文件,文件名是hello.c。源程序實際上就是一個由值0和1組成的位(又稱爲比特)序列,8個位被組織成一組,成爲字節。每個字節表示程序中的某些文本字符。

所以hello程序是以字節序列的方式存儲的文件中的。每個字節都有一個整數值,對應於某些字符。例如第一個字節的整數值爲35,它對應的字符是"#",第二個字節的整數值爲105,它對應的字符是"i"。像hello.c這樣只由ASCII字符構成的文本稱爲文本文件,其他所有的文件都稱爲二進制文件

  • hello.c的表示方法說明了一個基本思想:系統中的所有信息——包括磁盤文件、內存中的程序、內存中存放的用戶數據以及網絡上傳輸的數據,都是用一串比特表示的。區分不同數據對象的唯一方法是我們讀到這些數據對象時的上下文。比如,在不同的上下文中,一個同樣的字節序列可能表示爲一個整數,浮點數,字符串或者機器指令。

[看這些內容的時候,想到了一個生活中類比的事情(也與我最近在看哲學書籍有關):假設有外在存在的世界(位),這個世界在不同的人眼中看,反射出來的就是不一樣的(這裏暫不討論陽明心學中的吾心光明,只討論西方哲學思想),如果我相信世界是消極的,是無序的,是黑暗的(上下文),那麼世界在我這裏反射出來的就是消極的,無序的,黑暗的(信息),如果我相信世界是積極的,是有序的,是光明的(上下文),那麼世界在我這裏反射出來的就是積極的(信息)。]
[看當時自己這個類比還覺得蠻好的,哈哈哈 -2019/5/6]

程序被其他程序翻譯成不同的格式

hello程序的生命週期是從一個高級C語言程序開始的(因爲這種形式能夠被人讀懂)。然而,爲了在系統上運行hello.c程序,每條C語句都必須被其他程序轉化爲一系列的低級機器語言指令。然後這些執行按照一種稱爲可執行目標程序的格式打好包,並且以二進制磁盤文件的形式存放起來,目標程序也稱爲可執行目標文件

GCC編譯器驅動程序讀取源文件hello.c,並把它翻譯成一個可執行目標文件hello。翻譯過程如下圖:

在這裏插入圖片描述

  • 預處理階段:預處理器(cpp)根據以字符#開頭!的命令,修改原始的C程序。比如hello.c中的第一行 #include <stdio.h> 命令告訴預處理器讀取系統頭文件stdio.h的內容,並把它直接插入程序文本中。結果得到了另一個C程序,通常以.i作爲文件擴展名。
  • 編譯階段:編譯器(ccl)將文本文件hello.i翻譯成文本文件hello.s,它包含一個彙編語言程序
  • 彙編階段:彙編器(as)將hello.s翻譯成機器指令語言,把這些指令打包爲一種叫做可重定位目標程序(relocatable object program)的格式,並將結果保存在目標文件hello.o中。
  • 鏈接階段:hello程序調用的printf函數,printf函數存在一個名爲printf.o的單獨的預編譯好了的目標文件中,而這個文件必須以某種方式合併到我們的hello.o程序中。鏈接器(ld)就負責處理這種合併。結果就得到了hello文件,它是一個可執行目標文件(或可執行文件),可以被加載到內存中,由系統執行。

瞭解編譯系統如何工作是大有益處的

有一些重要的原因促使程序員必須知道編譯系統是如何工作的:

  • 優化程序性能。比如,一個switch語句是否總是比一些列的if-else語句高效得多?一個函數調用的開銷有多大?while循環比for循環更有效嗎?指針引用比數據索引更有效嗎?爲什麼循環求和的結果放到一個本地變量中,會比將其放到一個通過引用傳遞過來的參數中,運行起來快很多呢?爲什麼只是簡單地重新排列一下算術表達式中括號就能讓函數運行的更快呢?
  • 理解鏈接時出現的錯誤。比如,鏈接器報告說它無法解析一個引用,這是什麼意思?靜態變量和全局變量的區別是什麼?如果你在不同的C文件中定義了名字相同的兩個全局變量會發生什麼?靜態庫和動態庫的區別是什麼?我們在命令行上排列庫的順序有什麼影響?最嚴重的是,爲什麼有些鏈接錯誤直到運行時纔會出現?
  • 避免安全漏洞。緩衝區溢出錯誤是造成大多數網絡和Internet服務器上安全漏洞的主要原因。存在這些錯誤的因爲很少有程序員能夠理解需要限制從不受信任的源接受數據的數量和格式。

處理器讀出並解釋存儲在內存中的指令

此刻,hello.c源程序已經被編譯系統翻譯成了可執行目標文件hello,並被存放在磁盤中。想要在Unix系統上運行該可執行文件,我們將它的文件名輸入到稱爲shell的應用程序中即可。

shell是一個命令行解釋器,它輸出一個提示符,等待輸入一個命令行,然後執行這個命令。如果該命令行的第一個單詞不是一個內置的shell命令,那麼shell就會假設這是一個可執行文件的名字,它將加載並運行這個文件。

系統的硬件組成

這張圖是Intel系統產品族的模型,但是所有其他系統也有相同的外觀和特性。
在這裏插入圖片描述

  1. 總線。貫穿整個系統的一組電子管道,稱爲總線,它攜帶信息字節並負責在各個部件間傳遞。通常總線被設計成傳送定長的字節快,也就是字(Word)。字中的字節數(即字長)是一個基本的系統參數,各個系統中都不盡相同。現在大多數機器字長要麼是4個字節(32位),要麼是8個字節(64位)。

  2. I/O設備。I/O設備是系統與外部世界的聯繫通道。上圖中的示例系統包括四個I/O設備:作爲用戶輸入的鍵盤和鼠標,作爲用戶輸出的顯示器,用於長期存儲數據和程序的磁盤驅動器(簡單的說就是磁盤)。最開始,可執行程序hello就存放在磁盤上。每個I/O設備都通過一個控制器或者適配器與I/O總線相連。控制器是I/O設備本身或者系統的主印製電路板(通常稱爲主板)上的芯片組,適配器則是一塊插在主板插槽上的卡。它們的功能都是在I/O總線和I/O設備之間傳遞信息。[確實,在linux驅動中,也是這樣的架構。比如一個i2c設備,是掛接在i2c控制器上的,再由i2c控制器負責總線通訊的時序等流程。 -2019/5/6]

  3. 主存。主存是一個臨時存儲設備,在處理器執行程序時,用來存放程序和程序處理的數據。從物理上來說,主存是由一組動態隨機存取存儲器(DRAM)芯片組成的。從邏輯上來說,存儲器是一個線性的字節數組,每個字節都有其唯一的地址(數據索引),這些地址是從零開始的。一般來說,組成程序的每條機器指令都由不同數量的字節構成。與C程序變量相對應的數據項的大小是根據類型變化的。比如,在運行Linux的x86-64機器上,short類型的數據需要2個字節,int 和 float 類型需要4個字節,而long 和 double 類型需要8個字節。

  4. 處理器。中央處理單元(CPU),是解釋(或執行)存儲在主存中指令的引擎。處理器的核心是一個大小爲一個字的存儲設備(或寄存器),成爲程序計數器(PC)。在任何時候,PC都指向主存中的某條機器語言指令(即含有該條指令的地址)。從系統通電,到系統斷電,處理器一直在不斷地執行PC指向的指令,再更新程序計數器,使其指向下一條指令。處理器看上去是按照一個非常簡單的指令執行模型來操作的,這個模型由指令集架構決定。在這個模型中,指令按照嚴格的順序執行,而執行一條指令包含執行一系列的步驟。處理器從PC指向的內存處讀取指令,解釋指令中的位,執行該指令指示的簡單操作,然後更新PC,使其指向下一條指令(這條指令不一定與剛剛執行的指令相鄰)。這樣簡單的操作並不多,它們圍繞着主存寄存器文件(register file)算數/邏輯單元(ALU)進行。寄存器文件是一個小的存儲設備,由一些單個字長的寄存器組成,每個寄存器都有唯一的名字。ALU計算新的數據和地址值。CPU在指令的要求下可能會執行這些操作。

    4.1 加載:從主存複製一個字節或者一個字到寄存器,以覆蓋寄存器原來的內容。
    4.2 存儲:從寄存器複製一個字節或者一個字到主存的某個位置,以覆蓋這個位置上原來的內容。
    4.3 操作:把兩個寄存器的內容複製到ALUALU對這兩個字做算術運算,並將結果存放到一個寄存器中,以覆蓋該寄存器中原來的內容。
    4.4 跳轉:從指令本身中抽取一個字,並將這個字複製到PC中,以覆蓋PC中原來的值。
    PS:我們需要將處理器的指令集架構和處理器的微體系結構區分開來:指令集架構描述的是每條機器指令的效果;微體系結構描述的是處理器實際上是如何實現的。

[這些內容對我來說都只是單純的輸入,有一些與之前學的微機對應了起來,PC,寄存器文件,ALU等,對整個系統硬件有了一個總體認知]

運行hello程序

現在介紹當我們運行示例程序時到底發生了些什麼,這裏先做整體上的描述,省略很多細節,之後再補充。

  1. 初始時,shell程序執行它的指令,等待我們輸入一個命令。當我們在鍵盤上輸入字符串“./hello”後,shell程序將字符逐一讀入寄存器,再把它們放到內存([內存就是圖中的主存])中,如下圖:在這裏插入圖片描述
  2. 當我們在鍵盤上敲回車時,shell程序就知道我們已經結束了命令的輸入。然後shell執行一系列指令來加載可執行的 hello 文件,這些指令將hello目標文件中的代碼和數據從磁盤複製到主存。數據包括最終會被輸出的字符串“hello,world \n”。
    在這裏插入圖片描述
  3. 一旦目標文件 hello中的代碼和數據被加載到主存,處理器就開始執行hello程序的main程序中的機器語言指令。這些指令將“hello,world \n”字符串中的字節從主存複製到寄存器文件,再從寄存器文件中複製到顯示設備,最終顯示在屏幕上。
    在這裏插入圖片描述
    [通過這三幅圖我才理解一個程序的執行都經歷了什麼,再簡要概括一下:
    1.在shell輸入指令的時候,這些指令從鍵盤走到CPU中的寄存器,再到主存中;
    2.敲入回車之後,就把程序目標文件中的代碼和數據從磁盤(硬盤)複製到主存(內存);
    3.代碼和數據被複制到主存後,CPU就執行程序的機器語言指令,這些指令會從主存複製到寄存器,如果有輸出,再從寄存器中複製到顯示設備,最終顯示在屏幕上。]

高速緩存至關重要

這個簡單示例揭示了一個重要問題:系統花費了大量的時間把信息從一個地方挪到另一個地方。hello程序的機器指令最初是存放在磁盤上,程序加載時把機器指令複製到主存;當處理器運行程序時,指令從主存複製到處理器(準確的說是處理器中的寄存器)。這種複製就是開銷,減慢了程序“真正”的工作。因此,系統設計者的一個主要目標就是使這些複製操作儘可能快地完成。

比如說:一個典型系統上的磁盤可能比主存大1000倍,但是對處理器而言,從磁盤驅動器上讀取一個字的時間開銷要比從主存中讀取的開銷大1000萬倍!處理器從寄存器文件中讀取數據比從主存中讀取又幾乎快了100倍。而且隨着半導體技術的進步,這種處理器與主存之間的差距還在持續增大。加快處理器的運行速度比加快主存的運行速度要容易和便宜的多。

針對這種處理器與主存之間的差異,系統設計者採用了更小更快的存儲設備,稱爲高速緩存存儲器(cache memory,簡稱爲cache或高速緩存),作爲暫時的集結區域,存放處理器近期可能會需要的信息。下圖展示了典型系統中的cache
在這裏插入圖片描述

位於CPU芯片上的L1 cache的容量可以達到數萬字節,訪問速度幾乎和訪問寄存器文件一樣快。一個容量爲數十萬到數百萬字節的更大的L2 cache通過一條特殊的總線連接到CPU,進程訪問L2 cache的時間要比訪問L1 cache的時間長5倍,但是這仍然比訪問主存的時間快5~10倍。

L1 和 L2 cache是用一種叫做靜態隨機訪問存儲器(SRAM)的硬件計數實現的。更強大的系統甚至有3級cache:L1 和 L2 和 L3。使得系統獲得了一個很大的存儲區,同時訪問速度也很快,原因是利用了cache的局部性原理(即程序具有訪問局部區域裏數據和代碼的趨勢)。通過讓cache裏存放可能經常訪問的數據,大部分的內存操作都能在高速cache中完成。

  • 本書得出的重要結論之一就是,意識到cache存在的程序員能夠利用cache將程序的性能提高一個數量級。

存儲設備形成層次結構

在處理器和一個較大較慢的設備(例如主存)之間插入一個更小更快的存儲設備(例如cache)的想法已經成爲一個普遍的觀念。實際上,每一個計算機系統中的存儲設備都被組織成了一個存儲器層次結構,如下圖。
在這裏插入圖片描述

在這個層次結構中,從上至下,設備的訪問速度越來越慢、容量越來越大,並且每字節的造價也越來越便宜。寄存器文件在層級結構中位於最頂部,也就是第0級或記爲L0,上述說過的L1、L2、L3佔據了第1到3層,主存在第4層,以此類推。

正如可以運用不同的cache的知識來提高程序性能一樣,程序員同樣可以利用對整個存儲器層次結構的理解來提高程序性能。

[這幾節內容我理解了存儲器層次結構,實在太希望快點學習,能利用這些知識來讓我的程序更加高效了!另外我也在設想,是否可以利用cache的思想來改善一下我的生活,比如說努力提高自己的開發(空間)和任務交付(速度)的水平,讓自己成爲公司系統中的主存,進而成爲cache,甚至是寄存器?!?!,哈哈哈哈哈 whatever~]
[現在看去年留下的評論,都覺得積極陽光又可愛,怎麼現在竟然想不到這些了呢? -2019/5/6]

操作系統管理硬件

回到hello程序的例子。當shell加載和運行hello程序時,以及hello程序輸出自己的消息時,shell和hello程序都沒有直接訪問鍵盤、顯示器、磁盤或者主存。它們依靠的是操作系統提供的服務。我們可以把操作系統看成是應用程序和硬件之間插入的一層軟件。所有的應用程序對硬件的操作嘗試都必須通過操作系統。如下圖
在這裏插入圖片描述操作系統有兩個基本功能:1.防止硬件被失控的應用程序濫用;2.嚮應用程序提供簡單一致的機制來控制複雜而又通常大不相同的低級硬件設備。操作系統通過幾個基本的抽象概念(進程、虛擬內存和文件)來實現這兩個功能。如下圖
在這裏插入圖片描述
文件是對I/O設備的抽象表示。[linux下一切皆文件嘛,跟這裏一樣 -2018/5/6]
虛擬內存是對主存和磁盤I/O設備的抽象表示。
進程則是對處理器、主存和I/O設備的抽象表示。

進程

像hello這樣的程序在現在系統上運行時,OS會提供一種假象,好像OS上只有這個程序在運行。程序看上去是獨佔地使用CPU、主存和I/O設備。CPU看上去就像在不間斷地一條接一條地執行程序中的指令,即該程序的代碼和數據是系統內存中唯一的對象。這些假象是通過進程的概念來實現的,進程是計算機科學中最重要和最成功的概念之一。

進程是OS對一個正在運行的程序的一種抽象。在一個OS上可以運行多個進程,而每個進程都好像在獨佔地使用硬件。而併發運行,則是說一個進程的指令和另一個進程的指令是交錯執行的。在大多數系統中,需要運行的進程數量是多於可以運行它們的CPU的個數的。傳統系統在一個時刻只能執行一個程序,而多核處理器可以同時執行多個程序。無論是在單核還是多核系統中,一個CPU看上去都像是在併發地執行多個進程,這是通過CPU在進程間切換來實現的。OS實現這種交錯執行的機制成爲上下文切換。(爲了簡化討論,以下先只討論單處理器系統的情況)

OS保持跟蹤進程運行所需的所有狀態信息。這種狀態,也就是上下文,包括許多信息,比如PC和寄存器文件的當前值,以及主存的內容。在任何時刻,單處理器系統都只能執行一個進程的代碼。當OS決定要把控制權從當前進程轉移到某個新進程時,就會進行上下文切換(即保存當前進程的上下文、恢復新進程的上下文,然後將控制權傳遞到新進程)。新進程就會從它上次停止的地方開始。

讓我們看看示例hello進程運行場景的基本理念
在這裏插入圖片描述
示例場景中有兩個併發的進程:shell進程 和 hello進程。最開始,只有shell進程在運行,即等待命令行上的輸入。當我們讓它運行hello程序時,shell通過調用一個專門的函數,即系統調用,來執行我們的請求,系統調用會將控制權傳遞給OS。OS保存shell進程的上下文,創建新的hello進程及其上下文。然後將控制權傳給新的hello進程。hello進程終止後,OS恢復shell進程的上下文,並將控制權傳回給它,shell進程會繼續等待下一個命令行輸入。

從一個進程到另一個進程的轉換是由OS內核(kernel)管理的。內核是OS系統代碼常駐主存的部分。當應用程序需要OS的某些操作時,比如讀寫文件,它就執行一條特殊的系統調用(System call)指令,將控制權傳遞給kernel。然後內核執行被請求的操作並返回應用程序(注意,kernel不是一個獨立的進程。而是系統管理全部進程所用代碼和數據結構的結合)。

線程

儘管通常我們認爲一個進程只有單一的控制流,但是在現代系統中,一個進程實際上可以由多個稱爲線程的執行單元組成,每個線程都運行在進程的上下文中,並共享同樣的代碼和全局數據。由於網絡服務器中對並行處理的需求,線程成爲越來越重要的編程模型。當有多處理器可以用的時候,多線程也是一種使得程序可以運行得更快的方法。

虛擬內存

虛擬內存是一個抽象概念,它爲每個進程提供了一個假象,即每個進程都在獨佔地使用主存。每個進程看到的內存都是一致的,稱爲虛擬地址空間。下圖是Linux進程的虛擬地址空間。
在這裏插入圖片描述
在Linux中,地址空間最上面的區域是保留給OS中的代碼和數據的,這對所有進程來說都是一樣的。地址空間的底部區域存放用戶進程定義的代碼和數據。圖中的地址是從下往上增大的。

每個進程看到的虛擬地址空間由大量準確定義的構成,每個都有專門的功能。簡單的瞭解一下各個區:

  • 程序代碼和數據。對所有的進程來說,代碼是從同一固定地址開始,緊接着的是和C全局變量相對應的數據位置。代碼和數據區是直接按照可執行目標文件的內容初始化的,在例子中就是可執行文件hello。
  • 堆。代碼和數據區後緊隨着的是運行時。代碼和數據區在進程一開始時就被指定了大小,與此不同,當調用像malloc和free這樣的C標準庫函數時,堆可以在運行時動態地擴展和收縮。
  • 共享庫。大約在地址空間的中間部分是一塊用來存放像C標準庫和數學庫這樣的共享庫的代碼和數據的區域。共享庫的概念非常強大,也相當難懂
  • 棧。位於用戶虛擬地址空間頂部的是用戶棧,編譯器用它來實現函數調用。和堆一樣,用戶棧在程序執行期間可以動態地擴展和收縮。特別的,每次我們調用一個函數時,棧就會增長;從一個函數返回時,棧就會收縮。[在調用函數的時候要把返回地址要保存在棧裏面,所以調用一個函數,就需要增加一個返回地址,棧就增長,從一個函數返回,就減少一個返回地址,棧就收縮 -2019/5/6]
  • 內核虛擬內存。地址空間頂部的區域是爲內核保留的。不允許應用程序讀寫這個區域的內容或者直接調用內核代碼定義的函數。相反,它們必須調用內核來執行這些操作。

虛擬內存的運作需要硬件和操作系統軟件之間精密複雜的交互,包括對處理器生成的每一個地址的硬件翻譯。基本思想是把一個進程虛擬內存的內容存儲在磁盤上,然後用主存作爲磁盤的高速緩存。

[現在只是知道了虛擬內存的用途和每個區的用途,但是還是沒有感受到它有什麼好的,以後的學習中應該會體會到吧。]

文件

文件就是字節序列,僅此而已。每一個I/O設備,包括磁盤、鍵盤、顯示器,甚至網絡,都可以看成是文件。系統中的所有輸入輸出都是通過使用一組稱爲Unix I/O的系統函數調用讀寫文件來實現的。

文件這個簡單而精緻的概念是非常強大的,因爲它嚮應用程序提供了一個統一的視圖,來看待系統中可能含有的所有各式各樣的I/O設備。例如,處理磁盤文件內容的應用程序員可以非常幸福,因爲他們無須瞭解具體的磁盤計數。進一步說,同一個程序可以在使用不同磁盤技術的不同系統上運行。

[實習期間學習了TP和LCD的移植,在閱讀它們的驅動的時候,就能體會到這裏描述的概念,比如想讓LCD顯示一些東西,就在指定的緩存地址寫入希望顯示的東西,這不就是“把LCD看成了一個文件,我想要顯示什麼,我就在這個文件裏寫入什麼”嗎]

系統之間利用網絡通信

系統漫遊至此,我們一直是把系統視爲一個孤立的硬件和軟件的集合體。實際上,現代系統經常通過網絡和其他系統連接到一起。從一個單獨的系統來看,網絡可以視爲一個I/O設備,如下圖。
在這裏插入圖片描述
當系統從主存複製一串字節到網絡適配器時,數據流經過網絡到達另一臺機器,而不是比如說到達本地磁盤驅動器[這是說我發到網絡上的信息,在本地是沒有保存的意思嗎?我可以理解成我在CSDN寫這篇博客的文本時,我發佈文章後,我本地確實沒有這篇博客的文本嗎?這句話更想強調的還是數據是流到了其他機器上,而不是流到本地機器上這個意思 -2019/5/6]。相似的,系統可以讀取從其他機器發送來的數據,並把數據複製到自己的主存。

隨着Internet這樣的全球網絡的出現,從一臺主機複製信息到另外一臺主機已經成爲計算機系統中最重要的用途之一。比如email、FTP、Telnet這樣的應用都是基於網絡複製信息的功能。

再拿上面的hello例子,我們可以使用熟悉的Telnet應用在一個遠程主機上運行hello程序[我一點也不熟悉…,但是跟secureCRT軟件的SSH登錄一個道理吧 -2019/5/6]。假設用本地主機上的Telnet客戶端連接遠程主機上的Telnet服務器。在我們登錄到遠程主機並運行shell後,遠端的shell就在等待接收輸入命令。此後在遠端運行hello程序包括下圖的五個步驟。
在這裏插入圖片描述
當我們在Telnet客戶端鍵入“hello”字符串並敲下回車後,客戶端軟件就會將這個字符串發送到Telnet的服務器。Telnet服務器從網絡上接收到這個字符串後,會把它傳遞給遠端shell程序。接下來,遠端shell運行hello程序,並將輸出返回給Telnet服務器。最後,Telnet服務器通過網絡把輸出串轉發給Telnet客戶端,客戶端就將輸出串輸出到我們的本地終端上。

這種客戶端和服務器之間交互的類型在所有網絡應用中是非常典型的。

[我們自己買境外服務器來搭建VPN應該是一個道理吧?在科學上網的時候信息傳到服務器上,再轉發給我們的設備。]
[不對,用VPN的時候信息應該沒有傳到VPS上 ,這裏的遠程操作就跟SSH登錄是一樣的。-2019/5/6]

重要主題

在此有一個很重要的觀點,那就是系統不僅僅只是硬件。系統是硬件和系統軟件相互交織的集合體,它們必須共同協作以達到運行應用程序的最終目的。

Amdahl定律

該定律的主要思想是,當我們對系統的某個部分加速時,其對系統整體性能的影響取決於該部分的重要性和加速程度。
在這裏插入圖片描述
T old 是系統性能提升前執行某應用程序需要時間
α 系統某部分所執行時間與執行應用程序總時間比例
k 該部分性能提升比例
T new 系統性能提升後執行某應用程序需要的時間

由此可以計算加速比爲:
在這裏插入圖片描述
舉個例子,α = 60%,k = 3 則 S = 1.67,雖然我們對系統的一個部分做出了重大改進,但是獲得的系統加速比卻明顯小於這部分的加速比。這就是Amdahl定律的重要觀點——想要顯著加速整個系統,必須提升全系統中相當大的部分的速度

[這個定律似乎可以用於任何由多方複雜因素共同影響的事情,比如我想提高GPA,我想減肥等,因爲經歷過減肥的痛苦,所以感受頗深。減肥需要控制飲食、改變作息、改變日常習慣、增強鍛鍊等幾個部分,妄圖單單改變其中某一個部分來整體提升減肥加速比,簡直是癡心妄想,哈哈哈不小心暴露了自己對於減肥這件事情的深惡痛絕。]
[之前竟然能想到減肥,順便說一下最近看到的有關減肥的事情,就是要牢記減肥這個目標,而不是減肥是“好”是“壞”,如果我覺得鍛鍊是一件好事,那我鍛鍊之後就會給自己一個“道德許可”,這個許可讓我同意自己做一點“壞”事,就回去多喫點好喫的,這可能不進反退了,而如果我時刻記得我鍛鍊是爲了達成減肥目標,控制飲食也是爲了達成減肥目標,做了其中一個並不能換來不做另一個的權利,就會大大增強自控力。 -2019/5/6]

併發和並行

數字計算機的整個歷史中,有兩個需求是驅動進步的持續動力:一個是我們想要計算機做的更多,另一個是我們想要計算機運行的更快。
我們用的術語併發(concurrency)是一個通用的概念,指一個同時具有多個活動的系統。並行(parallelism)指的是用併發使一個系統運行得更快。並行可以在計算機系統的多個抽象層次上運用。
[在linux系統中,爲了阻止不同driver的併發而引發的競爭,會採取spinlock、mutex、completion等鎖機制;爲了讓work實現併發,又會有workqueue機制。]

1.線程級併發。構建在進程這個抽象上,我們可以設計出同時有多個程序執行的系統,這就導致了併發。使用線程,我們甚至能夠在一個進程中控制多個控制流。傳統意義上,這種併發執行只是模擬出來的,是通過一臺計算機在它正在執行的進程間快速切換來實現的。這種併發形式允許多個用戶同時與系統交互,例如,在一個窗口中開啓Web瀏覽器,在另一個窗口中運行字處理器,同時又播放音樂。在以前,即使處理器必須在多個任務間切換,大多數實際的計算也都是由一個處理器來完成的。這種配置稱爲單處理器系統

當構建一個由單操作系統內核控制的多處理器組成的系統時,我們就得到了一個多處理器系統。隨着多核處理器超線程的出現,這種系統才變得常見。
在這裏插入圖片描述
多核處理器是將多個CPU(稱爲核)集成到一個集成電路芯片上。下圖描述了典型的多核處理器的組織結構。
在這裏插入圖片描述
其中微處理器芯片有4個CPU核,每個核都有自己的L1和L2高速緩存,其中的L1高速緩存分爲兩個部分——一個保存最近取到的指令,另一個存放數據。這些核共享更高層次的高速緩存,以及到主存的接口。

超線程,有時稱爲同時多線程,是一項允許一個CPU執行多個控制流的計數。它設計CPU某些硬件有多個備份,必須程序計數器和寄存器文件,而其他的硬件部分只有一份,比如執行浮點算術運算的單元。常規的處理器需要大約20000個時鐘週期做不同線程間的轉換,而超線程的處理器可以在單個週期的基礎上決定要執行哪一個線程。這使得CPU能夠更好的利用它的處理資源。

比如,假設一個線程必須等到某些數據被裝載到高速緩存中,那CPU就可以繼續去執行另一個線程。舉例來說,Intel Core i7處理器可以讓每個核執行兩個線程,所以一個4核的系統實際上可以並行地執行8個線程。

多處理器的使用可以以下兩方面提高系統性能。首先,它減少了在執行多個任務時模擬併發的需要。正如前面提到的,即使是隻有一個用戶使用的個人計算機也需要併發地執行多個活動。其次,它可以使應用程序運行得更快,當然,這必須要求程序是以多線程方式來書寫的,這些線程可以並行地高效執行。

2.指令級並行,在較低的抽象層次上,現代處理器可以同時執行多條指令的屬性成爲指令級並行。早起的微處理器,比如Intel 8086,需要多個時鐘週期來執行一條指令。最近的處理器可以保持每個時鐘週期執行2~4條指令、其實每條指令從開始到結束需要長得多的時間,大約20個週期,但是處理器使用了非常多的聰明技巧來同時處理多達100條指令。之後會研究流水線的使用。在流水線中,將執行一條指令所需要的活動劃分爲不同的步驟,將處理器的硬件組織成一系列的階段,每個階段執行一個步驟。這些階段可以並行地操作,來處理不同指令的不同部分。

如果處理器可以達到比一個週期一個指令更快的執行速率,就稱之爲超標量處理器。大多數現代處理器都支持超標量操作。

3 .單指令、多數據並行,在最低層次上,許多現代處理器擁有特殊的硬件,允許一條指令生產多個可以並行執行的操作,這種方式稱爲單指令、多數據,即SIMD並行。例如,比較新的Intel和AMD處理器都具有並行地對8對單精度浮點數(C數據類型float)做加法的指令。

提供這些SIMD指令多是爲了提高處理影像、聲音和視頻數據應用的執行速度。雖然有些編譯器會試圖從C程序中自動抽取SIMD並行性,但是更可靠的方法是用編譯器支持的特殊向量數據類型來寫程序,比如GCC就支持向量數據類型。

[簡要的理一下,併發是描述同時有多個活動的系統,是個通用概念,並行是指用併發來使一個系統運行得更快。並行分爲線程級並行指令集並行單指令、多數據並行

線程級並行是較高抽象層次的概念,就是同時執行多個程序,在早先的單處理器只是通過快速切換程序間切換來模擬的,現在是由多核處理器超線程實現的,多核處理器就是多個CPU,每個CPU執行一個任務,超線程就是一個CPU可以執行多個任務,兩種方法通常結合起來實現更多任務的並行。

指令級並行是在較低的抽象層次上,讓處理器同時執行多條指令,比如同時執行復制和相加的指令。

單指令、多數據並行是最低層次上,通過處理器擁有的特殊硬件,讓一條指令產生多個可以並行執行的操作,比如同時對好幾對數據執行加法指令。]

計算機系統中抽象的重要性

抽象的使用是計算機科學中最爲重要的概念之一。例如,爲一組函數規定一個簡單的API就是很好的編程習慣,程序員無須瞭解它內部的工作便可以使用這些代碼。

我們已經介紹了計算機系統中使用的幾個抽象。如下圖
在這裏插入圖片描述
在處理器中,指令集架構提供了對實際處理器硬件的抽象。使用這個抽象,機器代碼程序表現得就好像運行在一個一次只執行一條指令的處理器上。底層的硬件遠比抽象描述的要複雜精細,它並行地執行多條指令,但又總是與那個簡單有序的模型保持一致。只要執行模型一樣,不同的處理器實現也能執行同樣的機器代碼,而又提供不同的開銷和性能。

在學習操作系統時,我們介紹了三個抽象:文件是對I/O設備的抽象,虛擬內存是對程序存儲器的抽象,而進程是對一個正在運行的程序的抽象。我們再增加一個新的抽象:虛擬機,它提供對整個計算機的抽象,包括操作系統、處理器和程序。因爲一些計算機必須能夠運行爲不同的操作系統或同一操作系統的不同版本設計的程序,虛擬機才顯示出其管理計算機方式上的優勢。

小結

計算機系統是由硬件和系統軟件組成的,它們共同協作以運行應用程序。計算機內部的信息被表示爲一組組的位,它們依據上下文有不同的解釋方式。程序被其他程序翻譯成不同的形式,開始時是ASCII文本,然後被編譯器和鏈接器翻譯成二進制可執行文件。

處理器讀取並解釋存放在主存裏的二進制指令。因爲計算機花了大量的時間在主存、I/O設備和CPU寄存器之間複製數據,所以將系統中存儲設備劃分成層次結構——CPU寄存器在頂部,接着是多層的硬件高速緩存存儲器(SRAM),主存(DRAM)和磁盤存儲器。在層次模型中位於更高層的存儲設備比磁層的存儲設備要更快,單位比特造價也更高。層次結構中較高層次的存儲設備可以作爲較低層次設備的高速緩存。通過理解和運用這種存儲層次結構的知識,程序員可以優化C程序的性能。

操作系統內核是應用程序和硬件之間的媒介。它系統了三個基本抽象:1.文件是對I/O設備的抽象;2.虛擬內存是對主存和磁盤的抽象;3.進程是處理器、主存和I/O設備的抽象。

最後,網絡提供了計算機系統之間通信的手段。從特出系統的角度來看,網絡就是一種I/O設備。

[第一章-計算機系統漫遊的內容就全部結束了,看完了這一章對計算機系統有了初步的瞭解。]
[再次看了一遍以後,加深了印象,對於一些概念有了更深的認識,比如進程和線程,不過還是不知道linux系統中的進程線程併發是怎麼做的,快快學習吧 -2019/5/6]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章