計算機系統是由硬體和系統軟體組成的,它們共同工作來執行應用程式。雖然系統的具體實現方式隨着時間不斷變化,但是系統內在的概念卻沒有改變。所有計算機系統都有相似的硬體和軟體元件,它們又執行着相似的功能。一些程式設計師希望深入瞭解這些元件是如何工作的以及這些元件是如何影響程式的正確性和效能的,以此來提高自身的技能
在Kernighan和Ritchie的關於C程式語言的經典教材中,他們通過下面 下麪所示的hello程式來向讀者介紹C。儘管hello程式非常簡單,但是爲了讓它實現執行,系統的每個主要組成部分都需要協調工作。從某種意義上來說,本文大致幫助你瞭解當你在系統上執行hello程式時,系統發生了什麼以及爲什麼會這樣
#include <stdio.h>
int main()
{
printf(「hello, world\n」);
return 0;
}
我們通過跟蹤hello程式的生命週期來開始對系統的學習——從它被程式設計師建立開始,到在系統上執行,輸出簡單的訊息,然後終止。我們將沿着這個程式的生命週期,簡要地介紹一些逐步出現的關鍵概念、專業術語和組成部分。後面的更多文章將圍繞這些內容展
一、資訊就是位+上下文
hello程式的生命週期是從一個源程式(或者說原始檔)開始的,即程式設計師通過編輯器建立並儲存的文字檔案,檔名是hello.c。源程式實際上就是一個由值0和1組成的位(又稱爲位元)序列,8個位被組織成一組,稱爲位元組。每個位元組表示程式中的某些文字字元
大部分的現代計算機系統都使用ASCII標準來表示文字字元,這種方式實際上就是用一個唯一的單位元組大小的整數值來表示每個字元(有其他編碼方式用於表示非英語類語言文字,詳情參閱後文「資訊的表示和處理」)。比如,下圖中給出了hello.c程式的ASCII碼錶 碼表示
hello.c程式是以位元組序列的方式儲存在檔案中的。每個位元組都有一個整數值,對應於某些字元。例如,第一個位元組的整數值是35,它對應的就是字元「#」。第二個位元組的整數值爲105,它對應的字元是‘i’,依此類推。注意,每個文字行都是以一個看不見的換行符‘\n’來結束的,它所對應的整數值爲10。像hello.c這樣只由ASCII字元構成的檔案稱爲文字檔案,所有其他檔案都稱爲二進制檔案
hello.c的表示方法說明了一個基本思想:系統中所有的資訊——包括磁碟檔案、記憶體中的程式、記憶體中存放的用戶數據以及網路上傳送的數據,都是由一串位元表示的。區分不同數據物件的唯一方法是我們讀到這些數據物件時的上下文。比如,在不同的上下文中,一個同樣的位元組序列可能表示一個整數、浮點數、字串或者機器指令
作爲程式設計師,我們需要瞭解數位的機器表示方式,因爲它們與實際的整數和實數是不同的。它們是對真值的有限近似值,有時候會有意想不到的行爲表現。這方面的基本原理將在後面「資訊的表示和處理」的文章詳細描述
備註:C程式語言的起源
C語言是貝爾實驗室的Dennis Ritchie於1969年~1973年間建立的。美國國家標準學會(American National Standards Institute,ANSI)在1989年頒佈了ANSI C的標準,後來C語言的標準化成了國際標準化組織(International Standards Organization,ISO)的責任。這些標準定義了C語言和一系列函數庫,即所謂的C標準庫。Kernighan和Ritchie在他們的經典著作中描述了ANSI C,這本著作被人們滿懷感情地稱爲「K&R」。用Ritchie的話來說,C語言是「古怪的、有缺陷的,但同時也是一個巨大的成功」
爲什麼會成功呢?
C語言與Unix操作系統關係密切。C從一開始就是作爲一種用於Unix系統的程式語言開發出來的。大部分Unix內核(操作系統的核心部分),以及所有支撐工具和函數庫都是用C語言編寫的。20世紀70年代後期到80年代初期,Unix風行於高等院校,許多人開始接觸C語言並喜歡上它。因爲Unix幾乎全部是用C編寫的,它可以很方便地移植到新的機器上,這種特點爲C和Unix贏得了更爲廣泛的支援。
C語言小而簡單。C語言的設計是由一個人而非一個協會掌控的,因此這是一個簡潔明瞭、沒有什麼冗贅的設計。K&R這本書用大量的例子和練習描述了完整的C語言及其標準庫,而全書不過261頁。C語言的簡單使它相對而言易於學習,也易於移植到不同的計算機上。
C語言是爲實踐目的設計的。C語言是設計用來實現Unix操作系統的。後來,其他人發現能夠用這門語言無障礙地編寫他們想要的程式。
C語言是系統級程式設計的首選,同時它也非常適用於應用級程式的編寫。然而,它也並非適用於所有的程式設計師和所有的情況。C語言的指針是造成程式設計師困惑和程式錯誤的一個常見原因。同時,C語言還缺乏對非常有用的抽象的顯式支援,例如類、物件和異常。像C++和Java這樣針對應用級程式的新程式語言解決了這些問題
二、編譯流程
hello 程式的生命週期是從一個高階 C 語言程式開始的,因爲這種形式能被人讀懂。然而,計算機系統是讀不懂高階語言的。爲了在系統上執行 hello.c 程式,每條 C 語句都必須要被其他程式轉化爲一系列的低階機器語言指令。
在Unix系統中,從原始檔到目標檔案的轉換是由編譯器驅動程式完成的:
gcc -o hello hello.c
在這裏,GCC編譯器驅動程式讀取源程式檔案hello.c,並把它翻譯成一個可執行目標檔案hello。這個翻譯過程可分爲四個階段完成,如下圖所示。執行這四個階段的程式(前處理器、編譯器、彙編器、鏈接器)一起構成了編譯系統(complication system)。
預處理階段:前處理器 cpp 根據以字元 # 開頭的命令,修改原始的 C 程式,比如 Hello.c 中第一行 #include<studio.h> 命令告訴前處理器讀取系統檔案 stdio.h 的內容,並把它直接插入到程式中。結果就得到另一個 C 程式,通常是以 .i 作爲副檔名。
編譯階段:編譯器(ccl)將文字檔案 hello.i 翻譯成文字檔案 hello.s,它包含一個彙編語言程式,該程式包含函數main的另一,如下所示:
定義中2~7行的每條語句都以一種文字格式描述了一條敵機機器語言指令。彙編語言是非常有用的,因爲它爲不同高階語言的不同編譯器提供了通用的輸出語言。例如,C編譯器和Fortran編譯器產生的輸出檔案用的都是一樣的彙編語言
彙編階段:彙編器 as 將hello.s 翻譯成機器語言指令,把這些指令打包成一種叫做可重定位目標程式的格式,並將結果儲存在目標檔案 hello.o 中。hello.o 檔案是一個二進制檔案,它包含的17個字元是函數main的指令編碼。如果我們用文字編輯器開啓 hello.o 檔案,將會是一堆亂碼。
鏈接階段:在 hello.c 程式中,我們看到程式呼叫了 printf 函數,它是每個 C 編譯器都會提供的標準 C 庫中的一個函數。printf 函數存在於一個名爲 printf.o 的單獨的預編譯好了的目標檔案中,而這個檔案必須以某種方式合併到我們的 hello.o 程式中。鏈接器 ld 就是負責處理這種合併,結果就得到一個 hello 檔案,它是一個可執行目標程式,可以被載入到記憶體中,由系統執行
三、瞭解編譯系統如何工作是大有益處的
程式設計師瞭解編譯系統是如何工作的,可以進行:
優化程式效能
理解鏈接時出現的錯誤
避免安全漏洞
四、硬體結構
現在我們的hello.c源程式被編譯器翻譯成可執行物件檔案hello,冰杯存放在磁碟上。要想在Unix系統上執行該可執行檔案,我們將它的檔名輸入到稱爲shell的應用程式中:
./hello
系統的硬體組成
匯流排:貫穿整個系統的是一組電子管道,稱爲匯流排,它攜帶資訊位元組並負責在各個部件間傳遞。通常匯流排被設計成傳送定長的位元組塊,也就是(字)。字中的位元組數(即字長)是一個基本的系統參數,各個系統不盡相同。大多數機器字長是4個位元組(32位元)或8個位元組(64位元)
I/O裝置:
I/O(輸入/輸出)裝置是系統與外部世界的聯繫通道。我們的範例系統包括四個I/O裝置:鍵盤、滑鼠、顯示器、以及用於長期儲存數據和程式的磁碟(或稱磁碟驅動器)。最開始,可執行程式hello就存放在磁碟上
每個I/O裝置都通過一個控制器或適配器與I/O匯流排相連。控制器與適配器之間的區別主要在於它們的封裝方式。控制器是I/O裝置本身或主印製電路板(通常稱爲主機板)上的晶片組。而適配器則是一塊插在主機板插槽上的卡。但它們的功能都是在I/O匯流排和I/O裝置之間傳遞資訊
主記憶體:主記憶體是一個臨時儲存的裝置,在處理器執行程式時,用來存放程式和程式處理的數據。從物理上來說,主記憶體是由一組動態隨機存取記憶體(DRAM)晶片組成。從邏輯上來說,記憶體是一個線性的位元組陣列,每個位元組都有其唯一的地址(陣列索引),這些地址是從0開始的
處理器:
中央處理單元(CPU),簡稱處理器,是解釋或執行儲存再主記憶體中指令的引擎。處理器的核心是一個大小爲一個字的儲存裝置(或暫存器),稱爲程式計數器(PC)
在任何時刻,PC都指向主記憶體中的某條機器語言指令(即含有該條指令的地址)。從系統開始通電到斷電,處理器不斷地在執行程式計數器指向的指令,再更新程式計數器,使其指向下一條指令。處理器看上去是按照一個非常簡單的指令執行模型來操作的,這個模型是由指令集架構決定的
在這個模型中,指令按照嚴格的順序執行,而執行一條指令包含執行一系列的步驟。處理器從程式計數器指向的記憶體讀取指令,解釋指令中的位,執行該指令指示的簡單操作,然後再更新PC,使其指向下一條指令,而這條指令並不一定和在記憶體中剛剛執行的指令相鄰
這樣的操作並不多,它們圍繞着主記憶體、暫存器檔案和算術/邏輯單元進行。暫存器檔案是一個小的儲存裝置,由一些單個字長的暫存器組成,每個暫存器都有唯一的名字,ALU計算新的數據和地址值。CPU在指令的要求下可能會執行如下操作:
載入:從主記憶體複製一個位元組或一個字到暫存器,以覆蓋暫存器原來的內容
儲存:從暫存器複製一個位元組或一個字到主記憶體的某個位置,以覆蓋這個位置上原來的內容
操作:把兩個暫存器的內容複製到ALU,ALU對這兩個字做算術運算,將結果存放到一個暫存器中,以覆蓋原來暫存器中的內容
跳轉:從指令本身中抽取一個字,並將這個字複製到程式計數器(PC)中,以覆蓋PC中原來的值
處理器看上去是它的指令集架構的簡單實現,但是實際上現代處理器使用了非常複雜的機制 機製來加速程式的執行。因此,我們將處理器的指令集架構和處理器的微體系結構區分開來:指令集架構描述的是每條機器程式碼指令的效果;而微體系結構描述的是處理器實際上是如何實現的。在「程式的機器級表示」文章研究機器程式碼時,我們考慮的是機器的指令集架構所提供的抽象性。「處理器結構」文章將更詳細地介紹處理器實際上是如何實現的。「優化程式效能」文章中會用一個模型說明現代處理器是如何工作的,從而能預測和優化機器語言程式的效能
執行hello程式
前面簡單描述了系統的硬體組成和操作,現在開始介紹當我們執行範例程式時到底發生了些什麼。在這裏必須省略很多細節,稍後會做補充,但是現在我們將很滿意於這種整體上的描述
初始時,shell程式執行它的指令,等待我們輸入一個命令。當我們在鍵盤上輸入字串「./hello」後,shell程式將字元逐一讀入暫存器,再把它存放到記憶體中,如下圖所示:
當我們在鍵盤上敲回車鍵時,shell程式就知道我們已經結束了命令的輸入。然後shell執行一系列指令來載入可執行的hello檔案,這些指令將hello目標檔案中的程式碼和數據從磁碟複製到主記憶體。數據包括最終會被輸出的字串「hello, world\n」
利用直接記憶體存取(DMA,「記憶體層次結構」中討論)技術,數據可以不通過處理器而直接從磁碟到達主記憶體。這個步驟如下圖所示:
一旦目標檔案hello中的程式碼和數據被載入到主記憶體,處理器就開始執行hello程式的main程式中的機器語言指令。這些指令將「hello, world\n」字串中的位元組從主記憶體複製到暫存器檔案,再從暫存器檔案中複製到顯示裝置,最終顯示在螢幕上。這個步驟如下圖所示:
五、快取記憶體的重要性
hello程式的機器指令最初是放在磁碟上,當程式載入時它們被複制到主記憶體;當處理器執行程式時,指令又從主記憶體複製到處理器。相似的,數據串"hello, world/n"開始是放在磁碟上,然後被複制到主記憶體,最後從主記憶體複製到顯示裝置。從程式設計師的角度來看,這些複製就是開銷,減慢了程式「真正」的工作。因此,系統的設計者的一個主要目標就是使得這些複製操作儘快完成
根據機械原理,較大的儲存裝置比較小的儲存裝置執行得慢,而快速裝置的造價遠高於同類的低速裝置。比如說,一個典型系統上的磁碟驅動器可能比主記憶體大1000倍,但對處理器而言,從磁碟驅動器上讀取一個字的時間開銷要比從主記憶體中讀取的開銷大1000萬倍
類似地,一個典型的暫存器檔案只儲存幾百位元組的資訊,而主記憶體裡可存放幾十億位元組。然而,處理器從暫存器檔案中讀數據比從主記憶體中讀取幾乎要快100倍。並且隨着這些年半導體技術的進步,這種處理器與主記憶體之間的差距還在持續增大。加快處理器的執行速度比加快主記憶體的執行速度要容易和便宜得多
針對這種處理器與主記憶體之間的差異,系統設計者採用了更小更快的儲存裝置,稱爲快取記憶體記憶體(cache memory,簡稱爲cache或快取記憶體),作爲暫時的集結區域,存放處理器近期可能會需要的資訊
下圖展示了一個典型系統中的快取記憶體記憶體。位於處理器晶片上的L1快取記憶體的容量可以達到數萬位元組,存取速度幾乎和存取暫存器檔案一樣快。一個容量爲數十萬到數百萬位元組的更大的L2快取記憶體通過一條特殊的匯流排連線到處理器。進程存取L2快取記憶體的時間要比存取L1快取記憶體的時間長5倍,但是這仍然比存取主記憶體的時間快5~10倍。L1和L2快取記憶體是用一種叫做靜態隨機存取記憶體(SRAM)的硬體技術實現的
比較新的、處理能力更強大的系統甚至有三級快取記憶體:L1、L2和L3。系統可以獲得一個很大的記憶體,同時存取速度也很快,原因是利用了快取記憶體的區域性性原理,即程式具有存取區域性區域裡的數據和程式碼的趨勢。通過讓快取記憶體裡存放可能經常存取的數據,大部分的記憶體操作都能在快速的快取記憶體中完成
重要結論之一就是,意識到快取記憶體記憶體存在的應用程式設計師能夠利用快取記憶體將程式的效能提高一個數量級。你將在「記憶體層次結構」文章裡學習這些重要的裝置以及如何利用它們
六、儲存裝置形成層次結構
在處理器和一個較大較慢的裝置(例如主記憶體)之間插入一個更小更快的儲存裝置(例如快取記憶體)的想法已經成爲一個普遍的觀念。實際上,每個計算機系統中的儲存裝置都被組織成了一個記憶體層次結構,如下圖所示。在這個層次結構中,從上至下,裝置的存取速度越來越慢、容量越來越大,並且每位元組的造價也越來越便宜。暫存器檔案在層次結構中位於最頂部,也就是第0級或記爲L0。這裏我們展示的是三層快取記憶體L1到L3,佔據記憶體層次結構的第1層到第3層。主記憶體在第4層,以此類推
記憶體層次結構的主要思想是上一層的記憶體作爲低一層記憶體的快取記憶體。因此,暫存器檔案就是L1的快取記憶體,L1是L2的快取記憶體,L2是L3的快取記憶體,L3是主記憶體的快取記憶體,而主記憶體又是磁碟的快取記憶體。在某些具有分佈式檔案系統的網路系統中,本地磁碟就是儲存在其他系統中磁碟上的數據的快取記憶體
正如可以運用不同的快取記憶體的知識來提高程式效能一樣,程式設計師同樣可以利用對整個記憶體層次結構的理解來提高程式效能。「記憶體層次結構」相關文章將更詳細地討論這個問題
七、操作系統管理硬體
讓我們回到hello程式的例子。當shell載入和執行hello程式時,以及hello程式輸出自己的訊息時,shell和hello程式都沒有直接存取鍵盤、顯示器、磁碟或者主記憶體。取而代之的是,它們依靠操作系統提供的服務。我們可以把操作系統看成是應用程式和硬體之間插入的一層軟體,如下圖所示。所有應用程式對硬體的操作嘗試都必須通過操作系統
操作系統有兩個基本功能:
(1)防止硬體被失控的應用程式濫用
(2)嚮應用程式提供簡單一致的機制 機製來控制複雜而又通常大不相同的低階硬體裝置
操作系統通過幾個基本的抽象概念(進程、虛擬記憶體和檔案)來實現這兩個功能。如下圖所示,檔案是對I/O裝置的抽象表示,虛擬記憶體是對主記憶體和磁碟I/O裝置的抽象表示,進程則是對處理器、主記憶體和I/O裝置的抽象表示。我們將依次討論每種抽象表示
進程
像hello這樣的程式在現代系統上執行時,操作系統會提供一種假象,就好像系統上只有這個程式在執行。程式看上去是獨佔地使用處理器、主記憶體和I/O裝置。處理器看上去就像在不間斷地一條接一條地執行程式中的指令,即該程式的程式碼和數據是系統記憶體中唯一的物件。這些假象是通過進程的概念來實現的,進程是電腦科學中最重要和最成功的概念之一
進程是操作系統對一個正在執行的程式的一種抽象。在一個系統上可以同時執行多個進程,而每個進程都好像在獨佔地使用硬體。而併發執行,則是說一個進程的指令和另一個進程的指令是交錯執行的。在大多數系統中,需要執行的進程數是多於可以執行它們的CPU個數的。傳統系統在一個時刻只能執行一個程式,而先進的多核處理器同時能夠執行多個程式。無論是在單核還是多核系統中,一個CPU看上去都像是在併發地執行多個進程,這是通過處理器在進程間切換來實現的。操作系統實現這種交錯執行的機制 機製稱爲上下文切換。爲了簡化討論,我們只考慮包含一個CPU的單處理器系統的情況。我們會在本文最後「並行/併發」中討論多處理器系統
操作系統保持跟蹤進程執行所需的所有狀態資訊。這種狀態,也就是上下文,包括許多資訊,比如PC和暫存器檔案的當前值,以及主記憶體的內容。在任何一個時刻,單處理器系統都只能執行一個進程的程式碼。當操作系統決定要把控制權從當前進程轉移到某個新進程時,就會進行上下文切換,即儲存當前進程的上下文、恢復新進程的上下文,然後將控制權傳遞到新進程。新進程就會從它上次停止的地方開始。下圖展示了範例hello程式執行場景的基本理念
範例場景中有兩個併發的進程:shell進程和hello進程。最開始,只有shell進程在執行,即等待命令列上的輸入。當我們讓它執行hello程式時,shell通過呼叫一個專門的函數,即系統呼叫,來執行我們的請求,系統呼叫會將控制權傳遞給操作系統。操作系統儲存shell進程的上下文,建立一個新的hello進程及其上下文,然後將控制權傳給新的hello進程。hello進程終止後,操作系統恢復shell進程的上下文,並將控制權傳回給它,shell進程會繼續等待下一個命令列輸入
如下圖所示,從一個進程到另一個進程的轉換是由操作系統內核(kernel)管理的。內核是操作系統程式碼常駐主記憶體的部分。當應用程式需要操作系統的某些操作時,比如讀寫檔案,它就執行一條特殊的系統呼叫(system call)指令,將控制權傳遞給內核。然後內核執行被請求的操作並返迴應用程式。注意,內核不是一個獨立的進程。相反,它是系統管理全部進程所用程式碼和數據結構的集合
實現進程這個抽象概念需要低階硬體和操作系統軟體之間的緊密合作。我們將在第8章中揭示這項工作的原理,以及應用程式是如何建立和控制它們的進程的
執行緒
儘管通常我們認爲一個進程只有單一的控制流,但是在現代系統中,一個進程實際上可以由多個稱爲執行緒的執行單元組成,每個執行緒都執行在進程的上下文中,並共用同樣的程式碼和全域性數據。由於網路伺服器中對並行處理的需求,執行緒成爲越來越重要的程式設計模型,因爲多執行緒之間比多進程之間更容易共用數據,也因爲執行緒一般來說都比進程更高效
當有多處理器可用的時候,多執行緒也是一種使得程式可以執行得更快的方法,我們將在本文最後「並行/併發」中討論這個問題
在「併發程式設計」相關文章中,你將學習併發的基本概念,包括如何寫執行緒化的程式
虛擬記憶體
虛擬記憶體是一個抽象概念,它爲每個進程提供了一個假象,即每個進程都在獨佔地使用主記憶體。每個進程看到的記憶體都是一致的,稱爲虛擬地址空間。下圖所示的是Linux進程的虛擬地址空間(其他Unix系統的設計也與此類似)。在Linux中,地址空間最上面的區域是保留給操作系統中的程式碼和數據的,這對所有進程來說都是一樣。地址空間的底部區域存放使用者進程定義的程式碼和數據。請注意,圖中的地址是從下往上增大的
每個進程看到的虛擬地址空間由大量準確定義的區構成,每個區都有專門的功能。在後續的文章你將學到更多有關這些區的知識,但是先簡單瞭解每一個區是非常有益的。我們從最低的地址開始,逐步向上介紹
程式程式碼和數據。對所有的進程來說,程式碼是從同一固定地址開始,緊接着的是和C全域性變數相對應的數據位置。程式碼和數據區是直接按照可執行目標檔案的內容初始化的,在本書案例中就是可執行檔案hello。在「鏈接」文章中研究鏈接和載入時,你會學習更多有關地址空間的內容
堆。程式碼和數據區後緊隨着的是執行時堆。「程式碼和數據區」在進程一開始執行時就被指定了大小,與此不同,當呼叫像malloc和free這樣的C標準庫函數時,堆可以在執行時動態地擴充套件和收縮。在「虛擬記憶體」相關文章學習管理虛擬記憶體時,我們將更詳細地研究堆
共用庫。大約在地址空間的中間部分是一塊用來存放像C標準庫和數學庫這樣的共用庫的程式碼和數據的區域。共用庫的概念非常強大,也相當難懂。在「鏈接」文章中介紹動態鏈接時,將學習共用庫是如何工作的
棧。位於使用者虛擬地址空間頂部的是使用者棧,編譯器用它來實現函數呼叫。和堆一樣,使用者棧在程式執行期間可以動態地擴充套件和收縮。特別地,每次我們呼叫一個函數時,棧就會增長;從一個函數返回時,棧就會收縮。在「程式的機器級表示」中將學習編譯器是如何使用棧的
內核虛擬記憶體。地址空間頂部的區域是爲內核保留的。不允許應用程式讀寫這個區域的內容或者直接呼叫內核程式碼定義的函數。相反,它們必須呼叫內核來執行這些操作
虛擬記憶體的運作需要硬體和操作系統軟體之間精密複雜的互動,包括對處理器生成的每個地址的硬體翻譯。基本思想是把一個進程虛擬記憶體的內容儲存在磁碟上,然後用主記憶體作爲磁碟的快取記憶體。「虛擬記憶體」相關文章將解釋它如何工作,以及爲什麼對現代系統的執行如此重要
檔案
檔案就是位元組序列,僅此而已。每個I/O裝置,包括磁碟、鍵盤、顯示器,甚至網路,都可以看成是檔案。系統中的所有輸入輸出都是通過使用一小組稱爲Unix I/O的系統函數呼叫讀寫檔案來實現的
檔案這個簡單而精緻的概念是非常強大的,因爲它嚮應用程式提供了一個統一的檢視,來看待系統中可能含有的所有各式各樣的I/O裝置。例如,處理磁碟檔案內容的應用程式設計師可以非常幸福,因爲他們無須瞭解具體的磁碟技術。進一步說,同一個程式可以在使用不同磁碟技術的不同系統上執行。你將在「系統級I/O」文章中學習Unix I/O
八、系統之間利用網路通訊
系統漫遊至此,我們一直是把系統視爲一個孤立的硬體和軟體的集合體。實際上,現代系統經常通過網路和其他系統連線到一起。從一個單獨的系統來看,網路可視爲一個I/O裝置,如下圖所示。當系統從主記憶體複製一串位元組到網路適配器時,數據流經過網路到達另一臺機器,而不是比如說到達本地磁碟驅動器。相似地,系統可以讀取從其他機器發送來的數據,並把數據複製到自己的主記憶體
隨着Internet這樣的全球網路的出現,從一臺主機複製資訊到另外一臺主機已經成爲計算機系統最重要的用途之一。比如,像電子郵件、即時通訊、萬維網、FTP和telnet這樣的應用都是基於網路複製資訊的功能
回到hello範例,我們可以使用熟悉的telnet應用在一個遠端主機上執行hello程式。假設用本地主機上的telnet用戶端連線遠端主機上的telnet伺服器。在我們登錄到遠端主機並執行shell後,遠端的shell就在等待接收輸入命令。此後在遠端執行hello程式包括如下圖所示的五個基本步驟
當我們在telnet用戶端鍵入「hello」字串並敲下回車鍵後,用戶端軟體就會將這個字串發送到telnet的伺服器。telnet伺服器從網路上接收到這個字串後,會把它傳遞給遠端shell程式。接下來,遠端shell執行hello程式,並將輸出行返回給telnet伺服器。最後,telnet伺服器通過網路把輸出串轉發給telnet用戶端,用戶端就將輸出串輸出到我們的本地終端上
這種用戶端和伺服器之間互動的型別在所有的網路應用中是非常典型的。在「網路程式設計」相關文章中,你將學會如何構造網路應用程式,並利用這些知識建立一個簡單的Web伺服器
九、重要概念:Amdahl定律
Gene Amdahl,計算領域的早期先鋒之一,對提升系統某一部分效能所帶來的效果做出了簡單卻有見地的觀察。這個觀察被稱爲Amdahl定律(Amdahl’s law)
該定律的主要思想是,當我們對系統的某個部分加速時,其對系統整體效能的影響取決於該部分的重要性和加速程度。若系統執行某應用程式需要時間爲Told
假設系統某部分所需執行時間與該時間的比例爲α,而該部分效能提升比例爲k。即該部分初始所需時間爲αTold,現在所需時間爲(αTold)/k。因此,總的執行時間應爲
由此,可以計算加速比S=Told/Tnew:
舉個例子,考慮這樣一種情況,系統的某個部分初始耗時比例爲60%(α=0.6),其加速比例因子爲3(k=3)。則我們可以獲得的加速比爲1/[0.4+0.6/3]=1.67倍。雖然我們對系統的一個主要部分做出了重大改進,但是獲得的系統加速比卻明顯小於這部分的加速比。這就是Amdahl定律的主要觀點:要想顯著加速整個系統,必須提升全系統中相當大的部分的速度
十、重要概念:並行/併發
數位計算機的整個歷史中, 有兩個需求是驅動進步的持續動力:一個是我們想要計算機做得更多, 另一個是我們想要計算機執行得更快。 當處理器能夠同時做更多的事情時, 這兩個因素都會改進
我們用的術語併發(concurrency)是一個通用的概念, 指一個同時具有多個活動的系統;而術語並行 (parallelism) 指的是用併發來使一個系統執行得更快。 並行可以在計算機系統的多個抽象層次上運用。 在此, 我們按照系統層次結構中由高到低的 順序重點強調三個層次
執行緒級併發
構建在進程這個抽象之上, 我們能夠設計出同時有多個程式執行的系統, 這就導致了併發。 使用執行緒, 我們甚至能夠在一個進程中執行多個控制流
自 20 世紀 60 年代初期出現時間共用以來, 計算機系統中就開始有了對併發執行的支援。 傳統意義上, 這種併發執行只是模擬出來的, 是通過使一臺計算機在它正在執行的進程間快速切換來實現的, 就好像一個雜耍藝人保持多個球在空中飛舞一樣。 這種併發形式允許多個使用者同時與系統互動, 例如, 當許多人想要從一個Web伺服器獲取頁面時。 它還允許一個使用者同時從事多 個任務, 例如, 在一個視窗中開啓Web瀏覽器, 在另 一視窗中執行字處理器, 同時又播放音樂。 在以前, 即使處理器必須在多個任務間切換, 大多數實際的計算也都是由一個處理器來完成的。 這種設定稱爲單處理器系
當構建一個由單操作系統內核控制的多處理器組成的系統時, 我們就得到了一個多處理器系統。 其實從 20 世紀 80 年代開始, 在大規模的計算中就有了這種系統, 但是直到最近, 隨着多核處理器和超執行緒 (hyperthreading) 的出現, 這種系統才變得常見。 下圖給出了這些不同處理器型別的分類
多核處理器是將多個CPU(稱爲 「核")整合到一個積體電路晶片上。 下圖描述的是一個典型多核處理器的組織結構, 其中微處理器晶片有4個CPU核, 每個核都有自己的Ll和 LZ快取記憶體, 其中的Ll快取記憶體分爲兩個部分 一個儲存最近取到的指令, 另 一個存放數據。 這些核共用更高層次的快取記憶體, 以及到主記憶體的介面。 工業界的專家預言他們能夠將幾十個、 最終會是上百個核做到一個晶片上
超執行緒, 有時稱爲同時多執行緒(simultaneous multi-threading),是一項允許一個CPU執行多個控制流的技術。 它涉及CPU某些硬體有多個備份, 比如程式計數器和暫存器檔案, 而其他的硬體部分只有一份, 比如執行浮點算術運算的單元。 常規的處理器需要大約20 000個時鐘週期做不同線程間的轉換, 而超執行緒的處理器可以在單個週期的基礎上決定要執行哪一個執行緒。 這使得CPU能夠更好地利用它的處理資源。 比如, 假設一個執行緒必須等到某些數據被裝載到快取記憶體中, 那CPU就可以繼續去執行另一個執行緒。 舉例來說,Intel Core i7處理器可以讓每個核執行兩個執行緒, 所以一個4核的系統實際上可以並行地執行 8個執行緒
多處理器的使用可以從兩方面提高系統效能:
首先, 它減少了在執行多個任務時模擬併發的需要。 正如前面提到的, 即使是隻有一個使用者使用的個人計算機也需要併發地執行多個活動
其次, 它可以使應用程式執行得更快, 當然, 這必須要求程式是以多執行緒方式來書寫的, 這些執行緒可以並行地高效執行
因此, 雖然併發原理的形成和研究已經超過50年的時間了, 但是多核和超執行緒系統的出現才極大地激發了一種願望, 即找到書寫應用程式的方法利用硬體開發執行緒級並行性。 第12章會更深入地探討併發, 以及使用併發來提供處理器資源的共用, 使程式的執行允許有更多的並行
指令級並行
在較低的抽象層次上, 現代處理器可以同時執行多條指令的屬性稱爲指令級並行。 早期的微處理器, 如1978年的Intel 8086, 需要多個(通常是3~10個)時鐘週期來執行一條期的微處理器, 如1978年的Intel 8086, 需要多個(通常是3~10個)時鐘週期來執行從開始到結束需要長得多的時間, 大約 20 個或者更多週期, 但是處理器使用了非常多的聰明技巧來同時處理多達 100 條指令。 在「處理器體系結構」文章中, 我們會研究流水線 (pipelining)的使用。 在流水線中,將執行一步驟。這些階段可並行地操作,用來處理不同指令的不同部分。我們會看到一個相當簡單的硬體設計,它能夠達到接近一個時鐘週期一條指令的執行速率
如果處理器可以達到比一個週期一條指令更快的執行速率, 就稱之爲超標量 (super-scalar)處理器。 大多數現代處理器都支援超標最操作。「優化程式效能」文章中, 我們將描述超標量處理 器的高階模型。 應用程式設計師可以用這個模型來理解程式的效能。 然後, 他們就能寫出擁有 更高程度的指令級並行性的程式程式碼, 因而也執行得更快
單指令,多數據並行
在最低層次上, 許多現代處理器擁有特殊的硬體, 允許一條指令產生多個可以並行執行的操作, 這種方式稱爲單指令、 多數據, 即SIMD並行。 例如, 較新幾代的 Intel 和AMD處理器都具有並行地對8對單精度浮點數CC數據型別float)做加法的指令
提供這些SIMD指令多是爲了提高處理影像、 聲音和視訊數據應用的執行速度。 雖然 有些編譯器會試圖從C程式中自動抽取SIMD並行性, 但是更可靠的方法是用編譯器支援的特殊的向量數據型別來寫程式, 比如GCC就支援向量數據型別。 作爲對第5章中比較通用的程式優化描述的補充, 我們在網路旁註OPT:SIMD中描述了這種程式設計方式
十一、重要概念:抽象
抽象的使用是電腦科學中最爲重要的概念之一。例如,爲一組函數規定一個簡單的應用程式介面(API)就是一個很好的程式設計習慣,程式設計師無須瞭解它內部的工作便可以使用這些程式碼。不同的程式語言提供不同形式和等級的抽象支援,例如Java類的宣告和C語言的函數原型
我們已經介紹了計算機系統中使用的幾個抽象,如下圖所示:
在處理器裡,指令集架構提供了對實際處理器硬體的抽象。使用這個抽象,機器程式碼程式表現得就好像執行在一個一次只執行一條指令的處理器上。底層的硬體遠比抽象描述的要複雜精細,它並行地執行多條指令,但又總是與那個簡單有序的模型保持一致。只要執行模型一樣,不同的處理器實現也能執行同樣的機器程式碼,而又提供不同的開銷和效能
在學習操作系統時,我們介紹了三個抽象:檔案是對I/O裝置的抽象,虛擬記憶體是對程式記憶體的抽象,而進程是對一個正在執行的程式的抽象
我們再增加一個新的抽象:虛擬機器,它提供對整個計算機的抽象,包括操作系統、處理器和程式。虛擬機器的思想是IBM在20世紀60年代提出來的,但是最近才顯示出其管理計算機方式上的優勢,因爲一些計算機必須能夠執行爲不同的操作系統(例如,Microsoft Windows、MacOS和Linux)或同一操作系統的不同版本設計的程式
在後續的文章中,我們會具體介紹這些抽象
十二、本文總結
計算機系統是由硬體和系統軟體組成的,它們共同共同作業以執行應用程式
計算機內部的資訊被表示爲一組組的位,它們依據上下文有不同的解釋方式。程式被其他程式翻譯成不同的形式,開始時是ASCII文字,然後被編譯器和鏈接器翻譯成二進制可執行檔案
處理器讀取並解釋存放在主記憶體裡的二進制指令
因爲計算機花費了大量的時間在記憶體、I/O裝置和CPU暫存器之間複製數據,所以將系統中的儲存裝置劃分成層次結構——CPU暫存器在頂部,接着是多層的硬體快取記憶體記憶體、DRAM主記憶體和磁碟記憶體。在層次模型中,位於更高層的儲存裝置比低層的儲存裝置要更快,單位位元造價也更高。層次結構中較高層次的儲存裝置可以作爲較低層次裝置的快取記憶體。通過理解和運用這種儲存層次結構的知識,程式設計師可以優化C程式的效能
操作系統內核是應用程式和硬體之間的媒介。它提供三個基本的抽象:
1)檔案是對I/O裝置的抽象
2)虛擬記憶體是對主記憶體和磁碟的抽象
3)進程是處理器、主記憶體和I/O裝置的抽象
最後,網路提供了計算機系統之間通訊的手段。從特殊系統的角度來看,網路就是一種I/O裝置