程序的機器級表示
程序編碼
編譯
編譯時,提升優化級別,能夠提升運行速度,可是編譯時間會更長,對代碼調試會更困難node
編譯過程
graph LR
A(C預處理器擴展源代碼)-->B(編譯器產生兩個文件的彙編代碼)
B-->C
C(彙編器將彙編代碼轉變爲二進制目標代碼)-->D(連接器將目標文件與標準Unix庫函數合併,產生最終的可執行文件)
複製代碼
彙編代碼與目標代碼
彙編代碼接近於機器代碼,目標代碼是二進制格式,彙編代碼的特色是,可讀性很好的文本格式表示程序員
數據格式
Intel用術語"字(word)"表示16位數據類型,所以稱32位爲雙字,64位爲4字算法
優化程序性能
編寫高效程序
- 選擇一組最好的算法和數據結構
- 編寫出編譯器可以有效優化以轉換成高效可執行代碼的源代碼
編譯技術
- 與機器有關:依賴機器的低級細節
- 與機器無關:不考慮計算機的特性
存儲器別名使用
編譯器必須假設不一樣的指針可能指向存儲器的同一個位置,阻礙編譯器優化shell
消除循環的低效率
Amdahl定律
當咱們加快系統中某一部分的速度時,對系統總體的影響取決於這個部分有多重要和速度提升了多少數據庫
編號高速緩存友好的代碼
讓最多見的狀況運行的更快
核心函數的少許循環編程
循環內部,緩存不命中數量最小
時間局部性
被引用過的一次的存儲器位置在不久的未來被屢次引用數組
空間局部性
若是一個存儲器被引用一次,在不久的未來引用附近的一個存儲器位置瀏覽器
存儲器山
- run函數的參數size和stride容許咱們控制產生讀序列的局部性程度
- size越小,每次讀取的越小,變量引用更快,時間局部性越好;stride越小,寫入越快,空間局部性越好
- 反覆以不一樣的size和stride調用run函數,造成讀帶寬的時間和空間局部性的二維函數,稱爲存儲器山
第二部分
第七章 連接
7.1 編譯器驅動程序
連接
- 連接就是將不一樣部分的代碼和數據組合成一個單一文件的過程,文件可被加載到存儲器執行
- 連接能夠執行於編譯時(源代碼->機器代碼),也能夠加載時(程序加載到內存中執行),,甚至在運行時由應用程序執行
- 早期,連接是手動執行,如今系統中,由叫作連接器的程序自動執行
7.3 目標文件
可重定位目標文件
包含二進制代碼和數據,可在編譯時與其餘可重定位目標文件合併起來,建立一個可執行目標文件緩存
可執行目標文件
包含二進制代碼和數據,可直接拷貝到存儲器並執行安全
共享目標文件
一種特殊類型的可重定位目標文件,可在加載時或運行時,被動態的加載到存儲器並連接
7.4 可重定位目標文件
7.9 加載可執行目標文件
p不是內置的shell命令,因此shell會認爲p是一個可執行目標文件,經過調用駐留在存儲器中稱爲加載器的操做系統代碼來運行p
- 任何Unix程序均可以調用execve函數來調用加載器
- 加載器將可執行目標文件中的代碼和數據從磁盤拷貝到存儲器中,而後跳轉到程序的第1條指令,即入口點(entry point),來運行程序
- 將程序拷貝到存儲器並運行的過程叫作 加載
加載器其實是如何工做的?
- Unix系統中的每一個程序都運行在一個進程上下文中,這個進程上下文有本身的虛擬地址空間
- 當shell運行一個程序時,父shell進程生成一個子進程,它是父進程的一個複製品
- 子進程經過execve系統調用啓動加載器,加載器刪除子進程已有的虛擬存儲器段,並建立一組新的代碼、數據、堆和棧段
- 新的棧和堆段被初始化爲零,經過將虛擬地址空間中的頁映射到可執行文件的頁大小組塊,新的代碼和數據段被初始化爲可執行文件的內容
- 加載器跳轉到_start地址,最終會調用應用的main函數
- 除了一些頭部信息,加載過程當中沒有任何從磁盤到存儲器的數據拷貝,直到CPU引用一個被映射的虛擬頁,纔會進行拷貝,此時,操做系統利用它的頁面調度機制自動將頁面從磁盤傳送到存儲器
7.10 動態連接共享庫
7.12 與位置無關的代碼
共享庫的目的,就是容許多個正在運行的進程,共享存儲器中相同的庫代碼,節省寶貴的存儲器資源
多個進程如何共享一個程序的一個拷貝呢?
給每一個共享庫實現分配一個專用地址空間塊,而後要求加載器老是在這個地址加載共享庫
很差的:1,即便一個進程不使用這個庫,那部分空間仍是會分配
2,難以管理: 咱們得保證沒有組塊會重疊,每次當一個庫修改了,就必須確認它的已分配的組塊仍是和以前的大小,而且,建立了新的庫,得爲它找空間,隨着時間發展,一個系統有數百個庫和各類版本的庫,不免地址空間分裂成大量小的、未使用而又再也不能使用的小洞,並且,每一個系統,從庫到存儲器的分配都是不一樣的
編譯庫代碼
不須要連接器修改庫代碼,能夠在任何地址加載和執行代碼,這樣的代碼叫位置無關的代碼(PIC代碼)
7.13 處理目標文件的工具
第八章 異常控制流
8.1 異常
- 異常是一種形式的異常控制流,它一部分是由硬件實現的,一部分是由操做系統實現的
- 具體細節隨系統不一樣,而有所不一樣
- 對於每一個系統而言,基本的思想都是相同的
異常 就是控制流中的突變,用來響應處理器狀態中的某些變化
異常處理過程
在任何狀況下,當處理器檢測到有事件發生時,他就會經過一張叫作異常表(exception table) 的跳轉表,進行一個間接過程調用(異常),到一個專門設計用來處理這類事件的操做系統子程序---異常處理程序(exception handler)
當異常處理程序完成處理後,根據異常類型,會發生如下三種狀況中的一種:
- 將控制返回給當前指令Icurr(異常發生時正在執行的指令)
- 將控制返回Inext(若是沒有發生異常將會執行的下一條指令)
- 終止被中斷的程序
8.1.1 異常處理
- 每種類型的異常都分配了一個惟一的非負整數的異常號(exception number),一些是由處理器的設計者分配,其餘由操做系統內核(操做系統常駐存儲器的部分)的設計者分配
- 處理器設計者分配:被零除、缺頁、存儲器訪問違例、斷點以及算術溢出
- 操做系統內核分配: 系統調用、來自外部I/O設備的信號
- 系統啓動時,操做系統分配和初始化一張稱爲 異常表 的跳轉表
- 異常號是異常表中的索引,異常表的起始地址放在一個叫作 異常表基寄存器(exception table base register) 的特殊CPU寄存器裏
異常相似於過程調用,不一樣之處:
- 若是控制從一個用戶程序轉移到內核,全部這些項目item都被壓到內核棧中,而不是壓到用戶棧中
- 異常處理程序運行在內核模式下,意味着他們對全部的系統資源都有徹底的訪問權限
- 過程調用時,跳轉以前,處理器將返回地址壓到棧中,然而處理異常時,根據異常類型,返回地址要麼是當前指令,要麼是下一條指令
8.1.3 異常的類型
中斷
- 硬件中斷不是由任何一條專門的指令形成的,從這個意義上來講是異步的
- I/O設備,例如網絡適配器、磁盤控制器和定時器芯片,經過向處理器芯片上的一個管腳發信號,並把異常號放到系統總線上,來觸發中斷,這個異常號標識了引起中斷的設備
陷阱
- 有意的異常,執行一條指令的結果
- 將控制返回到下一條指令
- 用途: 在用戶程序和內核之間提供一個像過程同樣的接口, 叫作系統調用
- 系統調用和普通的函數調用區別: 普通函數運行在用戶模式(user model) ,用戶模式限制了函數能夠執行的指令的類型,只能訪問與調用函數相同的棧;
- 系統調用運行在內核模式(kernel mode)中,內核模式容許系統調用執行指令,並訪問定義在內核中的棧
故障
- 由錯誤狀況引發,可能被故障處理程序修正
- 能修正,控制返回到故障指令,從新執行
- 不能修正,處理程序返回到內核中的abort例程,abort例程會終止引發故障的應用程序
終止
- 終止是不可恢復的致命錯誤形成的結果--典型的是一些硬件錯誤,好比DRAM或者SRAM位被損壞時發生的奇偶錯誤
- 將控制返回給一個abort例程,該例程會終止這個應用程序
8.2 進程
- 當咱們在一個現代系統上運行一個程序時,咱們會獲得一個假象,就好像咱們的程序是系統中當前運行的惟一程序,咱們的程序好像是獨佔的使用處理器和存儲器,處理器就好像是無間斷的一條接一條地執行咱們程序中的指令,最後,咱們程序中的代碼和數據顯得好像是系統存儲器中惟一的對象,這些假象都是經過進程的概念提供給咱們的
- 一個執行中程序的實例
- 每一個程序都運行在某個進程的上下文(context)中
- 上下文由程序正確運行所需的狀態組成的,包括存放在存儲器中的程序的代碼和數據、棧、通用目的存儲器的內容、程序計數器、環境變量以及打開文件描述符的集合
進程提供給應用程序的關鍵抽象
- 一個獨立的邏輯控制流,使咱們以爲獨佔使用處理器
- 一個私有的地址空間,使咱們以爲獨佔的使用存儲器系統
8.2.1 邏輯控制流
- 通常而言,每一個邏輯控制流與其它邏輯流想獨立
- 的那個進程使用進程間通訊(IPC)機制,好比管道、套接口、共享存儲器和信號量,顯示地與其它進程交互時,惟一例外就會發生
- 若干個邏輯流在時間上重疊,被稱爲併發進程,這兩個進程被稱爲併發運行
- 進程和其餘進程輪換運行的概念叫 多任務,一個進程執行它的控制流的一部分的每一時間段叫作時間片
- 多任務 也叫作時間分片
8.2.2 私有地址空間
Linux進程的地址空間的結構
- 地址空間底部的四分之三是預留給用戶程序的,包括一般的文本、數據、堆和棧段
- 頂部的四分之一是預留給內核的(好比執行系統調用時,使用的代碼、數據和棧)
8.2.3 用戶模式和內核模式
- 處理器是用某個控制寄存器中的一個方式位(mode bit)來提供這種功能,描述了進程當前享有的權力
- 運行在內核模式的進程,能夠執行指令集中的任何指令,而且能夠訪問系統中任何存儲器位置
- 用戶模式中的進程不能直接引用地址空間中內核區內的代碼和數據,必須經過系統調用接口間接訪問內核代碼和數據
- 進程從用戶模式變爲內核模式的惟一方法是經過諸如中斷、故障或者陷入系統調用這樣的異常
8.2.4 上下文切換
- 操做系統內核利用一種稱爲上下文切換(context switch)的較高級形式的異常控制流來實現多任務
- 內核能夠決定搶佔當前進程,從新開始一個先前被搶佔的進程,這種決定叫 調度(scheduling) ,是由內核中稱爲 調度器(scheduler) 的代碼處理的
上下文切換過程
- 保存當前進程的上下文
- 恢復某個先前被搶佔進程所保存的上下文
- 將控制傳遞給這個新恢復的進程
- 中斷也可能引起上下文切換,好比,全部的系統都有某種產生週期性定時器中斷的機制,典型的爲每1ms或10ms,每次發生定時器中斷時,內核就能斷定當前進程已經運行了足夠長的時間了,並切換到一個新的進程
切換過程
- 磁盤讀取數據須要較長時間(數量級爲十幾ms),因此內核執行從進程A到進程B的上下文切換,而不是等待
- 注意,切換以前,內核正表明進程A在用戶模式下執行指令
- 在切換的第一步中,內核表明進程A在內核模式下執行指令,而後在某一時刻,內核開始表明B進程執行指令(內核模式),切換完成後,內核表明B在用戶模式下執行指令
- B在用戶模式下運行一下子,直到磁盤發出一箇中斷信號,表示數據已經傳到存儲器,內核斷定B已經運行足夠長時間,就執行一個從B切換A的上下文切換,將控制返回給A中緊隨在read系統調用以後的那條指令,進程A繼續運行,直到下一次異常發生
高速緩存污染和異常控制流
- 通常而言,硬件高速緩存存儲器(L1,L2,L3很是小)不能和諸如中斷和上下文切換這樣的異常控制流很好的交互
- 若是當前進程被中斷,那麼對於中斷處理程序來講,高速緩存是冷的
- 若是處理程序從主存中訪問了足夠多的表目,那麼被中斷的進程繼續時,高速緩存對它來講也是冷的
- 當一個進程在上下文切換後繼續執行時,高速緩存對於應用程序而言也是冷的,必須再次熱身
8.3 系統調用和錯誤處理
- 咱們把系統調用和它們相關的包裝函數可互換地 稱爲 系統級函數
8.4 進程控制
8.4.2
進程的三種狀態
- 運行 進程要麼在CPU上執行,要麼等待被執行且最終會被調度
- 暫停 進程的執行被掛起(suspended) ,且不會被調度 ,當收到SIGSTOP、SIGTSTP、SIDTTIN或者SIGTTOU信號時,進程就暫停,而且保持暫停直到它收到一個SIGCONT信號,這時,進程再次開始運行
- 終止 進程永遠地被中止了; 三種緣由會終止進程: 收到一個信號,該信號的默認行爲是終止進程; 從主程序返回; 調用exit 函數.
fork函數
- 父進程經過調用fork函數建立一個新的運行子進程
- 新建立的子進程幾乎,但不徹底與父進程相同
- 子進程獲得與父進程用戶級虛擬地址控件相同的一份拷貝,包括文本、數據和bss段、堆以及用戶棧,子進程還得到與父進程 任何打開文件描述符 相同的拷貝,意味着 當父進程調用fork時,子進程能夠讀寫父進程中打開的任何文件
- 父進程和新建立的子進程之間最大的區別在於它們有不一樣的PID
- fork函數只被調用一次,卻會返回兩次: 一次是在調用進程(父進程)中,一次在子進程中, 父進程中,fork返回子進程的PID,子進程中fork返回0
- fork返回值用來分辨程序是在父進程仍是在子進程中執行的
8.4.3 回收子進程
- 當一個進程因爲某種緣由終止時,內核並不當即從系統中清除,而是被保持在一種終止狀態中,直到被它的父進程回收(reaped)
- 當父進程回收已終止的子進程時,內核將子進程的退出狀態傳遞給父進程,而後拋棄已終止的進程,今後時開始,該進程就不存在;額
- 一個終止了但還未被回收的進程稱爲 僵死進程(zombie)
- 若是父進程沒有回收它的僵死子進程,就終止了,內核會安排init進程來回收他們
- init進程的PID爲1,而且在系統初始化時,由內核建立
8.4.4 讓進程休眠
8.5 信號
- 更高層軟件形式的異常,稱爲 Unix信號 ,它容許進程中斷其餘進程
- 一個信號就是一條消息,通知進程一個某種類型的事件已經在系統中發生了
- 每種信號類型都對應某個類型的系統事件
- 底層的硬件異常是由內核異常處理程序處理的,對用戶進程而言一般是不可見的
- 信號提供了一種機制向用戶進程通知這些異常的發生
信號的案例
- ctrl-c,內核會發送SIGINT信號給前臺進程
- 一個進程能夠經過發送一個SIGKILL(號碼9)信號強制終止另一個進程
- 當一個子進程終止或者暫停時,內核會發送一個SIGCHLD(號碼17)給父進程
8.5.1 信號術語
- 一個只發出而沒有被接收的信號叫作待處理信號(pending signal)
- 在任什麼時候刻,一個類型至多隻會有一個待處理信號,接下來發送到這個進程的K類型信號不會排隊等待,只是被簡單地丟棄
- 一個進程能夠選擇性得阻塞接收某種信號,當一個信號被阻塞時,仍能夠被髮送,可是產生的待處理信號不會被接收,直到進程取消對這種信號的阻塞
8.5.2 發送信號
進程組
- 每一個進程都只屬於一個進程組,進程組是由一個正整數進程組ID來標識的
- 默認的,一個子進程和它的父進程同屬於一個進程組
- 一個進程能夠經過使用setpgid函數來改變本身或者其餘進程的進程組
8.5.3 接收信號
8.5.4 信號處理問題
- 系統信號能夠被中斷. 像read、write和accept這樣的系統調用潛在的會阻塞進程一段較長的時間,稱之爲 慢速系統調用
- 在某些系統中,當處理程序捕捉到一個信號時,被中斷的慢速系統調用在信號處理程序返回時,再也不繼續,而是當即返回給用戶一個錯誤條件
8.5.5 可移植的信號處理
- Posix標準定義了sigaction函數,顯式的指定他們想要的信號處理語義
- Signal包裝函數設置了一個信號處理程序
- 只有這個處理程序當前正在處理的那種類型的信號被阻塞
- 和全部信號實現同樣,信號不會排隊等待
- 只要可能,被中斷的系統調用會自動重啓
8.6 非本地跳轉
- C提供了一種形式的用戶級異常控制流,稱爲 非本地跳轉(nonlocal jump).它將控制直接從一個函數轉移到另外一個當前正在執行的函數,而不須要通過正常的調用-返回序列
- 非本地跳轉的一個重要應用就是容許從一個深層嵌套的函數調用中當即返回,一般是由檢測到某個錯誤狀況引發的
- 若是在一個深層嵌套的函數調用中發現一個錯誤狀況,可使用非本地跳轉直接返回到一個普通的本地化的錯誤處理程序,而不是費力地解開調用棧
- 非本地跳轉的另外一個重要應用,是使一個信號處理程序分支到一個特殊的代碼位置,而不是返回到被信號到達中斷了的指令的位置
8.7 操做進程的工具
Unix系統提供了大量的監控和操做進程的有用工具:
- strace: 打印一個程序和它的子進程調用的每一個系統調用的軌跡
- ps: 列出系統中當前的進程(包括僵死進程)
- top: 打印出關於當前進程資源使用的信息
- kill: 發送一個信號給進程
- /proc: 一個虛擬文件系統,以ASCII文本格式輸出大量內核數據結構的內容,用戶程序能夠讀取這些內容;好比輸入 "cat /proc/loadavg",觀察在你的Linux系統上當前的平均負載
第九章 測量程序執行時間
9.1 計算機系統上的時間流
- 在宏觀時間尺度上,處理器不停地在許多任務之間切換,一次分配給每一個任務大約5~20ms
- 用戶感受上任務是在同時進行的,由於人不可以察覺短於大約100ms的時間段,在這段時間內,處理器能夠執行幾百萬條指令
9.1.1 進程調度和計時器中斷
- 外部事件,例如鍵盤、磁盤操做和網絡活動,會產生中斷信號,中斷信號使得操做系統調度程序得以運行,可能還會切換到另外一個進程
- 咱們也但願處理器從一個進程切換到另外一個,這樣用戶看上去就像處理器在同時執行許多程序
- 計算機有一個外部計時器,週期性向處理器發送中斷信號,
- 中斷信號之間的時間稱爲 間隔時間(interval time)
- 中斷髮生時,調度程序能夠選擇要麼繼續當前正在執行的進程,要麼切換到另外一個進程
- 間隔時間必須足夠短,保證任務間切換足夠頻繁
- 從一個進程切換到另一個進程須要幾千個時鐘週期來保存當前進程的狀態,而且爲下一個進程準備好狀態,間隔時間過短會致使性能不好
- 典型的計時器間隔範圍是 1~10ms
- 操做系統函數,例如處理缺頁、輸入或者輸出(print函數),打印log很耗性能
9.1.2 從應用程序的角度看時間
9.2 經過間隔計數(interval counting)來測量時間
- 對得到程序性能的近似值有用,粒度太粗,不能用於持續時間小於100ms的測量
- 有系統誤差,太高的估計計算時間,平均大約4%
- 優勢: 它的準確性不是很是依賴於系統負載
9.3 週期計數器
- 運行在時鐘週期級的計時器,是一個特殊的寄存器,每一個時鐘週期他都會加1
- 不是全部的處理器都有這樣的計數器
9.4 用週期計數器來測量程序執行時間
9.5 基於 gettimeofday 函數的測量
9.7 展望將來
系統中引入了幾個對性能測量有很大影響的特性
- 頻率變化的時鐘: 爲了下降功耗,將來的系統會改變時鐘頻率,由於功耗直接與時鐘頻率相關
9.8 現實生活: K次最優測量方法
9.9 獲得的經驗教訓
- 每一個系統都是不一樣的
- 試驗能夠是很是有啓迪性的
- 在負載很重的系統上得到準確的計時特別困難
- 試驗創建必須控制一些形成性能變化的因素 高速緩存可以極大地影響一個程序的執行時間,傳統的技術是在計時開始以前,清空高速緩存中的全部有用的數據,或是在開始時,把一般會在高速緩存中的全部數據都加載進來
第十章 虛擬存儲器
- 存儲器很容易被破壞,若是某個進程不當心寫了另外一個進程使用的存儲器,那麼進程可能以某種徹底和程序邏輯無關的使人迷惑的方式失敗
- 虛擬存儲器是硬件異常、硬件地址翻譯、主存、磁盤文件和內核軟件的完美交互,它爲每一個進程提供了一個大的、一致的、私有地址空間
虛擬存儲器提供了三個重要的能力:
- 他將主存當作是一個存儲在磁盤上的地址空間的高速緩存,在主存中只保存活動區域,並根據須要在磁盤和主存之間來回傳送數據,經過這種方式,高效的使用了主存
- 它爲每一個進程提供了一致的地址空間,從而簡化了存儲器管理
- 他保護了每一個進程的地址空間不被其餘進程破壞
10.1 物理和虛擬尋址
- 計算機系統的主存被組織成一個由M個連續的字節大小的單元組成的數組,每一個字節都有一個惟一的物理地址(physical address),第一個字節的地址爲0,接下來的字節地址爲1,再下一個爲2,依次類推
- CPU訪問存儲器的最天然的方式就是使用物理地址,這種方式稱爲 物理尋址(physical addressing)
- 爲通用計算設計的現代處理器使用的是 虛擬尋址(virtual addressing)
虛擬尋址的過程
- CPU經過生成一個虛擬地址來訪問主存,地址翻譯(address translation)將虛擬地址轉換爲物理地址
- 地址翻譯須要CPU硬件和操做系統之間的緊密合做
- CPU芯片上叫作MMU(memory management unit,存儲器管理單元)的專用硬件,利用存放在主存中的查詢表來動態翻譯虛擬地址,該表的內容由操做系統管理的
10.2 地址空間
- 地址空間是一個非負整數地址的有序集合
- 若是地址空間中的整數是連續的,那麼是一個 線性地址空間
- 在一個帶虛擬存儲器的系統中,CPU從一個有
N = 2^n
複製代碼
個地址的地址空間中生成虛擬地址,稱爲虛擬地址空間
- 一個地址空間的大小是由表示最大地址所須要的位數來描述的
- 現代系統典型地支持32位或64位虛擬地址空間
- 一個系統還有一個物理地址空間,與系統中物理存儲器的M個字節(byte)相對應
10.3 虛擬存儲器做爲緩存的工具
10.3.1 DRAM高速緩存的組織結構
10.3.2 頁表
- 頁表將虛擬頁映射到物理頁,每次地址翻譯硬件將一個虛擬地址轉換爲物理地址時,都會讀取頁表
- 操做系統負責維護頁表的內容,以及在磁盤與DRAM之間來回傳送頁
10.3.3 頁命中
10.3.4 缺頁
- DRAM緩存不命中稱爲 缺頁
- 在磁盤和存儲器之間傳送頁的活動叫作 交換(swapping)或者頁面調度(paging)
- 當有不命中發生時,才換入頁面的這種策略被稱爲按需頁面調度(demand paging)
10.3.5 分配頁面
10.3.6 局部性再次搭救
- 只要咱們的程序有好的時間局部性,虛擬存儲器系統就能工做得至關好
- 若是工做集的大小超出了物理存儲器的大小,那麼程序將產生一種不幸的狀態,叫作顛簸(thrashing),這時頁面將不斷地換進換出
10.4 虛擬存儲器做爲存儲器管理的工具
10.4.1 簡化連接
- 獨立的地址空間容許每一個進程爲它的存儲器映像使用相同的基本格式,而無論代碼和數據實際存放在物理存儲器的何處
10.4.2 簡化共享
- 某些狀況下,須要進程來共享代碼和數據,例如,每一個進程必須調用相同的操做系統內核代碼,而每一個C程序都會調用標準庫中的程序,好比printf
- 操做系統經過將不一樣進程中適當的虛擬頁面映射到相同的物理頁面,從而多個進程共享這部分代碼的一個拷貝,而不是在每一個進程中都包括單獨的內核和C標準庫的拷貝
10.4.3 簡化存儲器分配
- 當進程要求額外的堆空間時,操做系統會分配一個適當數字(例如K)個連續的虛擬存儲器頁面,而且將它們映射到物理存儲器中任意位置的K個任意的物理頁面
- 因爲頁表的工做方式,操做系統沒有必要分配K個連續的物理存儲器頁面,頁面能夠隨機的分散在物理存儲器中
10.4.4 簡化加載
- 映射一個連續虛擬頁面的集合到任意一個文件中的任意一個位置,叫 存儲器映射(memory mapping)
10.5 虛擬存儲器做爲存儲器保護的工具
- SUP位表示進程是否必須運行在內核模式下,才能訪問該頁
- 若是一條指令違反了這些許可條件,那麼CPU就觸發一個通常保護故障,將控制傳遞給一個內核中的異常處理程序,Unix Shell稱這 爲 段錯誤(segmentation fault)
10.6 地址翻譯
- 地址翻譯是一個N元素的虛擬地址空間中的元素和一個M元素的物理地址空間中元素之間的映射
10.7 案例研究:Pentium/Linux存儲器系統
10.8 存儲器映射
10.8.1 再看共享對象
- 一個對象能夠被映射到虛擬存儲器的一個區域,要麼做爲共享對象,要麼做爲私有對象
- 一個共享對象映射到的虛擬存儲器區域叫作共享區域,私有對象映射到的虛擬存儲器區域叫作 私有區域
- 一個進程對共享對象的任何寫操做,對於也把這個共享對象映射到它們虛擬存儲器的其它進程而言,是可見的,並且,這些變化會反映在磁盤上的原始對象中
- 私有對象使用一種叫作寫時拷貝(copy-on-write)的巧妙技術被映射到虛擬存儲器中的
- 兩個進程將一個私有對象進程映射,共享這個對象同一個物理拷貝,對於每一個映射私有對象的進程,相應私有區域的頁表條目都被標記爲只讀,而且區域結構被標記爲私有的寫時拷貝
- 只要沒有進程試圖寫它本身的私有區域,它們就能夠繼續共享物理存儲器中對象的一個單獨拷貝,然而,只要有一個進程試圖寫私有區域內的某個頁面,這個寫操做就會觸發一個保護故障
寫時拷貝過程
- 當寫操做觸發保護故障時,故障處理程序就會在物理存儲器中建立這個頁面的一個新拷貝,更新頁表條目指向這個新的拷貝,而後恢復這個頁面的可寫權限,當故障處理程序返回時,CPU從新執行這個寫操做,在新建立的頁面上,這個寫操做就能夠正常執行了
- 經過延遲私有對象中的拷貝直到最後可能的時刻,寫時拷貝最充分地使用了稀有的物理存儲器
10.8.2 再看fork函數
- fork函數被當前進程調用時,內核爲新進程建立各類數據結構,並分配惟一的PID,對當前進程的mm_struct、區域結構和頁表的原樣拷貝,標記兩個進程中的每一個頁面爲只讀的,並標記兩個進程中的每一個區域結構爲私有的寫時拷貝
- 當fork在新進程中返回時,新進程如今的虛擬存儲器恰好和調用fork時存在的虛擬存儲器相同,當兩個進程中的任一個後來進行寫操做時,寫時拷貝機制就會建立新頁面,所以,也就爲每一個進程保持了私有地址空間的抽象概念
10.8.3 再看execve函數
execve函數在當前進程中加載並運行包含在可執行目標文件a.out中的程序,用a.out程序有效地替代了當前程序
- 刪除已存在的用戶區域
- 映射私有區域
- 映射共享區域
- 設置程序計數器
10.8.4 使用mmap函數的用戶級存儲器映射
Unix進程可使用mmap函數來建立新的虛擬存儲器區域,並將對象映射到這些區域中
10.9 動態存儲器分配
- 雖然可使用低級的mmap和munmap函數來建立和刪除虛擬存儲器的區域,可是大部分C程序在運行時須要額外虛擬存儲器時,使用一種 動態存儲器分配器(dynamic memory allocator)
- 一個動態存儲器分配器維護着一個進程的虛擬存儲器區域,稱爲 堆(heap)
- 在大多數Unix系統中,堆是一個請求二進制零的區域
- 對於每一個進程,內核維護着一個變量brk(break),它指向堆的頂部
- 分配器將堆視爲一組不一樣大小的塊(block)的集合來維護
- 每一個塊就是一個連續的虛擬存儲器組塊(chunk),要麼是已分配,要麼是空閒的
- 已分配塊顯示地保留爲供應用使用,直到被釋放,這種釋放要麼是應用顯示執行,要麼是存儲器分配器自身隱式執行(垃圾回收)
顯示分配器(explicit allocator)
- 應用顯示地釋聽任何已分配的塊,例如,C標準庫的malloc函數分配塊,free函數來釋放一個塊
隱式分配器(implicit allocator)
- 要求分配器檢測什麼時候一個已分配塊再也不被程序使用,而後釋放這個塊,隱式分配器也叫作 垃圾收集器(garbage collector)
- 自動釋放未使用的已分配的塊的過程叫作 垃圾收集(garbage collection)
10.9.1 malloc和free函數
- malloc函數返回一個指針,指向大小爲至少size字節的存儲器塊
- 調用free後,指針仍然指向被釋放的塊,應用在這個塊被一個新的malloc調用從新初始化以前,再也不使用這個指針
10.9.2 爲何要使用動態存儲器分配
10.9.3 分配器的要求和目標
顯式分配器必須在一些約束條件下工做:
- 處理任意請求序列 分配器不能夠假設分配和釋放請求的順序
- 當即響應請求 分配器必須當即響應分配請求,不容許分配器爲了提升性能從新排列或者緩衝請求
- 只使用堆 爲了使分配器是可擴展的,使用的任何非標量數據結構都必須保存在堆裏
- 對齊塊 分配器必須對齊塊,使得它們能夠保存任何類型的數據對象;在大多數系統中,意味着分配器返回的塊是8字節(雙字)邊界對齊的(int64,float64等)
- 不修改已分配的塊 分配器只能操做或改變空閒塊;一旦被分配了,就不容許修改或者移動它
- 目標1:最大化吞吐率 吞吐率:在單位時間裏完成的請求數(例如:1秒鐘500個分配請求,500個釋放請求,吞吐率就是沒秒1000次操做)
- 目標2:最大化存儲器利用率
10.9.4 碎片
- 當未使用的存儲器但不能用來知足分配請求時, 碎片 現象
內部碎片
- 已分配塊比有效載荷大 好比分配器對已分配塊,最小分配8字節,知足對齊約束條件,可是某個有效載荷是2字節
外部碎片
- 空閒存儲器合計起來足夠知足一個分配請求,可是沒有一個單獨的空閒塊足夠大能夠來處理這個請求
- 外部碎片是難以量化和不可預測的,因此分配器典型地採用啓發式策略來試圖維持少許的大空閒塊,而不是維持大量的小空閒塊
10.9.5 實現問題
- 空閒塊組織: 咱們如何記錄空閒塊
- 放置: 咱們如何選擇一個合適的空閒塊來放置一個新分配的塊
- 分割: 在將一個新分配的塊放置到某個空閒塊以後,如何處理這個空閒塊中的剩餘部分?
- 合併: 咱們如何處理一個剛剛被釋放的塊?
10.9.6 隱式空閒鏈表
- 塊=一個字的頭部(4字節,32bit)+有效載荷+填充
- 頭部編碼了快的大小(包括頭部和全部的填充)
隱式空閒鏈表
- 優勢: 簡單
- 缺點: 任何操做的開銷,例如放置分配的塊,要求空閒鏈表的搜索與堆中已分配塊和空閒塊的總數呈線性關係
10.9.7 放置分配的塊
- 當應用請求一個k字節的塊時,分配器搜索空閒鏈表,查找一個足夠大、能夠放置所請求塊的空閒塊,分配器執行這種搜索的方式是由 放置策略(placement policy) 肯定的
常見的策略
首次適配(first fit)
從頭開始搜索空閒鏈表,選擇一個合適的空閒塊
- 優勢: 趨向於將大的空閒塊保留在鏈表的後面
- 缺點: 在靠近鏈表起始處留下小空閒塊的"碎片",增長了對較大塊的搜索時間
下一次適配(next fit)
和首次適配類似,只不過不是從鏈表的起始處開始每次搜索,而是從上一次查詢結束的地方開始
- 源於這樣的想法: 若是咱們上一次在某個空閒塊裏已經發現了一個匹配,那麼極可能下一次咱們也能在這個剩餘塊中發現匹配
- 下一次適配比首次適配運行起來更快一些
- 下一次適配的存儲器利用率比首次適配低得多
最佳適配(best fit)
檢查每一個空閒塊,選擇匹配所需請求大小的最小空閒塊
- 比首次適配和下一次適配的利用率都要高一些
- 缺點: 再簡單空閒鏈表組織結構中,好比隱式空閒鏈表,使用最佳適配 要求對堆進行完全的搜索
10.9.8 分割空閒塊
一旦找到一個匹配的空閒塊,就必須決定,分配這個空閒塊中多少空間
- 用整個空閒塊,這種方式簡單而快捷,缺點是 會形成內部碎片
- 將這個空閒塊分紅兩部分,第一部分變成分配塊,剩下的變成一個新的空閒塊
10.9.9 獲取額外的堆存儲器
若是分配器不能找到合適的空閒塊,將發生什麼?
- 合併那些在存儲器中物理上相鄰的空閒塊來建立一些更大的空閒塊
- 若是1仍是不能生成一個足夠大的塊,或者空閒塊已經最大程度地合併了,分配器會向內核請求額外的堆存儲器,經過調用mmap,或者sbrk函數
3. 以上任一種狀況下,分配器都會將額外的存儲器轉化成一個大的空閒塊,插入到空閒鏈表中,而後將被請求的塊放置在這個新的空閒塊中
10.9.10 合併空閒塊
- 當分配器釋放一個已分配塊,新釋放的空閒塊可能與其它塊相鄰,這些鄰接的空閒塊可能引發一種現象,叫作 假碎片(fault fragmentation)
- 這些假碎片被切割爲小的、沒法使用的空閒塊,3個字+3個字 沒法分配給4個字
- 爲了解決假碎片問題,任何實際的分配器都必須合併相鄰的空閒塊,這個過程稱爲 合併(coalescing)
什麼時候執行合併
當即合併(immediate coalescing)
在每次一個塊被釋放時,就合併全部的相鄰塊 ,能夠在常數時間內執行完成
- 缺點: 在某些請求模式下,塊會反覆的合併,而後立刻分割,產生大量沒必要要的分割和合並
推遲合併(deferred coalescing)
等到某個稍晚的時候再合併空閒塊
- 例如,分配器能夠推遲合併,直到某個分配請求失敗,而後掃描整個堆,合併全部的空閒塊
10.9.11 帶邊界標記的合併
- 當前塊的頭部指向下一個塊的頭部,能夠檢查這個指針以判斷下一個塊是不是空閒的,若是是,就將它的大小簡單地加到當前塊頭部的大小上,這兩個塊在常數時間內被合併
- 邊界標記(boundary tag),容許在常數時間內進行對前面塊的合併
- 在每一個塊的結尾處添加一個 腳部(footer邊界標記),經過檢查腳部,判斷前面一個塊的起始位置和狀態
- 邊界標記的概念是簡單優雅的,對不一樣類型的分配器和空閒鏈表組織都是通用的
- 缺陷: 要求每一個塊都保持一個頭部和一個腳部,在應用程序操做許多個小塊時,會產生顯著的存儲器開銷,例如: 若是一個圖形應用反覆的調用malloc和free,來動態的建立和銷燬圖形節點,而且每一個圖形節點都只要求兩個存儲器字,那麼頭部和腳部將佔用每一個已分配塊的一半的空間
邊界標記的優化方法
只有在前面的塊是空閒時,纔會須要用到它的腳部,若是咱們把前面塊的已分配/空閒位存放在當前塊中多出來的低位中,那麼已分配的塊就不須要腳部了,空閒塊仍然須要腳部
10.9.12 綜合: 實現一個簡單的分配器
10.9.13 顯示空閒鏈表(雙向空閒鏈表)
- 堆能夠組織成一個雙向空閒鏈表,每一個空閒塊中,都包含一個pred(祖先)和succ(後繼)指針
- 使用雙向鏈表,使首次適配的分配時間從塊總數的線性時間減小到了空閒塊數量的線性時間
空閒鏈表中對塊排序的策略
新釋放的塊放置在鏈表的開始處,釋放塊能夠在常數時間內完成,若是使用邊界標記,合併也能夠在常數時間內完成
釋放一個塊須要線性時間的搜索,來定位合適的祖先; 有更高的存儲器利用率,接近最佳適配的利用率
顯式鏈表的缺點
- 空閒塊必須足夠大,以包含全部須要的指針,以及頭部和可能的腳部
- 致使更大的最小塊大小,潛在地提升了內部碎片的程度
10.9.14 分離的空閒鏈表(分離存儲)
-
一種流行的減小分配時間的方法,一般稱爲 分離存儲(segregated storage),維護多個空閒鏈表,其中每一個鏈表中的塊有大體相等的大小
-
通常的思路是將全部可能的塊大小分紅一些等價類,也叫作 大小類(size class)
-
分配器維護着一個空閒鏈表數組,每一個大小類就是一個空閒鏈表,按照大小的升序排列,當分配器須要一個大小爲n的塊時,它就搜索相應的空閒鏈表,若是它不能找到合適的塊與之匹配,它就搜索下一個鏈表,以此類推
-
有不少種分離存儲方法,主要區別在於: 如何定義大小類,什麼時候進行合併,什麼時候向操做系統請求額外的堆存儲器,是否容許分割,等等
簡單分離存儲(simple segregated storage)
- 每一個大小類的空閒鏈表包含大小相等的塊,每一個塊的大小就是這個大小類中最大元素的大小,例如,若是某個大小類定義爲{17-32},那麼這個類的空閒鏈表全由大小爲32的塊組成
- 分配時,檢查相應的空閒鏈表,若是鏈表爲非空,簡單的分配其中第一個塊的所有,空閒塊是不會分割以知足分配請求的
- 若是鏈表爲空,分配器就向操做系統請求一個固定大小的額外存儲器組塊,將這個組塊(chunk)分紅大小相等的塊,並將這些塊連接起來造成新的空閒鏈表
- 要釋放一個塊,只須要簡單地將這個塊插入到相應的空閒鏈表的前部
理解
- 將空閒內存按照固定大小分塊,方便管理,好比大小爲32的塊組成的鏈表,當系統向分配器請求分配的內存大小在17-32這個範圍內是,就從32的鏈表中取一個空閒塊使用
- 有效減小空閒鏈表的數量,下降維護難度
- 有時爲了減小內部碎片,須要減少17-32這個範圍
優勢
- 分配和釋放塊都是很快的常數時間操做
- 每一個組塊中都是大小相等的塊,不分割,不合並,這意味着每一個塊只有不多的存儲器開銷
- 沒有合併,因此已分配塊不須要頭部(已分配/空閒標記),也不須要腳部
- 分配和釋放操做都是在空閒鏈表的起始處操做,因此鏈表只須要是單向的
- 惟一在任何塊中都須要的字段是每一個空閒塊中的一個字的succ指針(後繼),所以最小的塊大小就是一個字
缺點
- 由於空閒塊是不會被分割的,因此會形成內部碎片,好比大量17大小的對象,使用32的塊
- 由於不會合並空閒塊,所以,某些引用模式會引發極多的外部碎片,好比說請求的都是較大對象,大小類比較小的空閒鏈表的利用率就很低,不會合並,被浪費着
合併方式對付外部碎片
分配器記錄操做系統返回的每一個存儲器組塊(chunk)中的空閒塊的數量,不管什麼時候,若是有一個組塊(好比32的大小類組塊)徹底由空閒塊組成,那麼分配器就從當前大小類中刪除這個組塊,回收內存,供其它大小類使用
分離適配(segregated fit)
- 每一個空閒鏈表是和一個大小類相關聯的,而且被組織成某種類型的顯式或隱式鏈表
- 每一個鏈表包含潛在的大小不一樣的塊,這些塊的大小是大小類的成員
過程
- 肯定請求的大小類,對適當的空閒鏈表作首次適配,查找一個合適的塊
- 若是找到一個,那麼分割它,將剩餘的部分插入到適當的空閒鏈表中
- 若是找不到合適的塊,就搜索下一個更大的大小類的空閒鏈表,如此重複,直到找到一個合適的塊
- 若是最後未找到合適的塊,就請求額外的堆存儲器,從這個新的堆存儲器中分配一個塊,將剩餘的部分放置到最大的大小類中
- 要釋放一個塊,咱們執行合併,將結果放置到相應的空閒鏈表中
優缺點
- 是一種常見的選擇,C標準庫中提供的GNU malloc包就是採用這種方法
- 既快速,對存儲器的使用也頗有效率
- 搜索時間減小了,由於搜索被限制在堆的某個部分,而不是整個堆
- 有一個有趣的事實: 對分離空閒鏈表的簡單的首次適配搜索至關於對整個堆的最佳適配搜索
夥伴系統
- 夥伴系統(buddy system) 是分離匹配的一種特例,其中每一個大小類都是2的冪,基本的思路是假設一個堆的大小爲2的m次方個字,咱們爲每一個塊大小爲2的k次方維護一個分離空閒鏈表,其中0<=k<=m
- 請求塊大小向上舍入到最接近的2的冪
- 最開始時,只有一個大小爲2的m次方個字的空閒塊
過程
優勢
缺點
- 要求塊大小爲2的冪可能致使顯著的內部碎片,所以夥伴系統分配器不適合通用目的的工做負載
- 對於某些與應用相關的工做負載,其中塊大小預支知道是2的冪,夥伴系統分配器就頗有吸引力了
10.10 垃圾收集
- 在諸如C malloc包這樣的顯示分配器中,應用經過調用malloc和free來分配和釋放堆塊,應用要負責釋放全部再也不須要的已分配塊
- 垃圾收集器(garbage collector) 是一種動態存儲分配器, 它自動釋放程序再也不須要的已分配塊,這些塊被稱爲 垃圾(garbage)
- 自動回收堆存儲的過程叫作 垃圾收集(garbage collection)
- 在一個支持垃圾收集的系統中,應用顯式分配堆塊,可是從不顯式地釋放它們,垃圾收集器按期識別垃圾塊,並相應地調用free,將這些塊放回到空閒鏈表中
10.10.1 垃圾收集器的基本要素
- 垃圾收集器將存儲器視爲一張 有向可達圖(reachability graph)
- 一組 根節點(root node) 和一組 堆節點(heap node),每一個堆節點對應於堆中的一個已分配塊
- 根節點對應於這樣一種不在堆中的位置,包含指向堆中的指針,能夠是寄存器,棧裏的變量,或者是虛擬存儲器中讀寫數據區域內的全局變量
- 當存在一條從任意根節點出發併到達p的有向路徑時,節點 p是可達(reachable)
- 在任什麼時候刻,和垃圾相對應的不可達節點是不能被應用再次使用的
- 垃圾收集器的角色是 維護可達圖的某種表示,並經過釋放不可達節點並將它們返回給空閒鏈表,來按期地回收它們
- 像Java這樣的語言的垃圾收集器,對應用如何建立和使用指針有很嚴格的控制,可以維護可達圖的一種精確的表示,所以可以回收全部垃圾
- 諸如C和C++這樣的語言的收集器一般不能維持可達圖的精確表示,這樣的收集器叫作 保守的垃圾收集器(conservative garbage collector). 從某種意義上來講它們是保守的,也就是每一個可達塊都被正確地標記爲可達,而一些不可達節點卻可能被錯誤地標記爲可達
10.10.2 Mark&Sweep垃圾收集器
- 由標記(mark)階段和清除(sweep)階段組成
- 標記階段標記出根節點的全部可達的和已分配的後繼,然後面的清除階段釋放每一個未被標記的已分配塊
- 塊頭部中空閒的低位中的一位用來表示這個塊是否被標記了
10.10.3 C程序的保守Mark&Sweep
10.11 C程序中常見的與存儲器有關的錯誤
10.11.1 間接引用壞指針
10.11.2 讀未初始化的存儲器
10.11.3 容許棧緩衝區溢出
- 若是一個程序不檢查輸入串的大小就寫入棧中的目標緩衝區,程序就會緩衝區溢出錯誤(buffer overflow bug)
10.11.4 假設指針和它們指向的對象是相同大小的
10.11.5 形成錯位錯誤
- 建立一個n個元素的指針數組,可是隨後試圖初始化這個數組的n+1個元素.這個過程當中覆蓋了A數組後面的某個存儲器
10.11.6 引用指針,而不是它所指向的對象
第3部分 程序間的交互和通訊
第十一章 系統級I/O
- 輸入/輸出(I/O)是在主存(main memory)和外部設備(例如磁盤驅動器、終端和網絡)之間拷貝數據的過程
- 輸入: 從I/O設備拷貝數據到主存
- 輸出: 從主存拷貝數據到I/O設備
- 在Unix系統中,是經過使用由內核提供的系統級Unix I/O函數來實現這些較高級別的I/O函數的
11.1 Unix I/O
- 全部的I/O設備,例如網絡、磁盤和終端,都被模型化爲文件,而全部的輸入和輸出都被當作對相應文件的讀和寫來執行
- 打開文件
- 改變當前的文件位置
- 讀寫文件
- 關閉文件: 不管進程由於何種緣由終止,內核都會關閉全部打開的文件並釋放它們的存儲器資源
11.2 打開和關閉文件
11.3 讀和寫文件
11.5 讀取文件元數據
11.6 共享文件
11.7 I/O重定向
第十二章 網絡編程
12.1 客戶端-服務器編程模型
- 每一個網絡應用都是基於 客戶端-服務器模型的
- 一個應用是由一個服務器進程和一個或者多個客戶端進程組成
- 基本操做是 事務(transaction)
客戶端-服務器事務與數據庫事務
它不是數據庫事務,並且也沒有數據庫事務的特性,例如原子性,在這裏,事務僅僅是客戶端和服務器之間執行的一系列步驟
12.2 網絡
- 物理上而言,網絡是一個按照地理遠近組成的層次系統,最低層是LAN(Local Area Network,局域網),範圍在一個建築或者校園內
- 最流行的局域網技術是以太網(Ethernet),被證實在3Mb/s~1Gb/s之間都是至關適合的
- 每一個以太網適配器都有一個全球惟一的48位地址
- 一個以太網段包括一些電纜和一個叫作集線器的小盒子,一般服務於一個小的區域,例如一個房間或者一個樓層。集線器不加分辨地將從一個端口上收到的每一個位複製到其餘全部的端口上,所以,每臺主機都能看到每一個位
- 一臺主機能夠發送一段位,稱爲 幀(frame) ,到這個網段內其餘任何主機,每一個主機適配器都能看到這個幀,可是隻有目的主機實際讀取它
- 幀=頭位(header,標識源和目的地址以及幀的長度)+有效載荷(payload)
- 網橋比集線器更充分地利用了電纜帶寬,利用一種聰明的分配算法,它們隨着時間自動學習哪一個主機能夠經過哪一個端口可達.而後有必要時,有選擇地將幀從一個端口拷貝到其它端口
- 例如,若是主機A發送一個幀到同網段上的主機B,當該幀到達網橋X的輸入端口時,它將丟棄此幀,於是節省了其它網段上的帶寬
- 若是主機A發送一個幀到一個不一樣網段上的主機C,那麼網橋X只會把此幀拷貝到和網橋Y相連的端口上,網橋Y會只把此幀拷貝到與主機C的網橋相連的端口
- 在層次更高級別中,多個不兼容的局域網能夠經過叫作 路由器(router) 的特殊計算機鏈接起來,組成一個 internet(互聯網絡)
- internet描述通常概念,而用大寫字母的Internet來描述一種特殊的實際應用(全球IP因特網)
- WAN(Wide-Area Network,廣域網),覆蓋的地理範圍比局域網大
- internet(互聯網絡),它能由採用徹底不一樣和不兼容技術的各類局域網和廣域網組成,
如何使得某臺源主機跨過全部這些不兼容的網絡發送數據位到另外一臺目的主機成爲可能呢?
一層運行在每臺主機和路由器上的協議軟件,它消除了不一樣網絡之間的差別,這個軟件執行一種協議,控制主機和路由器如何協同工做來實現數據傳輸
- 命名方法 不一樣的局域網技術有不一樣和不兼容的方式來爲主機分配地址,internet協議經過定義一種的一致的主機地址格式,消除差別,這個地址唯一的標識了它
- 傳送機制 在電纜上編碼位和將這些位封裝成幀方面,不一樣的網絡互聯技術有不一樣的和不兼容的方式,internet協議經過定義一種把數據位捆紮成不連續的組塊(chunk)--也就是包--的統一方式,消除差別
- 一個包由包頭(header)和有效載荷(payload)組成,其中包頭包括包的大小以及源主機和目的主機的地址,有效載荷包括從源主機發出的數據位
12.3 全球IP因特網
能夠把因特網看作一個世界範圍的主機集合,有如下特性:
- 主機集合被映射爲一組32位的IP地址
- 這組IP地址被映射爲一組稱爲因特網域名(Internet domain name)的標識
- 一個因特網主機上的進程可以經過一個鏈接(connection)和任何其餘因特網主機上的進程通訊
12.3.1 IP地址
- 一個IP地址就是一個32位無符號整數
- IP地址以點分十進制表示法表示
12.3.2 因特網域名
- 因特網客戶端和服務器互相通訊時使用IP地址
- 對人們而言,大整數很難記住,因此定義了一組更加人性化的域名(domain name),以及一種將域名映射到IP地址的機制
- 每臺因特網主機都有本地定義的域名 localhost,老是映射爲本地回送地址(loopback address) 127.0.0.1
12.3.3 因特網鏈接
- 客戶端和服務器經過在鏈接(connection)上發送和接收字節流來通訊
- 從鏈接一對進程的意義上,鏈接是 點對點(point-to-point) 的
- 從數據能夠雙向流動的角度,它是 全雙工(full-duplex) 的
- 套接字(socket)是鏈接的端點(end-point),每一個套接字都有相應的套接字地址,由一個IP地址和一個16位的整數端口組成,"地址:端口"表示
- 客戶端發起鏈接請求時,客戶端socket地址中的端口是由內核自動分配的,稱爲 臨時端口(ephemeral port)
- 服務器socket中的端口一般是某個知名的端口,和服務對應的,例如Web服務器一般用端口80,電子郵件服務器使用端口25
- 一個鏈接是由兩端的套接字地址惟一肯定的,這對套接字地址叫作 套接字對(socket pair)
12.4 套接字接口
- 套接字接口(socket interface) 是一組用來結合Unix I/O函數建立網絡應用的函數,大多數現代系統上都實現了它
12.4.1 套接字地址結構
12.4.2 socket函數
- 客戶端和服務器使用socket函數來建立一個 套接字描述符(socket descriptor)
12.4.3 connect函數
- 客戶端經過調用connect函數來創建和服務器的鏈接的
12.4.4 open_clientfd函數
- 將socket和connect函數包裝成一個叫作open_clientfd的輔助函數是很方便的,當connect函數返回時,咱們返回套接字描述符給客戶端,客戶端就能夠當即開始用Unix I/O和服務器通訊了
12.4.5 bind函數
12.4.6 listen函數
12.4.7 open_listenfd函數
- 將socket、bind和listen函數結合成一個叫作 open_listenfd的輔助函數是頗有幫助的,服務器能夠用它來建立一個監聽描述符
12.4.8 accept函數
- 服務器經過它來等待來自客戶端的鏈接請求
- accept函數等待來自客戶端的請求 到達偵聽描述符listenfd,而後在addr中填寫客戶端的套接字地址,並返回一個已鏈接描述符(connected descriptor),這個描述符可被用來利用Unix I/O函數與客戶端通訊
12.4.9 echo客戶端和服務器的示例
- 簡單的echo服務器一次只能處理一個客戶端,在客戶端間迭代,稱爲 迭代服務器(iterative server)
EOF意味什麼?
- 並無像EOF字符這樣的一個東西
- EOF是由內核檢測到的一種條件,應用程序在它接收到一個由read函數返回的零返回碼時,就會發現出EOF條件
- 對於磁盤文件,當前文件位置超出文件長度時,會發生EOF
- 對於網絡鏈接,當一個進程關閉鏈接,在鏈接的另外一端會發生EOF,另外一端的進程在試圖讀取流中最後一個字節以後,會檢測到EOF
12.5 Web服務器
12.5.1 Web基礎
- Web客戶端和服務器之間的交互用的是一個基於文本的應用級協議,叫 HTTP(Hypertext Transfer Protocol,超文本傳輸協議)
- FTP,文件檢索服務
- HTML(Hypertext Markup Language,超文本標記語言)
12.5.2 Web內容
- 內容是與一個 MIME(Multipurpose Internet Mail Extensions,多用途的網際郵件擴充協議) 類型相關的字節序列
Web服務器以兩種方式向客戶端提供內容
- 取一個磁盤文件,返回給客戶端,瓷盤文件稱爲 靜態內容(static content) ,返回文件給客戶端的過程稱爲 服務靜態內容(serving static content)
- 運行一個可執行文件,將輸出返回給客戶端, 可執行文件產生的輸出稱爲 動態內容(dynamic content),運行程序並返回它的輸出到客戶端的過程稱爲 服務動態內容(serving dynamic content)
12.5.3 HTTP事務
- HTTP標準要求每一個文本行都由一個回車和換行符對來結束
HTTP請求
- HTTP/1.1定義了一些附加的報頭,例如 緩存和安全等高級特性,還支持一種機制,容許客戶端和服務器在同一條**持久鏈接(persistent connection)**上執行多個事務
- HTTP/1.0和HTTP/1.1是互相兼容的,HTTP/1.0的客戶端和服務器會簡單地忽略HTTP/1.1的報頭
- 請求報頭爲服務器提供額外的信息,例如瀏覽器的商標名,或者MIME類型
HTTP響應
- 響應行(response line)+響應報頭(response header)+響應主體(response body)
- 響應報頭提供了關於響應的附加信息,兩個最重要的報頭是Content-Type,它告訴客戶端響應主體中內容的MIME類型;以及Content-Length,用來指示響應主體的字節大小
12.5.4 服務動態內容
- 服務器如何向客戶端提供動態內容? 一個叫作 CGI(Common Gateway Interface,通用網關接口) 的實際標準解決了這個問題
客戶端如何將程序參數傳遞給服務器?
- GET請求的參數在URI中傳遞,"?"字符分隔了文件名和參數,每一個參數用一個"&"分隔開,參數中不容許有空格,必須用字符串""%20"來表示,其它特殊字符,也存在相似的編碼
- POST請求中的參數是在請求主體(request body)中
服務器如何將參數傳遞給子進程
- 在服務器接收到以下請求後(GET /cgi-bin/adder?15000&213 HTTP/1.1),調用fork來建立一個子進程,子進程將CGI環境變量QUERY_STRING設置爲 "15000&213",adder程序在運行時能夠用Unix getenv函數來引用它,並調用execve在子進程的上下文中執行/cgi-bin/adder程序,經常被稱爲CGI程序(遵照CGI標準,經常使用Perl腳本編寫,也常被稱爲CGI腳本)
- 對於POST請求,子進程也須要重定向標準輸入到已鏈接描述符,CGI程序從標準輸入中讀取請求體中的參數
服務器如何將其餘信息傳遞給子進程?
CGI定義了大量的其餘環境變量,CGI程序在運行時,能夠設置這些環境變量
子進程將它的輸出發送到哪裏?
- 在子進程加載並運行CGI程序以前,它使用Unix dup2函數將標準輸出重定向到和客戶端相關聯的已鏈接描述符
- CGI程序將它的動態內容發送到標準輸出
- 父進程不知道子進程生成的內容的類型和大小,因此子進程要負責生成Content-type和Content-length響應報頭,以及報頭後的空行
第十三章 併發編程
- 若是邏輯控制流在時間上重疊,那麼它們就是 併發(concurrent) 的,這種現象,稱爲 併發性(concurrency),出如今計算機系統的許多不一樣層面中,例如 硬件異常處理程序、進程和 Unix 信號處理程序
- 使用應用級併發的應用程序稱爲 併發程序(concurrent program)
應用級並行
在只有一個 CPU 的單處理器上,併發流是交替的,在任什麼時候間點上,都只有一個流在 CPU 上實際執行,然而在有多個 CPU 的機器,稱爲多處理器,能夠真正地同時執行多個流,被分紅併發流的並行應用,在多處理器的機器上運行得快不少
當一個應用正在等待來自慢速 I/O 設備(例如磁盤)的數據到達時,內核會運行其餘進程,使 CPU 保持繁忙,經過交替執行 I/O 請求和其餘有用的工做,來使用併發性
與計算機交互的人要求計算機能同時執行多個任務的能力,例如,打印文檔時,可能想要調整一個窗口的大小,每次用戶請求某種操做時,一個獨立的併發邏輯流被建立來執行這個操做
好比,一個動態存儲分配器能夠經過推遲與一個運行在較低優先級上的併發"合併"流的合併,使用空閒時的 CPU 週期,來下降單個 free 操做的延遲
建立一個併發服務器,爲每一個客戶端建立各自獨立的邏輯流,同時爲多個客戶端服務
三種基本的構造併發程序的方法
每一個邏輯控制流都是一個進程,由內核來調度和維護。進程有獨立的虛擬地址空間,想要和其餘流通訊,控制流必須使用某種顯式的 進程間通訊(interprocess communication,IPC) 機制
應用程序在一個進程的上下文中顯式地調度它們本身的邏輯流,邏輯流被模型化爲狀態機,做爲數據到達文件描述符的結果,主程序顯式的從一個狀態轉換到另外一個狀態,程序是一個單獨的進程,全部的流都共享同一個地址空間
線程是運行在一個單一進程上下文中的邏輯流,由內核調度。能夠理解成其餘兩種方式的混合體,像進程流同樣由內核進行調度,而像I/O多路複用流同樣共享一個虛擬地址空間
13.1 基於進程的併發編程
- 在父進程中接受客戶端鏈接請求,而後建立一個新的子進程爲每一個新客戶端提供服務
- 在服務器派生一個子進程,這個子進程獲取服務器描述符表的完整拷貝,子進程關閉它的監聽描述符,而父進程關閉它的已鏈接描述符
13.1.1 基於進程的併發服務器
- 一般服務器會運行很長時間,因此須要一個SIGCHLD處理程序,來回收僵死(zombie)子進程的資源,當SIGCHLD處理程序執行時,SIGCHLD信號是阻塞的,而Unix信號是不排隊的,因此SIGCHLD處理程序必須準備好回收多個僵死子進程的資源
- 父子進程必須關閉它們各自的connfd拷貝(描述符),以免存儲器泄漏
- 由於套接字的文件表表項中的引用計數,直到父子進程的connfd都關閉了,到客戶端的鏈接纔會終止
13.1.2 關於進程的優劣
- 父子進程間共享狀態信息,模型:共享文件表,可是不共享用戶地址空間
獨立的進程地址空間
- 優勢: 一個進程不可能不當心覆蓋另外一個進程的虛擬存儲器
- 缺點: 使得進程共享狀態信息變得更加困難,爲了共享信息,它們必須使用顯式的IPC機制(每每比較慢,進程控制和IPC的開銷很高)
13.2 基於I/O多路複用的併發編程
- 服務器必須響應兩個互相獨立的I/O事件: 網絡客戶端發起鏈接請求;用戶在鍵盤輸入命令行
- I/O多路複用(I/O multiplexing)技術, 基本思路是,使用select函數,要求內核掛起進程,只有在一個或多個I/O事件發生後,纔將控制返回給應用程序
- 問題: 一旦鏈接到某個客戶端,就會連續回送輸入行,直到客戶端關閉鏈接,此時,輸入一個命令到標準輸入,將不會獲得響應,直到服務器和客戶端之間結束,更好的辦法是更細粒度的多路複用,服務器每次循環(至多)回送一個文本行
13.2.1 基於I/O多路複用的併發事件驅動服務器
13.2.2 I/O多路複用技術的優劣
優勢
- 比基於進程的設計給了程序員更多的對程序行爲的控制,例如,能夠設想編寫一個事件驅動的併發服務器,爲某些客戶端提供它們須要的服務
- 運行在單一進程上下文中,每一個邏輯流均可以訪問進程的所有地址空間,流之間共享數據很容易,能夠利用調試工具(例如GDB)來調試程序,就像對順序程序那樣
- 事件驅動設計經常比基於進程的設計要明顯高效的多,不要求有進程上下文切換來調度新的流
缺點
- 編碼複雜,例如,事件驅動的併發服務器的代碼比基於進程的服務器多三倍,而且隨着併發性粒度的減少,複雜性還會上升
- 粒度: 指每一個邏輯流每次時間片執行的指令數目
13.3 基於線程的併發編程
- 基於進程和基於I/O多路複用兩種方法的混合
- 一個線程是運行在一個進程上下文中的邏輯流,由內核自動調度,每一個線程有本身的線程上下文(thread context),包括一個惟一的整數線程ID(Thread ID,TID)、棧、棧指針、程序計數器、通用目的寄存器和條件碼
- 運行在一個進程裏的全部線程共享該進程的整個虛擬地址空間
13.3.1 線程執行模型
- 每一個進程開始生命週期時,都是單一線程,這個線程稱爲主線程(main thread)
- 在某一時刻,主線程建立一個 對等線程(peer thread),從這個時間點開始,兩個線程就併發運行
進程和線程不一樣點
- 線程上下文比進程上下文小得多,切換快得多
- 線程不像進程那樣,不是按照嚴格的父子層次來組織的,和一個進程相關的線程組成一個對等(線程)池(a pool of peers),獨立於其它進程建立的線程,一個線程能夠殺死它的任何對等線程,或者等待它的任意對等線程終止,每一個對等線程都能讀寫相同的共享數據
13.3.2 Posix線程
- Posix線程(Pthreads)是在C程序中處理線程的一個標準接口,在大多數Unix系統上均可用
- 定義了大約60個函數,容許程序建立、殺死和回收線程,與對等線程安全地共享數據,通知對等線程系統狀態的變化
- 線程的代碼和本地數據被封裝在一個 線程例程(thread routine) 中,能夠理解成Golang中,傳一個func進去
13.3.3 建立線程
13.3.4 終止線程
一個線程是如下列方式之一來終止的
- 當頂層的線程例程返回時,線程會隱式地終止
- pthread_exit 函數: 子線程調用pthread_exit 函數,線程會 顯式的終止 ,該函數會返回一個指向返回值 thread_return的指針
- 主線程調用用pthread_exit 函數,會等待全部其它對等線程終止,而後再終止主線程和整個進程,返回值爲thread_return
- Unix的exit函數: 某個對等線程調用Unix的exit函數,終止進程以及全部與該進程相關的線程
- pthread_cancle函數: 另外一個對等線程,經過調用pthread_cancle函數來終止指定線程ID對應的線程
13.3.5 回收已終止線程的資源
- 線程經過調用 pthread_join函數 等待指定線程終止
- pthread_join函數會阻塞,直到線程tid終止,而後回收已終止線程佔用的全部存儲器資源
13.3.6 分離線程
- 在任何一個時間點上,線程是 可結合的(joinable) 或者 分離的(detached)
- 一個結合的線程可以被其它線程收回其資源和殺死,在被回收前,它的存儲器資源(棧)是不釋放的
- 一個分離的線程是不能被其它線程回收或殺死的,它的存儲器資源在它終止時由系統自動釋放
- 默認下,線程被建立成可結合的
- 爲了不存儲器泄漏,每一個可結合線程都應該要麼被其它線程顯式的收回,要麼調用 pthread_detach函數 被分離
13.3.7 初始化線程
- pthread_once函數容許你初始化與線程例程相關的狀態
13.3.8 一個基於線程的併發服務器
- 傳遞已鏈接描述符的指針給子線程
- 不顯式的收回線程,必須分離每一個線程,資源才能在終止時被系統收回
13.4 多線程中的共享變量
13.4.1 線程存儲器模型
- 一組併發線程運行在一個進程的上下文中,每一個線程都有本身獨立的線程上下文(包括線程ID、棧、棧指針、程序計數器、條件代碼和通用目的的寄存器值)
- 多個線程共享進程上下文,包括整個用戶虛擬地址空間,打開文件的集合
- 寄存器從不共享,虛擬存儲器老是共享的
13.4.2 將變量映射到存儲器
多線程的C程序中的變量
- 全局變量 定義在函數以外,虛擬存儲器的讀/寫區域只包含一個實例
- 本地自動變量(局部變量) 函數內部定義的沒有static屬性的變量,在運行時,每一個線程的棧都包含它本身的全部局部變量的實例,即便多個線程執行同一個函數,局部變量都屬於線程各自獨有
- 本地靜態變量 定義在函數內部並static屬性的變量,和全局變量同樣,虛擬存儲器的讀/寫區域只包含一個實例
13.4.3 共享變量
13.5 用信號量同步線程
13.5.2 利用信號量訪問共享變量
- 一種叫作信號量(semaphore)的特殊類型變量,信號量s是具備非負整數值的全局變量,只能由兩個特殊的操做來處理,稱爲P和V
- P(s):若是s非零,P將s減1,而且當即返回,若是s爲零,就掛起進程,阻塞直到s變爲非零,而後被V操做重啓,完成P操做,得到控制權
- V(s):將s加1,若是有任何進程阻塞在P操做,那麼V操做會重啓這些進程中的一個
二進制信號量
- 將每一個共享變量(或相關共享變量集合)與一個信號量s(初始爲1)聯繫起來,而後用P和V操做將相應的臨界區包圍起來,它的值老是0或者1,因此叫作 二進制信號量
- 進度圖
- 由P和V操做建立的禁止區使得在任什麼時候間點上,在被包圍的臨界區中,不可能有多個線程在執行指令,信號量操做確保了對臨界區的互斥訪問,通常現象稱爲 互斥(mutual exclusion)
- 目的是提供互斥的二進制信號量一般叫作互斥鎖(mutex),在互斥鎖上執行一個P操做叫作加鎖,V操做叫作解鎖,一個線程已經對一個互斥鎖加鎖但尚未解鎖,被稱爲佔用互斥鎖
13.5.3 Posix信號量
- Posix標準定義了許多操做信號量的函數,三個基本的操做是sem_init、sem_wait(P操做)和sem_post(V操做)
13.5.4 利用信號量來調度共享資源
其它同步機制
- Java線程是用一種叫作Java監控器(Java Monitor)的機制來同步的,提供了對信號量互斥和調度能力的更高級別的抽象
13.6 綜合:基於預線程化的併發服務器
13.7 其它併發性問題
13.7.1 線程安全
- 一個函數,當且僅當被多個併發線程反覆地調用時,它會一直產生正確的結果,被稱爲線程安全的(thread-safe)
第1類:不保護共享變量的函數
- 修改一個未受保護的變量
- 解決: 利用相似P和V操做這樣的同步操做來保護變量,優勢是調用程序不須要作任何修改,缺點是同步操做將減慢程序的執行時間
第2類:保護跨越多個調用的狀態的函數
- 函數共享一個全局變量
- 解決:調用者在函數參數中傳遞狀態信息,缺點:須要被迫修改調用程序中的代碼
第3類:返回指向靜態變量的指針的函數
- 共享了一個全局變量
- 解決:1,重寫函數 2,使用lock-and-copy(加鎖-拷貝)技術,定義一個線程安全的包裝函數(wrapper),執行lock-and-copy,經過調用這個包裝函數來取代全部對線程不安全函數的調用
第4類:調用線程不安全函數的函數
13.7.2 可重入性
- 一類重要的線程安全函數,叫作 可重入函數(reentrant function)
- 特色: 當被多個線程調用時,不會引用任何共享數據
- 可重入函數一般比不可重入的線程安全的函數高效一些,由於不須要同步操做
- 若是全部的函數參數都是傳值傳遞(沒有指針),而且全部的數據引用都是本地的自動棧變量(也就是,沒有引用靜態或全局變量),那麼函數就是 顯式可重入的(explicitly reentrant) ,不管它是如何被調用,均可以判定它是可重入的
- 加入顯式可重入函數中一些參數能夠傳指針,那麼就獲得一個 隱式可重入函數(implicitly reentrant)函數 ,即,在調用線程當心地傳遞指向非共享數據的指針時,它是可重入的
13.7.3 在多線程中使用已存在的庫函數
- 大多數Unix函數和定義在標準c庫中的函數都是線程安全的,只有一小部分是例外
- Unix系統提供大多數線程不安全函數的可重入版本,老是以"_r"後綴結尾
13.7.4 競爭
- 緣由: 程序員假設線程將按照某種特殊的軌線穿過執行狀態空間
- 多線程必須對任何可行的軌線都正確工做
13.7.5 死鎖
- 死鎖(deadlock) ,指的是一組線程被阻塞了,等待一個永遠也不會爲真的條件
避免死鎖
- 互斥鎖加鎖順序規則: 若是對於程序中每對互斥鎖(s,t),每一個既包含s也包含t的線程都按照相同的順序同時對它們加鎖,那麼這個程序就是無死鎖的
- 即,兩個線程都是從P(s)->P(t)