1、基本概念
計算機系統是由硬件和系統軟件組成的,它們共同工作來運行應用程序,盡管系統的具體方式隨着時間不斷地變化,但是系統內在的概念卻沒有改變,所有的計算機系統都有相似的硬件和軟件組成,它們又執行着相似的功能。
2、信息就是位+上下文
一個最簡單的應用程序hello,源文件名為hello.c,內容如下所示:
#include <stdio.h> int main() { printf("hello, world\n"); return 0; }
hello程序的生命周期是從一個源程序開始的,程序員通過編輯器創建並保存的文本文件hello.c,源程序實際上就是一個由值0和1組成的位序列,8個位被組織成一組,稱為字節,每個字節表示源文件的文本字符,大部分的現代計算機系統都使用ASCII標准來表示文本字符,像hello.c這樣由ASCII字符構成的文件叫做文本文件,所有其它文件稱為二進制文件。
系統中所有的信息,包括磁盤文件、內存中的程序、內存中存放的用戶數據以及網絡上傳輸的數據,都是由一串比特位表示的,區分不同數據對象的唯一方法是讀到這些數據的上下文,比如在不同的上下文中,一個同樣的字節序列可能表示一個整數、浮點數、字符串或者機器指令等。
3、程序被其它程序翻譯成不同的格式
hello程序的生命周期是從一個高級C語言程序開始的,這種形式能夠被人容易讀懂,然而,為了在系統上運行hello.c程序,每條源文件的語句都必須被其它程序轉化為一系列的低級機器語言指令,然后,這些指令按照一種稱為可執行目標程序的格式進行打包,並以二進制磁盤文件的形式存放起來,目標程序也稱為可執行目標文件。
在Linux系統上,從源文件到目標文件的轉化是由編譯器驅動程序完成的:
# gcc -o hello hello.c
上述命令中,GCC編譯器驅動程序讀取源文件hello.c,並把它翻譯成一個可執行目標文件hello,整個翻譯過程可以分為四個階段完成,分別為預處理、編譯、匯編、鏈接,執行這四個階段的程序(預處理器、編譯器、匯編器、鏈接器)共同構成了編譯系統,整個編譯階段如下圖所示:
- 預處理階段:預處理器(cpp)根據以字符#開頭的命令,修改原始的C程序,例如hello.c中的第一行的#include <stdio.h>命令告訴預處理器讀取系統頭文件stdio.h的內容,並把它直接插入程序文本中,結果就得到了另一個C程序,通常是以.i作為文件拓展名;
- 編譯階段:編譯器(ccl)將文本文件hello.i翻譯成文件文件hello.s,它包含一個匯編語言程序,以一種文本格式描述低級機器語言指令;
- 匯編階段:接下來,匯編器(as)將hello.s翻譯成機器語言指令,把這些指令打包成一種叫做可重定位目標程序的格式,並將結果保存在目標文件hello.o中,hello.o文件是一個二進制文件;
- 鏈接階段:hello程序調用了printf函數,它是每個C編譯器都提供的標准C庫中的一個函數,存在於其它的目標文件中,而這個文件必須以某種方式合並到hello.o程序中,鏈接器(ld)就是負責處理這種合並,結果就得到hello可執行目標文件,該程序可以被加載到內存中,由系統執行。
4、處理器讀並解釋存儲在內存中的指令
hello.c源文件已經被編譯系統翻譯成了可執行目標文件hello,並被存放在磁盤上,如果想在Linux系統上運行該可執行文件,可以將它的文件名輸入到稱為shell的應用程序中:
# ./hello
hello, world
#
shell是一個命令行解釋器,它輸出一個提示符,並等待輸入一個命令行,然后執行這個命令,如果該命令行的第一個單詞不是一個內置的shell命令,那么shell就會假設這是一個可執行文件的名字,它將加載並運行這個文件。
(4.1)系統的硬件組成
一個典型系統的硬件組織,如下圖所示:
(4.1.1)總線
貫穿整個系統的是一組電子管道,稱作總線,它攜帶信息字節並負責在各個部件間傳遞,通常總線被設計成傳送定長的字節塊,也就是字(word),字中的字節數(字長)是一個基本的系統參數,各個系統中都不僅相同,現在大多數機器字長一般是4個字節(32位)或者8個字節(64位)。
(4.1.2)I/O設備
I/O(輸入/輸出)設備是系統與外部世界的聯系通道,在上圖的典型系統包括了四個I/O設備:作為用戶輸入的鍵盤和鼠標,作為用戶輸出的顯示器,以及用於長期存儲數據和程序的磁盤驅動器(磁盤),每個I/O設備都通過一個控制器或適配器與I/O總線相連,控制器和適配器的功能是在I/O總線和I/O設備之間傳遞信息。
(4.1.3)主存
主存是一個臨時存儲設備,在處理器執行程序時,用來存放程序和程序處理的數據,從物理上來說,主存是由一組動態隨機存取存儲器(DRAM)芯片組成的,從邏輯上來說,存儲器是一個線性的字節數組,每個字節都有唯一的地址(數組索引),這些地址是從零開始的,一般來說,組成程序的每條機器指令都由不同數量的字節構成,與C程序變量相應的數據項的大小是根據數據類型變化的。
(4.1.4)處理器
中央處理單元(CPU),簡稱處理器,是解釋存儲在主存中指令的引擎,處理器的核心是一個大小為一個字的存儲設備(寄存器),也稱為程序計數器(PC),在任何時刻,PC都指向主存中的某條機器語言指令(含有該條指令的地址),從系統通電開始,直到系統斷電,處理器一直在不斷地執行程序計數器指向的指令,再更新程序計數器,讓其指向下一條要指向的指令,處理器看上去是按照一個非常簡單的指令執行模型來操作的,這個模型是由指令集架構決定的,在這個模型中,指令按照嚴格的順序執行,而執行一條指令包含一系列的步驟,處理器從程序計數器指向的內存處讀取指令,解釋指令中的位,執行該指令指示的簡單操作,然后更新PC,使其指向下一條指令。
CPU在指令的要求下可能會執行以下操作:
- 加載:從主存復制一個字節或者一個字到寄存器,以覆蓋寄存器原來的內容;
- 存儲:從寄存器復制一個字節或者一個字到主存的某個位置,以覆蓋這個位置上原來的內容;
- 操作:把兩個寄存器的內容復制到算術/邏輯單元(ALU),ALU對這兩個字做算術運算,並將結果存放到一個寄存器中,以覆蓋寄存器中原來的內容;
- 跳轉:從指令本身中抽取一個字,並將這個字復制到程序計數器(PC)中,以覆蓋PC中原來的值。
(4.2)運行hello程序
系統是如何運行hello程序的呢?初始時,shell程序執行它的指令,等待我們輸入一個命令,當用戶通過鍵盤上輸入字符串"./hello"后,shell程序將字符逐一讀入寄存器,再把它存放到內存中,如下圖所示:
當用戶在鍵盤上按下回車鍵后,shell程序知道已經結束了命令的輸入,然后執行一系列指令來加載可執行目標文件hello,這些指令將hello目標文件中的數據和代碼從磁盤復制到主存,利用直接存儲器存取(DMA)技術,數據可以不通過處理器而直接從磁盤到達主存,如下圖所示:
一旦目標文件hello中的代碼和數據被加載到主存,處理器就開始執行hello程序中main函數的機器語言指令,這些指令將"hello, world\n"字符串中的字節從主存復制到寄存器文件,再從寄存器文件中復制到顯示設備,最終顯示輸出到用戶屏幕上,如下圖所示:
5、高速緩存的重要性
hello程序的運行實例揭示了一個重要的問題,即系統花費了大量的時間把信息從一個地方搬移到另一個地方,hello程序的機器指令最初是存放到磁盤上,當程序加載時,它們被復制到主存,當處理器運行程序時,指令又從主存復制到處理器,相似地,數據串"hello, world\n"開始是存到磁盤上,然后被復制到主存中,最后從主存上復制到顯示設備,系統設計者的一個主要目標就是使這些復制操作盡可能快地完成。
一個典型的寄存器文件只存儲幾百字節的信息,而主存里可以存放幾十億字節,處理器從寄存器文件中讀數據比從主存中讀取幾乎要快100倍,隨着半導體技術的進步,處理器與主存之間的差距還在持續增大,針對這種處理器與主存之間的差異,系統設計者采用了更小更快的存儲設備,稱為高速緩存存儲器(cache memory,簡稱為cache或高速緩存),作為暫時的集結區域,存放處理器近期可能會需要的信息,一個典型系統中的高速緩存存儲器如下圖所示:
位於處理器芯片上的L1高速緩存的容量可達數萬字節,訪問速度幾乎和訪問寄存器文件一樣快,系統可以獲得一個很大的存儲器,同時訪問速度也很快,原因是利用了高速緩存的局部性原理,即程序具有訪問局部區域里的數據和代碼的趨勢,通過讓高速緩存里存放可能經常訪問的數據,大部分的內存操作都能在快速的高速緩存中完成。
6、存儲設備的層次結構
在處理器和一個較大較慢的設備(例如主存)之間插入一個更小更快的存儲設備(例如高速緩存)的想法已經成為一個普遍的觀念,實際上,每個計算機系統中的存儲設備都被組織成了一個存儲器層次結構,如下圖所示:
在這個層次結構中,從上至下,設備的訪問速度越來越慢,容量越來越大,並且每字節的造價也越來越便宜。
7、操作系統管理硬件
當shell加載並運行hello程序時,以及hello程序輸出自己的消息時,shell和hello程序都沒有直接訪問鍵盤、顯示器、磁盤或者主存,取而代之的是,它們依靠操作系統提供的服務,用戶可以把操作系統看成是應用程序和硬件之間插入的一層軟件,所有應用程序對硬件的操作嘗試都必須通過操作系統,計算機系統的分層如下圖所示:
操作系統有兩個基本功能:
- 防止硬件被失控的應用程序濫用;
- 向應用程序提供簡單一致的機制來控制復雜而又通常不相同的低級硬件設備。
操作系統通過幾個基本的抽象概念(進程、虛擬內存和文件)來實現這兩個基本功能,如下圖所示:
文件是對設備的抽象顯示,虛擬內存是對主存和磁盤I/O設備的抽象表示,進程則是對處理器、主存和I/O設備的抽象表示。
(7.1)進程
像hello這樣的程序在現代計算機系統上運行時,操作系統會提供一種假象,就好像系統上只有這個程序在運行,程序看上去是獨占地使用處理器、主存和I/O設備的,處理器看上去就像不間斷地一條接一條地執行程序中的指令,即該程序的代碼和數據是系統內存中唯一的對象。
進程是操作系統對一個正在運行的程序的一種抽象,在一個系統上可以同時運行多個進程,而每個進程都好像在獨占地使用硬件資源,而並發運行,則是說一個進程的指令和另一個進程的指令是交錯執行的,在大多數系統中,需要運行的進程數是多於可以運行它們的CPU個數的,單處理器在一個時刻只能執行一個程序,而先進的多核處理器同時能夠執行多個程序,無論是在單核還是多核系統中,一個CPU看上去都像是在並發地執行多個進程,這是通過處理器在進程間切換來實現的,操作系統實現這種交錯執行的機制稱為上下文切換。
操作系統會保持跟蹤進程運行所需要的全部狀態信息,這種狀態,也就是上下文,包括許多信息,例如PC和寄存器文件的當前值,以及主存的內容,在任何一個時刻,單處理器系統都只能執行一個進程的代碼,當操作系統決定要把控制權從當前進程轉移到某個新進程時,就會進行上下文切換,即保持當前進程的上下文,恢復新進程的上下文,然后將控制權傳遞到新進程,新的進程就會從上次停止的地方開始執行。
hello進程和shell進程的上下文切換示意圖如下:
從一個進程到另一個進程的轉換是由操作系統內核(kernel)管理的,內核是操作系統代碼常駐內存的部分,當應用程序需要操作系統的某些操作時,比如讀寫文件,它就會執行一條特殊的系統調用(system call)指令,將控制權傳遞給內核,然后內核執行被請求的操作並返回應用程序,內核不是一個獨立的進程,它是系統管理全部進程所用代碼和數據結構的集合。
(7.2)線程
盡管通常我們認為一個進程只有單一的控制流,但是在現代系統中,一個進程實際上可以由多個稱為線程的執行單元組成,每個線程都運行在進程的上下文中,並共享同樣的代碼和全局數據,由於網絡服務器中對並行處理的需求,線程成為越來越重要的編程模型,因為多線程之間比多進程之間更容易共享數據,線程一般來說都比進程更高效,當有多處理器可用的時候,多線程也是一種使得程序可以運行得更快的方法。
(7.3)虛擬內存
虛擬內存是一個抽象概念,它為每個進程提供了一個假象,即每個進程都在獨占地使用主存,每個進程看到的內存都是一致的,稱為虛擬地址空間,下圖所顯示的是Linux進程的虛擬地址空間:
在Linux系統中,地址空間最上面的區域是保留給操作系統中的代碼和數據的,這對所有進程來說都是一致的,地址空間的底部區域存放用戶進程定義的代碼和數據,上圖中地址是從下往上增大的,每個進程看到的虛擬地址空間由大量准確定義的區構成,每個區都有專門的功能,從最低的地址開始,逐步向上介紹:
- 程序代碼和數據:對所有的進程來說,代碼是從同一固定地址開始,緊接着的是和C全局變量相對應的數據位置,代碼和數據區是直接按照可執行目標文件的內容初始化的;
- 堆:代碼和數據區后緊隨着的是運行時堆,代碼和數據區在進程一開始運行時就被指定了大小,與此不同,當調用像malloc和free這樣的C標准庫函數時,堆可以在運行時動態地拓展和收縮;
- 共享庫:大約在地址空間的中間部分是一塊用來存放像C標准庫和數學庫這樣的共享庫的代碼和數據的區域;
- 棧:位於用戶虛擬地址空間頂部的用戶棧,編譯器用它來實現函數調用,和堆一樣,用戶棧在程序執行期間可以動態地拓展和收縮,特別地,每次調用一個函數時,棧就會增長,從一個函數返回時,棧就會收縮;
- 內核虛擬內存:地址空間頂部是為內核所保留的,不允許應用程序讀寫這個區域的內容或者直接調用內核代碼定義的函數,相反,它們必須調用內核來執行這些操作。
虛擬內存的運作需要硬件和操作系統軟件之間精密復雜的交互,包括對處理器生成的每個地址的硬件翻譯,基本思想是把一個進程虛擬內存的內容存儲在磁盤上,然后用主存作為磁盤的高速緩存。
(7.4)文件
文件就是字節序列,每個I/O設備,包括磁盤、鍵盤、顯示器,甚至網絡等,都可以看成是文件,系統中的所有輸入輸出都是通過使用一小組稱為Unix I/O的系統函數調用讀寫文件來實現的。
8、計算機系統之間利用網絡通信
現代計算機系統經常通過網絡和其它系統連接到一起,從一個單獨的系統來看,網絡可視為一個I/O設備,如下圖所示:
當系統從主存復制一串字節到網絡適配器時,數據流經過網絡到另一台機器,而不是比如說到達本地磁盤驅動器,相似地,系統可以讀取從其它機器發送來的數據,並把數據復制到自己的主存,隨着Internet這樣的全球網絡的出現,從一台主機復制信息到另外一台主機已經成為計算機系統最重要的用途之一,比如,像電子郵件、萬維網、FTP等應用都是基於網絡復制信息的功能。