計算機系統(tǒng)漫游
- 代碼從文本到可執(zhí)行文件的過程(c語言示例):
- 預(yù)處理階段,處理 #inlcude <stadio.h>, #define MAX 100
- 編譯階段:將文本編譯成匯編程序,hello.s
- 匯編階段:匯編器將上一步的程序翻譯成機器指令。hello.o
- 鏈接階段就:hello 中調(diào)用的printf函數(shù),而函數(shù)存在一個printf.o 單獨的編譯完成文件,需要以某種方式合并到hello.o 中。
-
系統(tǒng)的硬件組成
- 總線
- I/O設(shè)備
- 主存
- 處理器, 指令集合: 加載(復(fù)制內(nèi)容到寄存器), 存儲(從寄存器到存儲),操作(加減乘除等計算), 跳轉(zhuǎn)(覆蓋程序計數(shù)器PC的數(shù)值,執(zhí)行代碼跳轉(zhuǎn))
- 緩存, 高速緩存
-
操作系統(tǒng)如何管理硬件
- 任何的硬件通過操作系統(tǒng)提供服務(wù), 所有應(yīng)用程序都是建立在操作系統(tǒng)之上的。
- OS 的基本功能: 1. 防止硬件被濫用, 2. 提供一套簡單一致的機制來控制復(fù)雜度而又大相徑庭的低級硬件設(shè)備。
- OS 的抽象: 文件 -> IO, 虛擬存儲器 -> 主存+磁盤, 進程 -> 處理器,主存,IO設(shè)備的抽象
-
抽象
- 進程: 計算機科學(xué)中最重要并且成功的概念。
提供一種假象, 好像系統(tǒng)上只有這個程序在運行,看上去只有這個程序在使用處理器、主存、和IO設(shè)備. 這是通過處理器在進程間切換來實現(xiàn)的。 操作系統(tǒng)實現(xiàn)這種交錯執(zhí)行的機制為 上下文切換, 實現(xiàn)進程這個抽象概念需要低級硬件和操作系統(tǒng)軟件之間的緊密合作。
- 線程:每個線程都運行在進程的上下文中,并共享同樣的代碼和全局?jǐn)?shù)據(jù),服務(wù)器對于并行處理的需求,導(dǎo)致線程編程成為越來越重要的編程模型, 一般來說,多線程之間更容易共享數(shù)據(jù),也比進程更輕量。
- 虛擬存儲器: 為進程提供了一個抽象、一致的存儲空間,稱為虛擬地址空間。包括: 程序代碼和數(shù)據(jù),堆,共享庫,棧,內(nèi)核虛擬存儲器。
- 文件: 字節(jié)序列。包括磁盤,鍵盤,顯示器,網(wǎng)絡(luò),都可以視為文件。
- 并行跟并發(fā)的區(qū)分: 并發(fā): 好像 同時具有多個活動在系統(tǒng)中。 并行:真正的并行。
- 抽象: 抽象的使用是計算機科學(xué)中最為重要的概念之一,因為,程序員無需了解它內(nèi)部的工作變可以使用這些代碼。在處理器中,指令集結(jié)構(gòu)提供了對實際處理器硬件的抽象。機器代碼程序表現(xiàn)的好像是運行在一個一次執(zhí)行一條指令的處理器上。底層的硬件幣抽象描述的要復(fù)雜精細(xì)的多,它并行的執(zhí)行多條指令,但又總是與那個簡單有序的模型保持一致。
計算機系統(tǒng)中一個重大的主題就是 提供不同層次的抽象表示,來 隱藏實際實現(xiàn)的復(fù)雜性
信息的表示和處理
因為只是介紹了二進制、無符號數(shù)、有符號數(shù)、以及小數(shù)的表示方法, 計算機教程中都有介紹,所以省略不寫了。只是簡單的摘錄重要的。
- 在相同長度的無符號和有符號整數(shù)之間 進行強制類型轉(zhuǎn)換時候,大多數(shù)C語言實現(xiàn)遵循 原則是 底層的位模式不變。而是改變位的解釋方法。
- 編碼的存儲長度有限。可能導(dǎo)致數(shù)值溢出。需要非常注意。
- 整數(shù)和浮點數(shù)的表示方法,有所區(qū)別,導(dǎo)致, 整數(shù)可以進行移位、結(jié)合等優(yōu)化方法,但是浮點數(shù)則不行,如 x * y * z 不等于 y * z * x 需要注意
程序的機器級別表示
精通細(xì)節(jié)是理解更深和更基本概念的先決條件, 所以魔鬼隱藏在細(xì)節(jié)之中。*
- 機器代碼的產(chǎn)生過程
機器代碼, 用字節(jié)序列編碼低級的操作,包括處理數(shù)據(jù)、管理存儲器、讀寫存儲設(shè)備上的數(shù)據(jù)、以及利用網(wǎng)絡(luò)通信。 編譯器機基于編程語言的原則、目標(biāo)機器的指令和操作系統(tǒng)遵循的原則, 經(jīng)過一系列的階段產(chǎn)生機器代碼。GCC C語言編譯器以匯編代碼的形式產(chǎn)生輸出,然后調(diào)用 匯編器和鏈接器從而根據(jù)匯編代碼生成可執(zhí)行的機器代碼。
-
抽象:
- 指令集體系結(jié)構(gòu)(ISA): 屏蔽了處理器的硬件實現(xiàn),將指令的執(zhí)行描述為,簡單的順序執(zhí)行(處理器的硬件遠(yuǎn)遠(yuǎn)比描述的精細(xì)復(fù)雜)
- 存儲抽象: 抽象成一個大的字節(jié)數(shù)組,存儲器的實現(xiàn)是,將多層硬件存儲器和操作系統(tǒng)軟件的結(jié)合
-
主要內(nèi)容:
- 了解C語言中的控制結(jié)構(gòu), 比如if while switch 語句的實現(xiàn)方法。
- 過程的實現(xiàn), 包括程序如何維護一個運行棧來支持過程間數(shù)據(jù)和控制的傳遞以及局部變量的存儲
- 數(shù)組、結(jié)構(gòu)、聯(lián)合這樣的數(shù)據(jù)結(jié)構(gòu)的實現(xiàn)方法
-
指令集:
- 指令操作數(shù)
- 源數(shù)據(jù): 常數(shù)、寄存器、存儲器
- 類型: 立即數(shù)(常數(shù))、寄存器、存儲器
- C 語言的指針就是地址,間接引用指針就是將該指針放在一個寄存器中,然后在存儲器引用中,使用這個寄存器, 局部變量通常保存在寄存器中。
- 數(shù)據(jù)傳送指令: mov
- 算數(shù)邏輯操作: add, sub, imul, sal, shl, leal, imull, mull, idivl, divl
- 控制:條件碼,跳轉(zhuǎn)指令,test, sete, sets, setg etc, cmp, jmp, 條件碼一般使用比較、算數(shù)、直接設(shè)定三種方式, 跳轉(zhuǎn)指令則利用,條件碼來進行跳轉(zhuǎn)或者間接跳轉(zhuǎn)
- 棧: push, pop
-
C 語言 控制結(jié)構(gòu) 匯編表示
-
while, for 一般是先將 for 循環(huán)轉(zhuǎn)變?yōu)榈葍r的 while 循環(huán),while 循環(huán) 套用固定的匯編代碼 模式。
do body-statement while(test-expr) loop: body-statement t = test-expr; if(t) goto loop; done: while(test-expr) body-statement t = test-expr; if(!t) goto done; loop: body-statement t = test-expr; if(t) goto loop; done:
switch 的實現(xiàn)
使用跳轉(zhuǎn)表 實現(xiàn),來達(dá)到 執(zhí)行時間跟 開關(guān)數(shù)量無關(guān)。條件傳送指令
因為現(xiàn)代處理器的流水線設(shè)計,導(dǎo)致在條件判斷時候,才能確定下一條執(zhí)行指令的位置,而導(dǎo)致按照順序執(zhí)行 準(zhǔn)備的代碼可能被拋棄,而對應(yīng)的準(zhǔn)備工作則變?yōu)榱死速M。 而 條件傳送 指令先計算出條件操作的兩種結(jié)果,然后根據(jù)條件來選擇滿足的結(jié)果。從而避免了 因為跳轉(zhuǎn)指令 帶來的資源浪費。另一方面現(xiàn)代處理器都采用了 分支預(yù)測 邏輯,來試圖猜測每條跳轉(zhuǎn)指令是否被執(zhí)行。(處理器設(shè)計試圖達(dá)到 90%的正確率),正確的預(yù)測可以沒有代價,然而額錯誤的預(yù)測則會帶來嚴(yán)重恩懲罰,大約 20-40 的時鐘周期的浪費,導(dǎo)致性能嚴(yán)重下降。
舉例: 例如簡單 三目運算符, x > y ? x+y : x-y, 當(dāng)兩個表達(dá)式具有副作用的時候則不能應(yīng)用。
-
-
結(jié)構(gòu)實現(xiàn):
- 數(shù)組分配和訪問: 基本實現(xiàn)為, 在存儲器中分配一個連續(xù)的 T A[N], L * N 字節(jié)的連續(xù)大小的空間。 L為T類型的字節(jié)大小。而C語言中數(shù)字指針的實現(xiàn)(ptr ++ )則實現(xiàn)為單純的 地址運算。嵌套數(shù)組 則以 行優(yōu)先、列優(yōu)先 的方式進行展開。
- Struct 的實現(xiàn), 變量為 首地址 + 偏移量。
- 數(shù)據(jù)對齊: 計算機系統(tǒng)對 基本數(shù)據(jù)結(jié)構(gòu)類型的大小做了限制,8的倍數(shù)等。這種 對齊限制,簡化了 處理器和存儲系統(tǒng)之間的硬件設(shè)計。
-
過程實現(xiàn)
過程調(diào)用 包括數(shù)據(jù)傳遞(過程參數(shù)、返回值)、控制跳轉(zhuǎn)。在進入是為過程的局部變量分配空間,并在退出時候釋放這些空間。-
簡單指令:
轉(zhuǎn)移控制: call, leave, ret.
call: 將返回地址入棧(call之后的下一條命令的地址) 2. 跳轉(zhuǎn)到被調(diào)用的過程處。
ret: 從棧中彈出地址,并跳轉(zhuǎn)到此位置。需要將棧指針指向call指令存儲的放回地址的位置(需要自己控制)
leave: ??? movl %ebp, %esp; popl %ebp 為ret 返回做好準(zhǔn)備工作如果使用整數(shù),指針作為返回值的話,可以使用%eax傳遞。(其他的呢?)
寄存器使用: 寄存器是計算中公用的資源。為了保證 被調(diào)用者不會覆蓋調(diào)用者時候用的寄存器的數(shù)值。需要遵守規(guī)范。
%eax, %edx, %ecx 調(diào)用者保存寄存器, %ebx, %esi, %edi 被調(diào)用者保存寄存器。 需要調(diào)用者與被調(diào)用者配合來保護共享的寄存器內(nèi)容。 實現(xiàn)過程:
函數(shù)調(diào)用過程的兩個寄存器 %ebp(幀指針), %esp(棧指針) 幀指針保存當(dāng)前過程的最高位置,%esp則向下增長, 用于分配必要的地址空間,調(diào)用函數(shù)參數(shù)等。 在調(diào)用時, 首先壓入調(diào)用參數(shù),返回地址, 壓入%ebp, 調(diào)用后,將 %ebp 重置為當(dāng)前的%esp, 標(biāo)記確定當(dāng)前的 函數(shù)的最高地址。返回時, movl %ebp, %esp; popl %ebp; ret; 恢復(fù)調(diào)用函數(shù)之前的樣子。天生的具有遞歸屬性。
-
-
什么時候需要幀指針:
- 局部變量太多,不能都存在在寄存器中
- 有些局部變量是數(shù)組或者結(jié)構(gòu)
- 函數(shù)用取地址操作符&,來計算一個局部變量的地址
- 函數(shù)必須將棧上的某些參數(shù)傳遞到另一個函數(shù)
- 在修改一個被調(diào)用者保存寄存器之前,需要保存它的狀態(tài)
-
X86-64 中對于過程的 一些具體優(yōu)化:
- 參數(shù)通過寄存器傳遞到過程,而不是在棧上,消除了在棧上存儲和檢索值的開銷
- call 指令將一個64位的返回地址存儲在棧上
- 許多函數(shù)不需要棧幀,只有那些不能將所有局部變量存儲在寄存器中的函數(shù)才需要在棧上分配空間
- 沒有幀指針,作為替代,對棧位置的引用相對于棧指針。
- C 語言 指針
- 每個指針都對應(yīng)一個具體的類型: 指針類型不是機器代碼中的一部分,C語言提供的一種抽象,地址運算,來避免尋址錯誤。
- 每個指針都有一個值, 這個值是某個指定類型對象的地址。
- 指針用& 運算符創(chuàng)建
- 運算符 * 用于指針的 間接引用
- 數(shù)組與指針緊密關(guān)聯(lián)
- 指針類型轉(zhuǎn)換: 只改變類型,而不是值
- 指針可以指向函數(shù),之函數(shù)機器代碼中的 第一條 指令地址。將
C語言跟匯編指令 的差別很大,在匯編語言中,各種數(shù)據(jù)類型之間的差距很小,程序以指令序列來表示。每條指令是一個單獨的操作。編譯器必須提供多條指令來產(chǎn)生和操作各種數(shù)據(jù)結(jié)構(gòu),來實現(xiàn)像條件、循環(huán)、和過程這樣的控制結(jié)構(gòu)、抽象機制。
處理器體系結(jié)構(gòu)
一個處理器支持的指令和指令的字節(jié)編碼成為它的 指令集體系結(jié)構(gòu) (ISA)ISA的編輯器編寫者和處理器設(shè)計人員之間提供了一個概念抽象層。現(xiàn)代處理器的實際工作方式可能跟ISA 隱含的計算模型大相徑庭
- 目的
- 設(shè)計 Y86 處理器,首先是基于順序的、功能正確的處理器設(shè)計
- 創(chuàng)建一個流水線化的處理。處理器可以同時執(zhí)行五條指令的不同階段
-
Y86 指令編碼,
-
具體的描述了, 指令的機器字節(jié)表示。字節(jié)編碼必須有唯一 的解釋,任何一個字節(jié)序列要么是一個唯一的指令序列的編碼,要么就不是一個合法的字節(jié)序列。 每條指令的第一個字節(jié)碼都有唯一的代碼和功能組合。給定這個字節(jié),我們就可以決定所有的其他的附加字節(jié)的長度和含義。這個性質(zhì)確保處理器可以無二義性的執(zhí)行目標(biāo)代碼程序。反匯編程序的翻譯解釋,就是如此。
command_sample.png
RISC(精簡指令集) 和 CISC(復(fù)雜指令集):簡單的指令集形式可以產(chǎn)生更搞笑的代碼, 實際上,許多加到指令集中的高級指令很難被編輯器產(chǎn)生,所以也很少被利用。90年代,沉淪逐漸平息,無論是淡出的RISC,還是單純的CISC都不如結(jié)合兩者思想精華的設(shè)計。今天的RISC機器的指令表,已經(jīng)有數(shù)百條指令,幾乎與 精簡指令集機器 的名字不相符了。那種將實現(xiàn)細(xì)節(jié)暴露給機器級程序的思想已經(jīng)被證明是目光短淺的。(RISC做過這樣的事情?)
-
-
Y86 的實現(xiàn):
- 拆分指令為階段:
取指: 取指階段從存儲器讀取指令字節(jié),地址為程序計數(shù)器的值,從指令中 抽取出 指令的,icode, ifun, 操作的字符, rA, rB, 常數(shù)
譯碼: 譯碼階段從寄存器文件讀入最多兩個操作數(shù),得到值, valA, valB等。
執(zhí)行: ALU運算,
訪存: 可以將數(shù)據(jù)寫入存儲器,或者從存儲器讀出數(shù)據(jù)。
寫回: 將結(jié)果寫回到寄存器文件
-
更新PC: 將PC設(shè)置成為下一條指令的地址
在設(shè)計硬件時候, 一個非常簡單而一致的結(jié)構(gòu)是非常重要的。降低復(fù)雜度的一種方法是,讓不同的指令共享盡量多的硬件,因為在硬件上復(fù)制邏輯快比用軟件來處理困難的多。
command_flow.png
- 硬件結(jié)構(gòu) (SEQ)
組合電路從本質(zhì)上講,不存儲任何信息,他們只是簡單的響應(yīng)輸入信號。 產(chǎn)生等于輸入的某個函數(shù)的輸出。
存儲設(shè)備: 時鐘寄存器: 存儲單個位,或字,時鐘信號控制寄存器加載輸入值
-
隨進訪問寄存器: 存儲多個字,用地址來選擇該讀或者寫。應(yīng)用有:寄存器文件, %eax etc
register.png
-
流水線通用原理:
流水線化的一個重要特性就是增加了系統(tǒng)的吞吐量(單位時間內(nèi)服務(wù)的顧客的總數(shù)),代價是可能稍微的增加了延遲(服務(wù)一個用戶所需的時間)。舉例來說, 一個客戶需要沙拉,在一個非流水線化的服務(wù)中,非常簡單,只需要在沙拉階段停留。但是在一個流水線化的服務(wù)中,則需要無謂的其他階段的等待。
流水線的局限性: 運行時鐘的速率是由最慢的階段的延遲限制的。所以對于硬件設(shè)計者來說,將系統(tǒng)計算設(shè)計成 具有相同延遲的階段 是一個嚴(yán)峻的挑戰(zhàn)。
-
預(yù)測下一個PC:
- 流水線的設(shè)計目的是 每個時鐘周期 都發(fā)射一條指令,也就說每個時鐘周期都有一條新的指令進入執(zhí)行階段并最終完成。
- 要做到這一點就 需要在取出當(dāng)前指令之后,馬上確認(rèn)下一條指令。
- 如果取出的指令是條件分支指令,要到幾個周期之后,才能確定是否要選擇分支。(jxx)類似的是ret
- 分支預(yù)測和處理預(yù)測錯誤
-
流水線冒險: 將流水線引入一個帶反饋的系統(tǒng),當(dāng)相鄰指令間存在相關(guān)時會導(dǎo)致出現(xiàn)問題, 這些相關(guān)可能會導(dǎo)致流水線產(chǎn)生計算錯誤,稱為冒險。
- 數(shù)據(jù)相關(guān): 下一條指令會用到這一條指令計算出的結(jié)果。(數(shù)據(jù)冒險)
-
控制相關(guān): 一條指令要確定下一條指令的位置。例如執(zhí)行條件跳轉(zhuǎn)。(控制冒險)
flow_command.png
-
控制邏輯:
- 處理ret
- 加載使用冒險
- 預(yù)測錯誤的分支: 在分支邏輯發(fā)現(xiàn)不應(yīng)該選擇分支之前,分支目標(biāo)處的幾條指令已經(jīng)進入到了流水線中,必須從流水線中舍棄這些操作
- 當(dāng)一條指令發(fā)生異常,需要禁止后面的指令更新 程序員可見狀態(tài),并且在異常指令到達(dá)寫回階段時,停止執(zhí)行。
-
通用的冒險簡單解決辦法:
- 暫停來避免冒險: 讓一條指令停留在譯碼階段,直到他需要的操作數(shù)的指令通過了寫回階段,這樣來避免數(shù)據(jù)冒險。 雖然這一機制實現(xiàn)起來相當(dāng)簡單,但是得到的性能卻并不好,一條指令更新一個寄存器,緊隨其后的指令使用被更新過的寄存器的事情非常普遍,為了保證正確的執(zhí)行,在其中不斷的加入nop,導(dǎo)致流水線暫停長達(dá)三個周期,這嚴(yán)重的降低了 整體的吞度量。
- 轉(zhuǎn)發(fā)來避免冒險: 將結(jié)果直接從一個流水線階段傳到較早階段的技術(shù)稱為 數(shù)據(jù)轉(zhuǎn)發(fā), 也就是較早的反饋到需要的階段。比如 譯碼階段。
- 控制邏輯的特殊處理: 控制邏輯的優(yōu)化,有些繁雜,需要結(jié)合 時鐘周期、代碼執(zhí)行階段來 具體分析。
-
未考慮的方面:
- 多周期指令,一些復(fù)雜的操作 例如乘法、除法。一種方法是 同步到特殊單元來進行處理,流水線繼續(xù)處理其他指令(并發(fā)執(zhí)行)。但是不同的單元操作需要是同步的,以避免 出錯。
- 存儲器接口: 涉及到存儲器的命令,具體來說是 是以存儲器位置的虛擬地址來引用他們,這涉及到, 地址翻譯(將虛擬地址翻譯成物理地址),然后對存儲器進行操作。在有些情況,被引用的存儲器位置儲存在硬盤上,硬件會產(chǎn)生一個 缺頁 異常信號,這個異常會導(dǎo)致處理器調(diào)用操作系統(tǒng)的缺頁代碼,然后訪問磁盤數(shù)據(jù)到高速緩存中,訪問 磁盤就需要數(shù)百萬個 時鐘周期。所以其導(dǎo)致的性能下降是非常嚴(yán)重的。
總結(jié)
- ISA指令集結(jié)構(gòu),提供了代碼到處理器具體實現(xiàn)的一層抽象。也就是一條指令執(zhí)行完了,下一條指令執(zhí)行。
- 流水線化 通過讓不同的階段并行操作,改進了系統(tǒng)的吞度量性能,然而我們必須小心,以便流水線化 執(zhí)行與程序的順序執(zhí)行得到相同的程序行為。
優(yōu)化程序性能
-
代碼標(biāo)準(zhǔn):
- 清晰簡潔的代碼,能夠很容的理解代碼。
- 運行的快(比如實時處理視頻幀,網(wǎng)絡(luò)包)
-
如何編寫高效率的代碼:
組合正確的數(shù)據(jù)結(jié)構(gòu)和算法,
需要編寫出編譯器能夠有效優(yōu)化以轉(zhuǎn)換成高效可執(zhí)行代碼的源代碼。對于第二點,理解編譯器的能力和局限性是非常重要的
-
并行計算
在算法級別上, 幾分鐘就能編寫一個插入排序,而搞笑的是排序算法程序可能需要一天或更長的時間 來實現(xiàn)和優(yōu)化,在代碼級上, 許多低級別的優(yōu)化往往會降低程序的可讀性和模塊性,是的程序容易出錯,并且難以修改和擴展,對于在性能重要的環(huán)境中反復(fù)執(zhí)行的代碼,進行廣泛的優(yōu)化比較合適。一個挑戰(zhàn)就是盡管做了廣泛的優(yōu)化,但還是要維護代碼一定程度的簡潔和可讀性。一個很有用的策略是,只寫到編譯器能夠產(chǎn)生有效代碼的程度就好了。
-
程序優(yōu)化:
- 消除不必要的內(nèi)容,讓代碼盡可能有效的執(zhí)行期望的工作。這包括不必要的函數(shù)調(diào)用,條件測試,存儲器引用。并且這些是不依賴于目標(biāo)環(huán)境的(思想通用)
- 利用處理器提供的指令級表示進行優(yōu)化。
-
編譯器的局限性
int f(); int func1(){ return f() + f() + f() + f(); } int func2(){ return f() * 4; } int counter = 0; in func1(){ return counter ++; }
func1 函數(shù)具有副作用,他修改了程序狀態(tài)的一部分,改變了整體程序的行為,大多數(shù)編輯器不會試圖判斷一個函數(shù)是否具有副作用,所以,編譯器會保持函數(shù)調(diào)用不變, 并不會按照人們預(yù)期的 進行函數(shù)調(diào)用優(yōu)化
-
另一個示例
消除循環(huán)的低效率代碼; 例如for(i = 0; i< strlen(s); i++) 中,strlen的調(diào)用,我們可能會假想strlen函數(shù)只調(diào)用一次,然而編譯器并不會這么做,他假定每次strlen的函數(shù)調(diào)用是不同的。從而在每次循環(huán)中多增加了一次函數(shù)調(diào)用。示例說明一個問題: 一個看上去無足輕重的代碼片段有隱藏的漸進低效率,通常人們會在一個小的數(shù)據(jù)集中進行測試和分析程序,對此程序的性能是足夠的。不過, 當(dāng)程序最終部署好以后,過程完全可能應(yīng)用在一個100萬個字符串上。突然,這段無危險的代碼變成了程序的主要性能瓶頸。大型編程項目中出現(xiàn)這樣的問題的故事比比皆是。一個有經(jīng)驗的程序員工作的一部分就是避免引入這樣的漸進低效率。
這個優(yōu)化是常見的一類優(yōu)化例子: 代碼移動, 這類優(yōu)化包括識別要執(zhí)行的多次但是計算結(jié)果不會改變的計算。因而可以將計算移動到代碼前面不會被多次求值的部分,編譯器會試圖進行代碼移動,他們不能夠發(fā)現(xiàn)一個函數(shù)是否會有副作用,因而假設(shè)函數(shù)會有副作用。所以,程序員經(jīng)常需要顯示的完成代碼移動。
-
-
手動優(yōu)化的幾個建議:
- 減少過程調(diào)用: 一個純粹主義者可能會說這種變換嚴(yán)重?fù)p害了程序的模塊性。比較實際的程序員會爭辯說 這種變換是獲得高性能結(jié)果的必要步驟。對于性能至關(guān)重要的應(yīng)用來說。為了速度,經(jīng)常必要的損害一些功能模塊性和抽象性,為了防止以后修改代碼,添加一些文檔是很明智的,說明采用了那些變換以及導(dǎo)致這些變換的假設(shè)。
- 消除不必要的存儲器引用
- 循環(huán)展開: 一種程序變換, 通過增加每次迭代計算的元素數(shù)量,減少循環(huán)的迭代次數(shù),循環(huán)展開能夠從兩方面改善程序的性能。 1. 減少 不直接有助于程序結(jié)果 的操作的數(shù)目, 例如循環(huán)索引計算和條件分支 2. 提供了一些方法,有助于減少整個計算中關(guān)鍵路徑上的操作數(shù)量。
- 提高并行性: 程序是受運算單元的延遲限制的。執(zhí)行加法和乘法的功能單元 是完全流水線化的。這意味著他們可以每個時鐘周期開始一個新操作。代碼不能利用這種能力。即使是使用循環(huán)展開也不能,這是因為我們將積累值放在一個單獨的變量acc中,在前面的計算完成之前,都不能計算acc的新值。打破這種順序關(guān)系是問題的關(guān)鍵。1, 多個累積變量。 2. 重新結(jié)合變換。減少計算中的關(guān)鍵路徑上的操作數(shù)量。通過更好地利用功能單元的流水線能力得到更好點性能,大多數(shù)編譯器不會對浮點數(shù)做重新結(jié)合,因為這些運算是不符合結(jié)合律的。通常我們發(fā)現(xiàn), 循環(huán)展開和并行的積累在多個值,是提高程序性能的更可靠的方法。
- 分支預(yù)測和預(yù)測錯誤的處罰: 書寫適合用條件傳送實現(xiàn)的代碼。分支預(yù)測錯誤,會招致嚴(yán)重的處罰
- 存儲器性能: 只考慮所有的數(shù)據(jù)都存放在高速緩存中的情況。(在下一章節(jié)中,進行詳細(xì)的介紹)
-
對循環(huán)展開和多個累計變量,重新結(jié)合變化的代碼示例
void combine4(vect_ptr v, data_t * dest) { long int i; long int length = vect_length(v); data_t * data = get_vec_start(v); data_t acc = 1; for (i = 0; i < length; i ++){ acc = acc * data[i]; } * dest = acc }
combine4.pngvoid combine5(vec_ptr v, data_r * dest) { long int i; long int length = vect_length(v); long int limit = length - 1; data_t * data = get_vec_start(v); data_t acc = 1; for(i = 0; i < limit; i+=2){ acc = (acc * data[i]) * data[i+1]; } for (; i< length ; i++){ acc = acc * data[i]; } * dest = acc; }
combine5.pngvoid combine6(vec_ptr v, data_r * dest) { long int i; long int length = vect_length(v); long int limit = length - 1; data_t * data = get_vec_start(v); data_t acc0 = 1; data_t acc1 = 1; for(i = 0; i < limit; i+=2){ acc0 = acc0 * data[i]; acc1 = acc1 * data[i + 1]; } for (; i< length ; i++){ acc = acc0 * data[i]; } * dest = acc0 * acc1; }
combine6.pngvoid combine7(vec_ptr v, data_r * dest) { long int i; long int length = vect_length(v); long int limit = length - 1; data_t * data = get_vec_start(v); data_t acc = 1; for(i = 0; i < limit; i+=2){ acc0 = acc * (data[i] * data[i + 1]; } for (; i< length ; i++){ acc = acc * data[i]; } * dest = acc; }
combine7.png -
總結(jié) 優(yōu)化程序效率的層次以及方法:
- 高級設(shè)計: 選擇恰當(dāng)?shù)乃惴ê蛿?shù)據(jù)結(jié)構(gòu),避免使用那些會漸進的產(chǎn)生糟糕的性能的算法和編碼技術(shù)
- 基本編碼原則:避免限制優(yōu)化的因素
1. 消除連續(xù)的函數(shù)調(diào)用。在可能時,可以犧牲模塊性來獲得更大的效率
2. 消除不必要的存儲器引用: 引入臨時變量來存儲結(jié)果,只在 最后的時候,將結(jié)果存放到數(shù)組變量、全局變量中 - 低級優(yōu)化
1. 展開循環(huán)
2. 多個累積變量 和重新結(jié)合的技術(shù),找到方法提高指令級別的并行(利用指令的流水線化)
3. 用功能的風(fēng)格重新條件操作,使編譯器采用條件數(shù)據(jù)傳送。
- profiling
警惕在優(yōu)化效率時候引入錯誤,引入新變量,改變循環(huán)邊界 使得代碼整體上更為復(fù)雜,很容易導(dǎo)致錯誤,需要測試來保證優(yōu)化代碼的正確性。
存儲器層次結(jié)構(gòu)
-
認(rèn)識存儲器系統(tǒng)
- 存儲器系統(tǒng): 是一個具有不同容器、成本和訪問時間的存儲設(shè)備的層次結(jié)構(gòu),CPU寄存器保存最常用的數(shù)據(jù),高速緩沖區(qū)作為一部分存儲在相對慢速的主存儲器中的數(shù)據(jù)和指令的緩沖區(qū), 主存暫時存放存儲在容量較大、慢速的磁盤上的數(shù)據(jù),而磁盤常常作為存儲在通過網(wǎng)絡(luò)連接的其他機器的磁盤上的數(shù)據(jù)的緩沖區(qū)。
- 存儲器層次結(jié)構(gòu): 對應(yīng)用程序的性能有著巨大的影響,CPU寄存器中的數(shù)據(jù)在0個周期可以訪問,在高速緩沖中1-30個周期,主存中50-200個周期,如果存儲在磁盤上,大概需要 幾千萬個周期。
- 存儲器層次結(jié)構(gòu)是可行的,一個編寫良好的程序傾向于頻繁的訪問某一個層次上的存儲設(shè)備。存儲器實現(xiàn)的整體效果是,其成本與層次結(jié)構(gòu)底層最便宜的存儲設(shè)備相當(dāng)。 但是卻以最接近于層次結(jié)構(gòu)頂部存儲設(shè)備的高速率向程序提供數(shù)據(jù)。
- 局部性: 一個計算機程序的基本屬性。具有良好局部性的程序傾向于一次又一次的訪問相同的數(shù)據(jù)項集合。或是傾向于訪問鄰近的數(shù)據(jù)項集合。具有良好局部性的程序比局部程序差的程序更傾向于訪問更高層次的數(shù)據(jù)項,其運行速度也可以相差20倍。
-
存儲技術(shù):
- 靜態(tài)RAM: SRAM, 雙穩(wěn)態(tài)特性。
- 動態(tài)RAM:DRAM, DRAM 存儲器單元對干擾非常敏感,暴露在光線之下會導(dǎo)致電容電壓的改變,數(shù)據(jù)照相機攝像機中的傳感器本質(zhì)就是DRAM,DDR(2 位), DDR2(4位), DDR3(8位)雙倍數(shù)據(jù)速率同步DRAM, (Double Data-rate Synchronous)
- 總線: 數(shù)據(jù)通過總線 共享電子電路在處理器和DRAM主存之間來來回回,每次CPU和主存之間的數(shù)據(jù)傳送都是通過一系列步驟來完成的。這些步驟通過總線事務(wù)。總線是一組并行的導(dǎo)線, 能攜帶地址、數(shù)據(jù)、控制信號。控制總線攜帶的信號會同步事務(wù),并區(qū)分當(dāng)前正在被執(zhí)行的事務(wù)類型。例如, 當(dāng)前得兒事務(wù)是主存,還是磁盤,以及其他磁盤設(shè)備,信息是地址,還是數(shù)據(jù),事務(wù)是讀還是寫。
- 總線接口 --- I/O橋 ------ 主存, I/O橋?qū)⑾到y(tǒng)總線的信號翻譯成存儲器總線的電子信號。
- 磁盤存儲, 畫圖
- 固態(tài)硬盤(Solid State Disk SSD) ssd 的性能特性, 順序讀和寫性能相當(dāng),順序℃比順序?qū)懮晕⒖煲稽c,但是按照隨機順序訪問邏輯快時, 寫比讀慢一個數(shù)量級。一個閃存是由B個塊的序列組成。每個塊有P頁組成,通常頁的大小是512-4Kb, 塊是32-128頁組成的。樹蕨是以頁單位讀寫的。只有一頁所屬的真?zhèn)€塊被擦除之后,才能寫這一頁,不過一旦一個塊被擦除了,塊中每一個都可以不需要在進行擦除就可以寫了,大約在100000次重復(fù)寫之后,塊就會損壞。速記寫慢的原因: 1. 擦出塊需要相對較長的時間,1ms級別,比訪問頁所需時間要高出一個數(shù)量級,2. 如果寫操作試圖修改一個包含已經(jīng)有數(shù)據(jù)的頁,那么這個塊中所有有用數(shù)據(jù)的頁都必須拷貝到一個新塊,才能進行對頁p的寫。
-
局部性:
一個編寫良好的程序具有良好的局部性, 他傾向于引用鄰近其他最近引用過的數(shù)據(jù)項的數(shù)據(jù),或者最近應(yīng)用過的數(shù)據(jù)項,這種傾向性被稱為局部性原理。1. 時間局部性, 2. 空間局部性- 時間局部性: 被引用一次的存儲器位置很可能在不遠(yuǎn)的將來再被多次引用。
- 空間局部性: 一個存儲器位置被引用了,那么程序很可能在不遠(yuǎn)的將來引用附近的一個存儲位置
int sumec(int v[N]) { int i, sum = 0; for(i = 0 ; i < N ; i++) { sum += v[i]; } return sum; }
空間局部性很好, 時間局部性很差, 在一個連續(xù)的向量模式中,每隔k個元素進行訪問, 就被稱為步長為k的引用模式,一般而言,隨著步長的增加, 空間局部性下降。
- 存儲器層次結(jié)構(gòu)
圖片- 緩存命中:
- 緩存不命中:
- 第k層的緩存從第k+1緩存中 取出包含的那個塊,如果k層的緩存已經(jīng)滿了的話,就需要覆蓋現(xiàn)存的一個塊。 成為替換或驅(qū)逐這個塊,倍驅(qū)逐的這個塊兒,稱為 犧牲塊,決定該替換那個塊是有緩存的替換策略來控制的。比如隨機替換策略和最近最少被使用的(LRU)替換策略
- 緩存不命中的種類: 冷緩存: 一個空的緩存,他是短暫的事件,不會在反復(fù)訪問存儲器是的緩存暖身之后的穩(wěn)定狀態(tài)中出現(xiàn)。2. 只要發(fā)生了不命中,k層的緩存就必須執(zhí)行某個放置策略,確定它從第k+1層中的數(shù)據(jù)放在哪里。
- 沖突不命中: 被引用的數(shù)據(jù)對象,映射到同一個緩存塊,緩存會一直不命中
- 容量不命中: 一個嵌套的循環(huán)可能會反復(fù)的訪問同一個數(shù)組的元素,這個塊的集合稱為這個階段的工作集,當(dāng)工作集的大小超過了緩存的大小,就會發(fā)生 容量不命中的
鏈接
TODO
鏈接 是將各種代碼和數(shù)據(jù)部分收集起來并組合成為一個單一文件的過程,鏈接可以執(zhí)行于 編譯時(compile time 也就是在源代碼被翻譯成機器代碼時)也可以執(zhí)行于加載時(load time在程序倍加載器加載到存儲器并運行時)甚至執(zhí)行于運行時(run time 有應(yīng)用程序來執(zhí)行)
鏈接器 在軟件開發(fā)中,扮演者一個關(guān)鍵的角色,因為他們使得的分離編譯 成為可能,我們不用將一個大型的應(yīng)用程序組織為一個巨大的源文件,而是可以把她分解為更小,更好管理的模塊,可以獨立的修改和編譯這些模塊。當(dāng)我們修改一個文件時候,簡單的編譯她,并重新連接應(yīng)用,而不必重新編譯整個項目。
鏈接器產(chǎn)生的可執(zhí)行文件在重要的系統(tǒng)功能中扮演者重要的角色,比如 加載和運行程序,虛擬存儲器,分頁,和存儲器映射。隨著共享庫和動態(tài)連接在 操作系統(tǒng)中的重要性加強,鏈接成為一個復(fù)雜的過程。linux elf目標(biāo)文件格式。
異常控制流
異常控制流(ExceptI/Onal Control Flow ECF): 異常控制流發(fā)生在計算機的各個層次。
硬件層: 硬件檢測的事情會觸發(fā)控制突然轉(zhuǎn)移到異常處理程序
操作系統(tǒng)層: 內(nèi)核通過 上下文切換 將控制從一個用戶進程轉(zhuǎn)移到另一個用戶進程
應(yīng)用層面: 一個進程可以通過發(fā)送信號到另一個進程, 而接受者將轉(zhuǎn)移到信號處理程序
-
異常控制流:
- 是操作系統(tǒng)用來實現(xiàn)I/O、進程、和虛擬存儲器的基本機制
- 是應(yīng)用程序與操作系統(tǒng)交互的方式: 應(yīng)用程序通過一個陷阱(trap)或 系統(tǒng)調(diào)用(system call) 的形式,向操作系統(tǒng)請求服務(wù)。
- 操作系統(tǒng)為程序提供了強大的ECF機制,創(chuàng)建進程、等待進程、通知其他進程系統(tǒng)中的異常事件等
- ECF是計算機實現(xiàn)并發(fā)的基本機制
- 提供程序方面的異常機制,try, catch。 setjmp, longjmp)
-
異常:
- 異常是異常控制流的一種形式。
- 實現(xiàn): 硬件 + 操作系統(tǒng), 隨著系統(tǒng)不同而不同,但是基本思想相同,
- 控制流中的突變, 用來響應(yīng)處理器狀態(tài)中的某些變化。也稱為事件, 比如: 發(fā)生虛擬存儲器缺頁,算數(shù)溢出, I/O請求完成等。
- 事件放生時候, 處理器通過 異常表(exceptI/On table, 間接跳轉(zhuǎn)表) 來調(diào)用 操作系統(tǒng)子程序---異常處理程序(一個專門設(shè)計來處理這種事件的操作系統(tǒng)子程序)
- 異常處理類似于函數(shù)調(diào)用, 但是不同于函數(shù)調(diào)用:
1. 異常處理程序運行在內(nèi)核模式下,這意味著他們對所有的系統(tǒng)資源都有完全的訪問權(quán)限
2. 如果控制從一個用戶態(tài)轉(zhuǎn)到內(nèi)核態(tài),那么所有的項目都需要壓到內(nèi)核棧中
3. 過程調(diào)用, 在跳轉(zhuǎn)到程序之前,處理器將返回地址壓倒棧中, 異常:返回地址要么是當(dāng)前指令,要么是 下一臺指令。
-
異常類型: 中斷(interrupt), 陷阱(trap), 故障(fault)終止(abort)
- 中斷: 來自處理器外部的I/O設(shè)備的信號的結(jié)果, 異步, 硬件中斷的異常處理程序 通常稱為 中斷處理程序。
- 陷阱: 有意的異常,是執(zhí)行一條指令的結(jié)果。陷阱的重要用途是: 在用戶程序和內(nèi)核之間提供一個像過程一樣的接口,叫系統(tǒng)調(diào)用。 從程序員的角度來看, 系統(tǒng)調(diào)用和函數(shù)調(diào)用是一樣的,然而其中的內(nèi)部實現(xiàn)是非常不同的。 普通的函數(shù)運行在用戶模式中, 用戶模式限制了函數(shù)可以執(zhí)行的指令的類型
- 故障: 由錯誤引起的,故障發(fā)生時, 處理器將控制轉(zhuǎn)移到故障處理程序,如果處理程序能夠處理修正錯誤,他將控制權(quán)轉(zhuǎn)移到引起故障的指令,從而重新執(zhí)行。否則,處理程序返回到內(nèi)核中的abort函數(shù),從而終止程序。 一個經(jīng)典的故障示例是缺頁異常, 指令引用的一個虛擬地址,與該虛擬地址關(guān)聯(lián)的物理頁面不在 存儲器中,從而發(fā)生故障。缺頁處理程序 從磁盤加載到存儲器中之后,將控制權(quán)轉(zhuǎn)交到 程序指令, 程序執(zhí)行再次執(zhí)行,就不會發(fā)生故障了。
- 終止: 是不可以修復(fù)的致命錯誤造成的結(jié)果, 通常是一些硬件錯誤。終止處理程序,不會將控制返回到應(yīng)用程序,而是終止應(yīng)用程序。
- linux 中的異常示例:
-
linux 故障和終止:
異常號 描述 異常類型 0 除法錯誤 故障 13 一般保護故障 故障 14 缺頁 故障 18 機器檢查 終止 32-127 操作系統(tǒng)定義的異常 中斷或陷阱 128 系統(tǒng)調(diào)用 陷阱 129-255 操作系統(tǒng)定義的異常 中斷或陷阱 linux 系統(tǒng)調(diào)用: 每個系統(tǒng)調(diào)用都有一個唯一的整數(shù)號, 對應(yīng)于一個到內(nèi)核中的跳轉(zhuǎn)表的偏移量。
系統(tǒng)調(diào)用是通過 指令 int 0x80 調(diào)用的, 參數(shù)傳遞都是通過寄存器而不是棧傳遞的。 %eax為系統(tǒng)調(diào)用號, 寄存器%ebx, %ecx ...為調(diào)用參數(shù), 標(biāo)準(zhǔn)C函數(shù)庫提供了包裝函數(shù), 這些包裝函數(shù)將參數(shù)打包在一起, 以適當(dāng)系統(tǒng)調(diào)用號陷入內(nèi)核。然后將系統(tǒng)調(diào)用的返回狀態(tài)傳遞給調(diào)用函數(shù),完成包裝任務(wù)。
-
進程:
- 一個執(zhí)行中的程序的示例。 系統(tǒng)中的每個程序都是運行在某個進程的上下文中的。上下文是程序正確運行所需要的狀態(tài)的組成。包括, 程序代碼和數(shù)據(jù),棧,通用寄存器的內(nèi)容,程序計數(shù)器等
- 異常是允許 操作系統(tǒng)提供進程的概念所需要的基本構(gòu)造塊。
- 在外殼程序中輸入可執(zhí)行目標(biāo)文件的名字, 外殼會創(chuàng)建一個新的進程,然后在這個進程的上下文中 運行這個可執(zhí)行目標(biāo)文件。
- 進程提供的關(guān)鍵抽象有:
1. 一個獨立的邏輯控制流,提供一個獨占使用處理器的假象
2. 一個私有的地址空間,提供一個獨占存儲系統(tǒng)的假象 - 并發(fā)與并行:
1. 邏輯控制流:因為進程提供每個程序單獨使用處理器的假象。所以程序計數(shù)器(PC)的值的序列叫做 邏輯控制流
2. 并發(fā): 并發(fā)流(concurrent)一個邏輯控制流 的執(zhí)行時間 與另一個邏輯控制流重疊, 稱為并發(fā)的執(zhí)行。多個流并發(fā)的執(zhí)行稱為并發(fā),
3. 一個進程與其他進程輪流運行成為多任務(wù)
4. 并行流(parallel follow) 是并發(fā)流的一個真子集, 如果兩個流并發(fā)的運行在不同的處理器核或者計算機上,我們稱為并行流, - 私有地址空間: n為地址的機器上, 0,1,2......z**n - 1, 地址空間頂部是保留給內(nèi)核的,該部分是內(nèi)核在代表進程執(zhí)行指令時使用的代碼,數(shù)據(jù)和棧
- 用戶模式、內(nèi)核模式:
1. 為了提供一個無懈可擊的進程抽象,處理器提供一種機制,來限制一個應(yīng)用可以執(zhí)行的指令以及他可以訪問的地址空間。( 為了一個無懈可擊的抽象?還是因為安全, 比如內(nèi)核模式應(yīng)該可以對用戶模式,掩蓋 地址抽象的細(xì)節(jié),而不是,讓用戶破壞這種抽象)
2. 一般是處理器通過一個寄存器中的一個模式位(mode bit)來提供這種功能的。該寄存器 描述了進程當(dāng)前的特權(quán), 設(shè)定了位模式, 進程在內(nèi)核模式,反之在用戶模式
3. 內(nèi)核模式: 一個運行在內(nèi)核模式中的程序可以執(zhí)行指令集中的任何指令,并可以訪問系統(tǒng)中的任何存儲位置。
4. 用戶模式: 不允許執(zhí)行執(zhí)行特權(quán)命令,比如:停止處理器, 發(fā)起一個I/O操作。
5. 進程進入到內(nèi)核模式的唯一方法是:通過,中斷、故障、或者陷入(系統(tǒng)調(diào)用)來進入到內(nèi)核模式。 - 上下文切換
* 上下文切換: 操作系統(tǒng)通過這種較高層次的異常控制流來實現(xiàn)多任務(wù)。上下文切換機制是建立在 中斷,陷阱,故障,終止 較低異常層次機制之上的。
* 上下文: 就是內(nèi)核重啟一個被搶占進程所需要的數(shù)據(jù)狀態(tài)。包括,寄存器,程序計數(shù)器,內(nèi)核棧,和各種數(shù)據(jù)結(jié)構(gòu)等。內(nèi)核為每一個進程提供一個上下文。
* 調(diào)度: 在進程執(zhí)行的某些時刻(那些?),內(nèi)核可以決定搶占 當(dāng)前進程,并重新開始一個進程, 這種決定叫做調(diào)度。有內(nèi)核中成為調(diào)度器的代碼處理。在內(nèi)核調(diào)度了一個進程運行后,她就搶占了當(dāng)前進程,使用上下文切換機制來將控制轉(zhuǎn)移到新的進程。
1. 保存當(dāng)前進程的上下文
2. 恢復(fù)某個先前被搶占的進程被保存的上下文
3. 將控制轉(zhuǎn)移到新的進程- 上下文切換的幾個簡單示例:
- 中斷(任何時候): 所有系統(tǒng)都有某種產(chǎn)生周期性定時器中斷的機制,典型的為1毫秒, 每次發(fā)生定時器中斷時,內(nèi)核就能夠判斷 當(dāng)前進程是否執(zhí)行了足夠的時間,并切換到一個新的進程。(操作系統(tǒng)賴以生存的控制機制)
- 阻塞:系統(tǒng)調(diào)用時候,因為等待某個事件而發(fā)生阻塞,可能發(fā)生上下文切換。比如 read 系統(tǒng)請求磁盤訪問,內(nèi)核可能進行上下文切換, 來執(zhí)行其他操作,而不是等待I/O, Sleep 系統(tǒng)調(diào)用, 顯示的請求讓調(diào)用進程休眠。
- 上下文切換的幾個簡單示例:
-
進程控制:
- 進程創(chuàng)建和終止: 從程序員的角度來看 進程總是處于3個狀態(tài): 運行, 停止,終止,
- 停止: 進程的執(zhí)行被掛起。 且不會被調(diào)度, 當(dāng)收到SIGSTOP, SIGTSTP, SIDTTIN, SIGTTOUT 進程就會停止,并保持停止直到它收到一個SIGCONT 程序再次執(zhí)行
- 終止: 進程永遠(yuǎn)停止。原因有: 1. 收到一格信號,該信號默認(rèn)行為是終止進程 2. 從主程序返回 3. 調(diào)用exit函數(shù)
- fork
子進程得到與父進程用戶級虛擬地址空間相同的 一份拷貝, 文本,數(shù)據(jù),bss,堆等。 子進程還獲得父進程任何打開文件描述符的相同的拷貝。 這意味子進程可以讀寫父進程中打開的任何文件。最大的區(qū)別在于PID。
-
fork函數(shù) 只被調(diào)用一次, 卻返回兩次。在父進程中 fork返回子進程的PID, 在子進程中,返回0,因為子進程的PID為非零,所以提供了一種明確的方法,來分辨程序在 父進程中,還是子進程中執(zhí)行。
int main() { pid_t = pid; int x = 1; pid = Fork(); if (pid == 0){ printf("child :x %d\n", ++x); exit(0); } printf("parent: x %d\n", --x); exit(0); }
- 調(diào)用一次返回兩次: fork函數(shù)被父進程 調(diào)用一次, 但是卻返回兩次, 一次返回到父進程中,一次返回到子進程中,對于只fork一次的程序來說,這還是相當(dāng)簡單直接的,但是對于多次fork的程序來說,需要謹(jǐn)慎的分析
- 并發(fā)執(zhí)行:父進程和子進程是并發(fā)運行的獨立進程。也就說不能夠假定 進程的執(zhí)行順序。
- 相同的但是獨立的地址空間
- 共享文件:子進程繼承了父進程的所有的打開文件。
- 回收子進程:wait/waitpid
- 回收: 當(dāng)一個進程終止時,內(nèi)核并不是把它從系統(tǒng)中清除出去,而是保持終止?fàn)顟B(tài),直到父進程回收。當(dāng)父進程回收已經(jīng)終止的子進程時,內(nèi)核將子進程的退出狀態(tài)傳遞給父進程。然后拋棄已終止的進程。改進程就不存在了,
- 僵死進程: 一個終止了但是還沒有被回收的進程,
- 僵死進城處理: 父進程還沒有回收僵死進程就退出了,那么內(nèi)核會安排init進程來回收他們。init 進程PID 為1在系統(tǒng)初始化時候有內(nèi)核創(chuàng)建。
- waitpid 函數(shù)
1. waitpid(pid_t pid, int * status, int optins)
2. pid > 0 等待單個pid, pic = -1, 等待 父進程的所有子進程。
3. optIons:
* WNOHANG: 等待集合中的任何子進程都還沒有終止, 那么就立即返回, 默認(rèn)行文是 掛起調(diào)用進程, 直到所有子進程終止,
* WUNTRACED: 掛起調(diào)用進程, 直到 等待集合中一個進程變成 已終止或者被停止,返回, 返回的PID為引起返回的 進程PID, 默認(rèn)行為:返回已經(jīng)終止的子進程
* WHOHANG | WUNTRACED, 立即返回, 如果等待集合中沒有任何子進程被停止或已終止, 那么返回值為0, 或者返回那個停止的終止的進程PPID- 如果status參數(shù)非空,waitpid在status中放置 關(guān)于導(dǎo)致返回子進程的狀態(tài)信息。下面是解釋status參數(shù)的幾個宏
* WIFEXITED(status): 如果子進程通過調(diào)用exit或者一個返回(return) 正常終止。就返回真
* WEXITSTATUS(status): 返回一個正常終止的子進程的退出狀態(tài),只有在WIFEXITED 返回為真時了, 才會定義這個狀態(tài)。- WIFSIGNALED(status): 如果子進程是因為一個未被捕獲的信號終止的。那么返回真
- WTERMSIG(status): 返回導(dǎo)致子進程終止的信號的編碼, 只有咋WIFESIGNailed為真 才定義這個狀態(tài)
- WIFSTOPPED(status): 如果引起返回的子進程當(dāng)前是被停止的。那么就返回真。
- WSTOPSIG(status): 返回引起子進程停止的信號的數(shù)量,只有 wifstopped 返回為真時,才定義這個狀態(tài)。
- 如果status參數(shù)非空,waitpid在status中放置 關(guān)于導(dǎo)致返回子進程的狀態(tài)信息。下面是解釋status參數(shù)的幾個宏
- wait 函數(shù)是waitpid 的 簡單版本, wait(int * status) === waitpid(-1, & status, 0)
- 讓進程休眠
- sleep:休眠一定秒數(shù)
- pause:函數(shù)讓調(diào)用進程休眠,直到該進程收到一個信號
- 加載并運行程序 execve:
1. execve(char * filename, char * argsv[], char * envp[])
2. 函數(shù)加載并運行可執(zhí)行目標(biāo)文件filename, 并傳遞參數(shù)argv, 環(huán)境變量 envp execve 調(diào)用一次 并不返回 fork函數(shù)在新的子進程中運行相同的程序,新的子進程是父進程的一個復(fù)制品。 execve 函數(shù)在當(dāng)前進程的上下文中加載并運行一個新的程序,他會覆蓋當(dāng)前進程的地址空間。但是并沒有創(chuàng)建一個新進程。新進程依然有相同的PID, 已打開的文件描述符
- 進程創(chuàng)建和終止: 從程序員的角度來看 進程總是處于3個狀態(tài): 運行, 停止,終止,
-
信號
- 信號概念
* 更高層的軟件異常形式。允許中斷其他進程
* man 7 signal 得到信號列表
* 每個信號類型都對應(yīng)于 某種系統(tǒng)事件, 底層的硬件異常 是由內(nèi)核異常處理程序處理的。正常情況下, 對用戶進程是不可見的。信號提供了一種機制,來通知用戶發(fā)生了這些異常。 - 發(fā)射信號 到接受者的過程 相關(guān)概念:
1. 發(fā)送信號: 內(nèi)核通過更新目的進程的上下文中的某個狀態(tài),來發(fā)送一個信號到目的進程,發(fā)射信號的原因有: 1. 內(nèi)核檢測到一個事件,通知進程, 2. 一個進程調(diào)用kill函數(shù), 要求內(nèi)核發(fā)送信號。
2. 接受信號: 當(dāng)目的進程被內(nèi)核強迫以某種形式對信號的發(fā)送做出反應(yīng)時,反應(yīng)有: 1. 忽略信號, 2. 終止 3. 執(zhí)行信號處理程序的用戶函數(shù)
3. 待處理信號:一個發(fā)出而沒有被接受的信號,任何時刻, 一種類型至多只會有一個待處理信號。如果一個進程有一個類型為k的待處理信號,那么任何接下來的發(fā)送到進程的類型為k的信號,都會被簡單的丟棄。
4. 阻塞信號: 一個進程可以設(shè)定阻塞一種類型的信號。可以被發(fā)送,但是不會被接受。
5. 實現(xiàn): pending 位向量 維護待處理信號集合, blocked 位向量,維護被阻塞信號集合。傳遞類型為k的信號, pending 的k位 被標(biāo)記, 接收了 清除標(biāo)記。 - 發(fā)送信號
1. 進程組: 提供了大量向進程發(fā)送信號的機制, 所有這些機制都是基于進程組的。每個進程都屬于一個進程組,
2. kill 程序發(fā)送信號
3. 鍵盤發(fā)送信號, 外殼程序 創(chuàng)建程序執(zhí)行,ctrl-c 會發(fā)送一個SIGINT信號到 外殼, 外殼程序捕獲到該信號,然后發(fā)送SIGINT 信號到這個前臺進程中的每個進程。ctrl-z則會發(fā)送 SIGTSTP 信號
4. kill 函數(shù)調(diào)用
5. alarm 函數(shù)發(fā)送信號 SIGALRM 信號, 在sec 秒中發(fā)送信號 - 接受信號
1. 當(dāng)內(nèi)核從一個異常處理程序中返回,準(zhǔn)備將控制傳遞到進程p時,他會檢查進程p的未被阻塞的待處理信號的集合。 如果這個集合不為空, 那么內(nèi)核選擇集合中的某個信號k, 并且強制p接受信號k, 觸發(fā)進程的某個行為, 每個信號類型都有一個預(yù)訂的默認(rèn)行為如下:
* 進程終止
* 進程終止并轉(zhuǎn)存儲器
* 進程停止知道倍SIGCONT 信號重啟
* 進程忽略該信號
2. 進程可以通過signal函數(shù)修改和信號相關(guān)聯(lián)的默認(rèn)行為, 唯一的例外是: SIGSTOP, SIGKILL他們的默認(rèn)行為是不可以修改的。 signal(int signum, sighandler_t handler) 設(shè)置信號處理程序選項
* 如果handler 是SIG_IGN, 那么忽略類型為 signum 的信號
* 如果handler 是SIG_DFL,那么類型為signum的信號行為修改未默認(rèn)
* 否則handler是用戶定義的 信號處理程序地址,
* 調(diào)用信號處理程序成為捕獲信號, 執(zhí)行信號處理程序為 處理信號。處理程序會調(diào)用會傳遞一個參數(shù)k,為信號類型, 因此, 同一個函數(shù)可以設(shè)定為處理多個信號處理程序- 設(shè)定阻塞,和取消阻塞, sigismember(sigset_t * set, int signum)
- 信號處理問題:
1. 待處理信號被阻塞: Unix信號處理程序通常會阻塞當(dāng)前處理程序正在處理類型的待處理信號。如果一個進程捕獲一個SIGINT信號,并運行處理程序,如果另外一個SIGINT 信號傳遞到這個進程, 那么這個SIGINT將變成待處理的,但是不會被接受,直到處理程序返回
2. 待處理信號不會排隊等待,任意類型至多只有一個待處理信號
3. 系統(tǒng)調(diào)用可以被中斷,慢系統(tǒng)調(diào)用, read, wait, accept等會阻塞進程一段時間,某些系統(tǒng)中當(dāng)捕獲一個信號時,被中斷的慢系統(tǒng)調(diào)用不會被繼續(xù)而是返回錯誤 - 非本地跳轉(zhuǎn): 它將控制直接從一個函數(shù)轉(zhuǎn)移到另一個當(dāng)前正在執(zhí)行的函數(shù),而不需要經(jīng)過正常的調(diào)用--返回序列。(setjump longjump, sigsetjmp, siglongjmp)
- setjump 函數(shù)在env 緩沖區(qū)中保存當(dāng)前調(diào)用環(huán)境
- longjmp 從env緩沖區(qū)中恢復(fù)調(diào)用環(huán)境,然后觸發(fā)一個從最近一次初始化env的setjmp調(diào)用的返回,然后setjmp返回,并帶有非零的返回值retval
- 從深層嵌套函數(shù)調(diào)用中立刻返回
- 信號處理程序分支到一個特殊的代碼位置,而不是返回到倍信號到達(dá)中斷了的指令位置
- C++, java提供的異常機制是較高層次的,是C語言的setjmp, longjmp更結(jié)構(gòu)化的版本。
- 信號概念
-
總結(jié)
- 異常控制流發(fā)生在計算機系統(tǒng)的個個層次,是計算機系統(tǒng)中提供并發(fā)的基本機制
- 硬件層面, 異常是有處理器中的事件 觸發(fā)的控制流突變。控制流傳遞給一個軟件處理程序,止嘔返回給被中斷的控制流
- 異常類型: 中斷 故障 終止 陷阱
- 在操作系統(tǒng)層面, 內(nèi)核用ECF 提供進程的基本概念
- 在應(yīng)用層面, C語言使用分本地跳轉(zhuǎn)來規(guī)避正常的調(diào)用、返回棧 規(guī)則(try catch 等的實現(xiàn)類似)
虛擬存儲器
TODO
系統(tǒng)級 I/O
I/O 主要在主存和外部設(shè)備之間拷貝數(shù)據(jù)的過程, 輸入操作是從I/O 設(shè)備拷貝數(shù)據(jù)到主存, 而輸出操作是從主存拷貝數(shù)據(jù)到 I/O設(shè)備
- I/O
- 是系統(tǒng)中不可或缺的一部分,經(jīng)常會遇到I/O和其他系統(tǒng)概念循環(huán)依賴的情景:I/O 在進程的創(chuàng)建和執(zhí)行扮演著關(guān)鍵角色,反過來,進程創(chuàng)建在不同進程間的文件共享中扮演關(guān)鍵角色。
- 所有的I/O設(shè)備,如網(wǎng)絡(luò)、磁盤、終端 都被模型化為文件。而所有的輸入和輸出都被當(dāng)做對應(yīng)的文件的讀寫操作。這種將設(shè)備優(yōu)雅的映射為文件的方式,允許Unix內(nèi)核引出一個簡單的低級的應(yīng)用接口,成為Unix I/O, 這使得所有的輸入和輸出都以統(tǒng)一、一致的方式來執(zhí)行。
- 操作
- 打開文件: 用程序通過 要求內(nèi)核打開相應(yīng)的文件,來訪問一個I/O設(shè)備, 內(nèi)核返回一個小的非負(fù)數(shù)的描述符。后續(xù)的操作都通過這個描述符
- seek: 改變當(dāng)前的文件位置, 對于每個打開的文件,內(nèi)核保持著一個文件位置k,標(biāo)志著從文件開頭起始的字節(jié)偏移量。seek操作可以顯示的設(shè)定位置
- 讀寫文件: 讀操作就從文件拷貝 n > 0個字節(jié),k 為當(dāng)前文件位置, m 為文件大小, k+n >=m 的操作,會觸發(fā)一個end-of-file(EOF) 的條件。應(yīng)用程序能夠檢測到這個條件, 在文件結(jié)尾處并沒有明確的 “EOF” 符號。
- 關(guān)閉文件: 當(dāng)應(yīng)用完成了文件的訪問之后, 他通知內(nèi)核關(guān)閉這個文件。內(nèi)核釋放文件打開時創(chuàng)建的數(shù)據(jù)結(jié)構(gòu),并將描述符恢復(fù)到可用的描述符池中。無論一個進程因為什么原因終止,內(nèi)核都會關(guān)閉所有打開的文件并釋放存儲器資源。
- 共享文件
- 描述符表(descriptor table):
1. 每個進程都擁有獨立的描述符表
2. 描述符: 每個打開的描述符索引 文件表 中的一個表項 - 文件表(file table):
1. 所有的進程共享
2. 所有的打開的文件的集合組成的 文件表
3. 文件表項 包括當(dāng)前的文件的位置、引用計數(shù)、以及一個指向 v-node 表中對應(yīng)表項的指針
4. 關(guān)閉一個描述符會減少響應(yīng)的文件的表 表項中的引用計數(shù)。為零時,內(nèi)核會刪除表項 - v-node 表:
1. 所有的進程共享
2. 包含文件的大多數(shù)信息。包含st_mode, st_size 等成員,
3. todo, 補充文件元信息的數(shù)據(jù)結(jié)構(gòu)
- 描述符表(descriptor table):
- 場景
- 打開同一個file 兩次, 會產(chǎn)生, 兩個文件表表項, 來標(biāo)注兩個文件不同的文件位置,引用計數(shù)等。
-
fork: 子進程會copy 一個 父進程描述符表表項的副本,指向相同的 文件表表項。所以子進程跟父進程共享同一個文件位置,增加文件引用計數(shù)。
IO.png
- 總結(jié)
unix 提供了少量的系統(tǒng)級別函數(shù), 他們允許應(yīng)用程序打開、關(guān)閉、讀和寫文件、提取文件的元數(shù)據(jù),以及執(zhí)行I/O重定向。Unix內(nèi)核使用三個相關(guān)的數(shù)據(jù)結(jié)構(gòu)來表示打開的文件。描述符表項指向文件表表項, 文件表表項指向v-node 表項,每個進程都有自己的描述符表項,所有進程共享 文件表和v-node表。理解這些數(shù)據(jù)結(jié)構(gòu),利于,理解,fork, 已經(jīng)I/O重定向的實現(xiàn)。標(biāo)準(zhǔn)I/O庫是基于UnixI/O實現(xiàn)的,標(biāo)準(zhǔn)I/O更簡單,優(yōu)于unix I/O, 因為對標(biāo)準(zhǔn)I/O和網(wǎng)絡(luò)文件一些相互不兼容的問題,Unix I/O 更適合網(wǎng)絡(luò)應(yīng)用程序
網(wǎng)絡(luò)編程
網(wǎng)絡(luò)應(yīng)用隨處可見,web瀏覽器,email,wechat,有趣的是,所有的網(wǎng)絡(luò)應(yīng)用都是基于相同的基本編程模型,有著相似的整體邏輯結(jié)構(gòu), 并且依賴相同的編程接口。
網(wǎng)絡(luò)應(yīng)用依賴于許多概念: 進程、信號、字節(jié)序列、存儲器映射以及動態(tài)分配存儲, 都扮演者重要的角色
-
套接字:
客戶端和服務(wù)器 混合使用套接字接口函數(shù)和 Unix I/O函數(shù)來進行通訊,套接字函數(shù)典型的是作為會陷入內(nèi)核的系統(tǒng)調(diào)用來實現(xiàn)。并調(diào)用各種內(nèi)核模式和TCP/IP 函數(shù)
- int socket(int domain, int type, int protocol)
- int connect(int sockfd, struct sockaddr, int addrlen): 客戶端通過調(diào)用 connect 函數(shù) 建立服務(wù)端的連接, connect函數(shù)會阻塞,一直到成功建立連接,或是發(fā)生錯誤,如果成功, sockfd 描述符可以進行讀寫了, 并且得到連接套接字對(x:y, serv_addr,:serv_addr.sin_port) x表示客戶端的ip地址,y表示臨時窗口,他唯一確定了客戶端主機上的進程
- bind(int sockfd, struct sockaddr * my_addr, int addrlen): bind函數(shù)告訴內(nèi)核將my_addr中的服務(wù)器套接字地址和套接字描述符sockfd聯(lián)系起來
- listen(int sockfd, int backlog): 服務(wù)器調(diào)用listen函數(shù)告訴內(nèi)核, 描述符是被服務(wù)器而不是客戶端使用的。listen函數(shù)將sockfd 從一個主動套接字轉(zhuǎn)化為一個監(jiān)聽套接字(listening socket) 該套接字可以接受來自客戶端的連接請求,backlog 表示內(nèi)核在開始拒絕請求之前, 應(yīng)該放入隊列中等待的未完成連接的請求的數(shù)目。backlog參數(shù)的確切含義要求退TCPIP協(xié)議的理解。
- accept(int listenfd, struct sockaddr * addr, int * addrlen): accept函數(shù)等待客戶端到達(dá)listenfd的連接請求,然后在addr中填入客戶端的套接字地址,并返回一個已連接描述符(connected descriptor)這個描述符 可以使用unix I/O進行操作與客戶端通訊
-
監(jiān)聽描述符和已連接描述符的區(qū)別:
- 監(jiān)聽描述符: 是作為客戶端連接請求的一個端點, 創(chuàng)建一次, 并存在于服務(wù)器的整個生命周期
- 已連接描述符: 是客戶端與服務(wù)端已經(jīng)建立連接起來的一個端點。服務(wù)器每次接受請求都會創(chuàng)建一次,他只存在于服務(wù)器為一個客戶端服務(wù)的過程中
- 然而區(qū)分: 這兩者, 被證明是很有用的。因為他使得我們可以簡歷并發(fā)服務(wù)器,他能夠同時處理許多客戶端連接,例如,每一個請求到達(dá)監(jiān)聽描述符時, 我們可以fork一個進程去處理,他可以通過已連接描述符對客戶端進行通訊,
-
HTTP
-
Web服務(wù)器, web客戶端和服務(wù)器之間的交互是一個基于文本應(yīng)用級協(xié)議,叫做HTTP, 其內(nèi)容是一個與Mime(multipurpose internet Mail ExtensI/Ons 多用途網(wǎng)際郵件擴充協(xié)議)類型關(guān)聯(lián)的字節(jié)序列。
MIME 類型 描述 text/html HTML 頁面 text/plain 無格式文本 applicatI/On/postscript postscript 文檔 image/gif gif 格式編碼的圖片 image/jpeg jpeg格式編碼的圖片
-
-
服務(wù)動態(tài)內(nèi)容: 客戶端如何將程序參數(shù)傳遞給服務(wù)器, 服務(wù)器如何將這些參數(shù)傳遞給 他所創(chuàng)建的子進程? 子進程 將他的輸出發(fā)送到哪里?一個成為CGI(common Gateway Interface) 通用網(wǎng)關(guān)接口的實際標(biāo)準(zhǔn)解決了這些問題。
- 客戶端如何將程序參數(shù)傳遞給服務(wù)器: ?, &分割開來
- 服務(wù)器如何將參數(shù)傳遞給 子進程: CGI設(shè)定環(huán)境變量 QUERY_STRING=name=xx&age=xxx, 子進程可以通過getenv函數(shù)獲得
| 環(huán)境變量 | 描述 | | :------------- | :------------- | | SERVER_PORT | 父進程監(jiān)聽的port| | QUERY_STRING | url 參數(shù)| |REQUEST_METHOD | get or post etc | |REMOTE_HOST | 客戶端域名 | | REMOTE_ADDR| 客戶端ip | | CONTENT_TYPE | 請求體的MIME | | CONTENT_LENGTH | 請求體的大小 |
- 子進程將輸出到哪里? 一個CGI程序?qū)討B(tài)內(nèi)容發(fā)送到標(biāo)準(zhǔn)輸出, 在子進程加載并運行CGI程序之前, 使用unix dup21將標(biāo)準(zhǔn)輸出重定向到客戶端相關(guān)的已連接描述符。 因為CGI程序小入到標(biāo)準(zhǔn)輸出的東西,會直接送到客戶端。
并發(fā)編程
邏輯控制流在時間上重疊,就稱為并發(fā)。(concurrency)出現(xiàn)在計算機系統(tǒng)的多個層面上, 硬件異常處理, 進程和Unix信號處理程序
-
應(yīng)用層面上的并發(fā) 在下面的場景非常有用:
- 訪問慢速I/O設(shè)備, 應(yīng)用需要等待I/O設(shè)備的數(shù)據(jù)到達(dá)時候,內(nèi)核會運行其他進程,使得CPU保持繁忙。每個應(yīng)用都可以按照類似的方式,通過交替執(zhí)行I/O請求和其他有用的工作進行并發(fā)
- 服務(wù)多個網(wǎng)絡(luò)客戶端: 為每個客戶端創(chuàng)建創(chuàng)建一個單獨的控制流,允許服務(wù)器同時服務(wù)多個客戶端服務(wù)。避免慢速I/O操作獨占服務(wù)器
- 多個內(nèi)核機器上的并行計算
-
操作系統(tǒng)提供并發(fā)的基本方法:
- 進程: 每個邏輯控制流都是一個進程, 有內(nèi)核進行維護調(diào)度,進程間通訊通過使用顯示的進程間通訊(IPC) 機制
- I/O多路復(fù)用: 應(yīng)用程序在一個進程的上下文中顯示的調(diào)度他們自己的邏輯控制流,邏輯流被模型化為狀態(tài)機,數(shù)據(jù)達(dá)到文件描述符后,主程序顯示的從一個狀態(tài)轉(zhuǎn)換到另一個狀態(tài),因為程序是一個單獨的進程,所以所有的流都共享一個地址空間
- 線程: 是運行在一個單一進程上下文中的邏輯流,想進程流一樣有內(nèi)核進行調(diào)度,像I/O多路復(fù)用一樣 共享同一個虛擬地址。
-
基于進程的并發(fā)編程:
- 服務(wù)器 接受請求之后,父進程fork一個子進程。子進程獲得服務(wù)器描述符表的完整拷貝。
- 子進程關(guān)閉他拷貝的監(jiān)聽描述符, 父進程關(guān)閉他的已連接描述符, 因為子父子進程描述符指向同一個文件表表項,所以父進程關(guān)閉已連接描述符 是至關(guān)重要, 否則, 將永遠(yuǎn)不會釋放已連接描述符的文件表條目,由此引發(fā)的存儲器泄露 將最終消耗盡所有的存儲器,導(dǎo)致系統(tǒng)崩潰 (為什么沒有說 子進程關(guān)閉 監(jiān)聽描述符呢?以為子進程總是早于 父進程死掉,所以總是可以釋放?)
- 父進程回收子進程
- 優(yōu)劣:
- 非常清晰的并發(fā)模型: 共享文件表,不共享用戶地址空間。
-
獨立的地址空間容易 使得進程共享狀態(tài)信息變得更加困難。 為了共享信息,需要使用IPC機制, 2. 慢, 進程控制和IPC 的開銷太高
prcess_base.png
-
I/O 多路復(fù)用并發(fā)編程:
- 用來做 并發(fā)事件驅(qū)動(event-driven) 程序的基礎(chǔ), 在事件驅(qū)動程序中,流是因為某種事件而前進的。
- I/O并發(fā)模型中的 邏輯流模型 轉(zhuǎn)換為狀態(tài), 不嚴(yán)格的說, 一個狀態(tài)機就是一組狀態(tài)(state), 輸入事件(input event), 和轉(zhuǎn)移(transitI/On) 其中轉(zhuǎn)移就是將輸入事件和狀態(tài)映射到另一個狀態(tài)。
- 自循環(huán)(self loop) 是同一個輸入和輸出狀態(tài)之間的轉(zhuǎn)移。
- 服務(wù)器使用I/O 多路復(fù)用。 select 函數(shù)檢測輸入事件的發(fā)生。 當(dāng)一個已連接描述符準(zhǔn)備好讀取的時候,服務(wù)器作為響應(yīng)的狀態(tài)機 執(zhí)行轉(zhuǎn)移
- 優(yōu)劣:
- 優(yōu)點:
1. 比基于進程設(shè)計的設(shè)計給了程序員更多的對程序行為的控制,
2. 事件驅(qū)動服務(wù)器運行在單一的進程上下文中, 因為每個邏輯控制流都能否訪問該進程的全部地址空間。這是的在流之間 共享數(shù)據(jù)變得容易。
3. 調(diào)試起來變得簡單,就像順序程序一樣
4. 事件驅(qū)動 常常比基于進程的設(shè)計的要高效的多,因為他們不需要進程上下文的調(diào)度 - 缺點:
1. 編碼復(fù)雜, 不幸的是, 隨著并發(fā)粒度的減小, 復(fù)雜性還會上升,
2. 不能夠充分利用多核處理器
- 優(yōu)點:
-
基于線程的并發(fā)模型:
- 概念:
- 是運行在進程上下文中的邏輯流, 有內(nèi)核自動調(diào)度,每個線程都有自己的線程上下文, 包括一個唯一的整數(shù)線程ID(thread ID TID), 棧,棧指針,程序計數(shù)器,etc,
- 基于線程的并發(fā)模型是結(jié)合 進程、I/O多路復(fù)用流的特性。 同進程一樣內(nèi)核自動調(diào)度, 同I/O復(fù)用一樣, 多個線程運行在單一進程的上下文, 因此共享相同的虛擬地址空間, 代碼,數(shù)據(jù)等
- 主線程: 每個進程開始生命周期時候是單一線程,這個線程成為主線程。 然后創(chuàng)建對等線程。 這個時間點開始,兩個線程開始并發(fā)的執(zhí)行。然后被 系統(tǒng)進行調(diào)度。
- 與進程的不同: 線程的上線文要比進程的上下文小得多。不嚴(yán)格按照父子層次來組織。和一個進程相關(guān)的線程組成一個對等(線程池),主線程和其他線程的區(qū)別在于他總是進程中的第一個運行的線程,對等線程池的概念的主要影響是: 一個線程可以殺死他的任何對等進程,或者等待他的任何對等線程終止,每個對等線程都能讀寫相同的共享數(shù)據(jù)。
- 結(jié)合、分離: 在任何一個時間點, 線程是可結(jié)合的, 或者是分離的。一個可結(jié)合的線程能夠被其他線程收回其他資源和殺死。在被其他線程回收之前,他的存儲器資源是沒有倍釋放的。相反。 一個分離的線程是不能被其他線程回收或殺死的。他的存儲資源在它終止由系統(tǒng)自動釋放。 所以要么顯示的回收,要么 pthread_join , pthread_detach. 在現(xiàn)實中,很好的理由要使用分離線程, web瀏覽器請求時都創(chuàng)建一個新的對等線程, 因為每個連接都是有一個單獨的線程獨立處理的。 對于服務(wù)器而言,就沒有必要等待所有的對等線程終止,所以直接使用分離線程,在他們終止之后,資源就自動回收了。
thread_base.png-
多線程序中的共享變量
- 線程存儲模型: 線程都有自己的線程上下文, 線程ID, 棧, 棧指針, 程序計數(shù)器,條件碼 和通用的寄存器,每個線程和其他線程一起共享進程上下文中的其他部分,包括用戶虛擬地址空間(制度文本代碼, 讀寫數(shù)據(jù),堆,已經(jīng)所有的共享庫代碼和數(shù)據(jù)區(qū)域)也同樣共享打開的文件集合。
- 變量映射到線程:
- 全局變量: 定義在函數(shù)之外,只有一個實例, 任何線程都可以引用。
- 本地自動變量: 定義在函數(shù)內(nèi),沒有static的變量。每個線程棧都包含它自己的所有本地變量的實例。
- 本地靜態(tài)變量: 函數(shù)內(nèi)部static變量。多個線程共享。
-
同步線程 信號量:
- 為了共享全局?jǐn)?shù)據(jù)結(jié)構(gòu)的并發(fā)程序的正確執(zhí)行。
- P(s) : s != 0 那么將s - 1 并返回, 如果s == 0 那么掛起這個線程,直到等待V操作會重啟這個線程, p操作繼續(xù)將S-1,執(zhí)行。
- V(s) : 將s + 1 , 重啟任何一個阻塞的線程。
- 使用信號量, 實現(xiàn)互斥。 應(yīng)用, 生產(chǎn)者--- 消費者, 讀者---寫者,
-
并發(fā)問題:
- 線程安全:
1. 不保護共享變量的函數(shù): 全局變量, static變量
2. 保持調(diào)用狀態(tài)的函數(shù), 例如rand函數(shù)不是線程安全的。當(dāng)前調(diào)用結(jié)果依賴前次調(diào)用的中間結(jié)果, 使rand函數(shù)線程安全的方法是, 重寫他,使其不在依賴static變量。
3. 返回指向靜態(tài)變量的函數(shù)
4. 調(diào)用線程不安全函數(shù)的函數(shù)
5. 主要韓式,使用,返回,依賴,共享變量的問題。 - 可重入函數(shù):
特點在于倍多個線程調(diào)用時,不會引用任何共享數(shù)據(jù),可重入是線程安全的 真子集。
可重入函數(shù)通常要比線程安全的函數(shù)要高效一點: 因為他們不需要同步操作, - 競爭,死鎖
- 線程安全:
-
基于預(yù)線程化的并發(fā)服務(wù)器: 通過生產(chǎn)者消費者一個模型,
- 服務(wù)器是有一個主線程和一組工作者線程構(gòu)成的
- 主線程不斷的接受來自客戶端的連接請求,并講的到的連接請求描述符放到一個緩沖區(qū)中
- 每一個工作者線程 反復(fù)的從共享緩沖區(qū)中消費描述符。
使用線程提高并行性: 操作系統(tǒng)內(nèi)核在多個核上并行的調(diào)度這些并發(fā)線程。并行程序常常被寫為 每個核上只運行一個線程。
- 概念:
-
總結(jié):
- 進程由內(nèi)核調(diào)度,因為擁有獨立的虛擬地址空間, 所以需要顯示的IPC機制,來實現(xiàn)共享數(shù)據(jù),同時 編程模型簡單一致 ( 能否利用多核?)
- 事件驅(qū)動 使用I/O多路復(fù)用 顯示的調(diào)度 并發(fā)邏輯流。以為在同一個進程中,所以共享數(shù)據(jù)變得簡單, 復(fù)雜度比較高 (只能是單核應(yīng)用了,但是能夠高效利用IO,)
- 線程是兩個的結(jié)合, 能夠充分利用多核優(yōu)勢。但是調(diào)用函數(shù),必須具有一種成為線程安全的性質(zhì)。(信息同步,信號量,線程安全函數(shù), 使得編程起來比較困難)
- 無論那種并發(fā)機制,同步對共享數(shù)據(jù)的訪問都是一個困難的問題