標籤:
異常控制流程
系統必須能對系統狀態的變化做出反應,這些系統狀態不是被內部程式變數捕獲,也不一定和程式的執行相關。
現代系統通過使控制流程 發生突變對這些情況做出反應。我們稱這種突變為異常控制流程( Exceptional Control Flow,ECF)
異常控制流程發生在系統的各個層次。
理解ECF很重要
- 理解
ECF將協助你理解重要的系統概念。
- 理解
ECF將協助你理解應用程式如何與作業系統互動
- 通過陷阱(
trap)或者系統調用(system call)的ECF形式,向作業系統請求服務。
- 理解
ECF將協助你編寫有趣的應用程式
- 理解
ECF將協助你理解並發
- 理解
ECF將協助你理解軟體異常如何工作。
這一章你將理解如何與作業系統互動,這些互動都圍繞ECF
8.1 異常
異常是異常控制流程的一種,一部分由硬體實現,一部分由作業系統實現。
8.1.1 異常處理
為每個異常分配了一個非負的異常號(exception number)
- 一些號碼由處理器設計者分配
- 其他號碼由作業系統核心的設計者分配。
系統啟動時,作業系統分配和初始化一張稱為異常表的跳轉表。
異常表的地址放在叫異常表基底位址暫存器的特殊CPU寄存器中。)
異常類似程序呼叫,不過有以下不同
- 程序呼叫,跳轉到處理常式前,處理器將返回地址壓入棧中。對於異常,返回地址是當前,或下一跳指令。
- 處理器會把額外的處理器狀態壓入棧中。
- 如果控制一個使用者程式到核心,那麼所有這些項目會被壓入核心棧中,而是使用者棧。
- 例外處理常式運行在核心模式下,這意味他們對所有系統資源有完整存取權限。
8.1.2 異常的類別
異常分為一下四類:中斷(interrupt),陷阱(trap),故障(fault)和終止(abort)。
中斷
中斷是非同步發生,是來自處理器外部的I/O裝置的訊號的結果。硬體中斷不是由任何一條專門的指令造成,從這個意義上它是非同步。
- 硬體中斷的例外處理常式通常稱為中斷處理常式(interrupt handle)
- I/O裝置通過向處理器晶片的一個引腳發訊號,並將異常號放到系統匯流排上,以觸發中斷。
- 在當前指令執行完後,處理器注意到中斷引腳的電壓變化,從系統匯流排讀取異常號,調用適當的中斷處理常式。
- 當處理常式完成後,它將控制返回給下一條本來要執行的指令。
-
剩下的異常類型(陷阱,故障,終止)是同步發生,執行當前指令的結果。我們把這類指令叫做故障指令(faulting instruction).
陷阱和系統調用
陷阱是有意的異常,是執行一個指令的結果。也會返回到下一跳本來要執行的指令。
陷阱最重要的用途是在使用者程式和核心之間提供一個像過程一樣的介面,叫做系統調用
- 使用者程式經常需要向核心請求服務。
- 讀檔案(read)
- 建立進程(fork)
- 新的程式(execve)
- 終止當前進程(exit)
- 為了運行對這些核心服務的受控訪問,處理器提供了一條特殊的
syscall n的指令
- 系統調用是運行在核心模式下,而普通調用是使用者模式下。
故障
故障由錯誤引起,可能被故障處理常式修正。
- 如果能被修正,返回引起故障的指令。
- 否則返回
abort常式,進行終結。
終止
- 終止是不可恢複的致命錯誤造成的結果,通常是一些硬體錯誤,比如DRAM和SRAM被損壞。
- 終止處理常式從不將控制返回給應用程式。返回一個
abort常式。
8.1.3 Linux/IA32 系統中的異常
研究程式如何使用int指令直接調用Linux 系統調用是很有趣的。所有到Linux系統調用的參數都是通過通用寄存器而不是棧傳遞。
慣例
- %eax 包含系統調用號
- %ebx,%ecx,%edx,%esi,%edi,%ebp包含六個任意的參數。
- %esp不能使用,進入核心模式後,核心會覆蓋它。
系統級函數寫的hello world
int main(){ write(1,"hello,world\n",13); exit(0);}
彙編寫的hello world
string: "hello world\n"main: movl $4,%eax movl $1,%ebx movl $String,%ecx movl $len,%edx int $0x80 movl $1,%eax movl $0,%ebx int $0x80
8.2 進程
異常是允許作業系統提供進程的概念的基本構造快,進程是電腦科學中最深刻,最成功的概念之一。
- 假象,覺得我們的程式是系統中唯一運行著的程式。我們的程式好像獨佔處理器和儲存空間。
- 這些假象都是通過進程概念提供給我們的。
進程經典定義:一個執行中的程式執行個體.
- 系統中每個程式都是運行某個進程的
上下文中的。
- 上下文是由程式正確運行所需的狀態組成。
- 這個狀態包括儲存空間中的代碼和資料,它的棧,通用目的寄存器,程式計數器,環境變數等。
進程提供的假象
8.2.1 邏輯控制流程
8.2.2 並發流
你吃飯吃到一半,電話來了,你一直到吃完了以後才去接,這就說明你不支援並發也不支援並行。
你吃飯吃到一半,電話來了,你停了下來接了電話,接完後繼續吃飯,這說明你支援並發。
你吃飯吃到一半,電話來了,你一邊打電話一邊吃飯,這說明你支援並行。
並發的關鍵是你有處理多個任務的能力,不一定要同時。
並行的關鍵是你有同時處理多個任務的能力。
8.2.3 私人地址空間
進程 為個程式好像獨佔了系統地址空間。
- 一個
進程為每個程式提供它自己的私人地址空間。
- 不同系統一般都用相同的結構。
8.2.4 使用者模式和核心模式
處理器提供一種機制,限制一個應用程式可以執行的指令以及它可以訪問的地址空間範圍。這就是使用者模式和核心模式。
8.2.5 環境切換作業系統核心使用一種稱為
環境切換的 較高層次 的
異常控制流程來實現多任務。
- 環境切換機制建立在之前討論的較低層次異常機制上的。
核心為每個進程維護一個
上下文。
- 上下文就是重新啟動一個被搶佔的進程所需的狀態。
- 由一些對象的值組成
- 通用目的寄存器
- 浮點寄存器
- 程式計數器(PC)
- 使用者棧
- 狀態寄存器
- 核心棧
- 各種核心資料結構
- 描繪地址空間的頁表
- 包含當前進城資訊的進程表
- 進程已開啟檔案資訊的檔案表
在進程執行的某些時刻,核心可以決定搶佔當前進程,並重新開始一個先前被搶佔的進程。這種決定叫做調度(shedule),由核心中稱為調度器(scheduler)的代碼處理的。
- 當核心選擇一個新的進程運行時,我們就說核心調度了這個進程。
當調度進程時,使用一種環境切換的機制來控制轉移到新的進程
- 儲存當前進程的上下文
- 恢複某個先前被搶佔的進程被儲存的上下文
- 將控制傳遞給這個新恢複的進程
- 什麼時候會發生環境切換
- 核心代表使用者執行系統調用。
- 如果系統調用因為某個事件阻塞,那麼核心可以讓當前進程休眠,切換另一個進程。
- 或者可以用
sleep系統調用,顯式請求讓調用進程休眠。
- 即使系統調用沒有阻塞,核心可以決定執行內容切換
- 中斷也可能引發環境切換。
- 所有系統都有某種產生周期性定時器中斷的機制,典型為1ms,或10ms。
- 每次定時器中斷,核心就能判斷當前進程運行了足夠長的時間,切換新的進程。
快取汙染和異常控制流程
一般而言,硬體快取儲存空間不能和諸如中斷和環境切換這樣的異常控制流程很好地互動,如果當前進程被一個中斷暫時中斷,那麼對於中斷處理常式來說快取器是冷的。如果處理常式從主存訪問足夠多的表項,被中斷的進程繼續的時候,快取對於它來說也是冷的,我們稱中斷處理常式汙染了快取。使用 環境切換也會發生類似的現象。
8.3 系統調用錯誤處理
當Unix系統級函數遇到錯誤時,他們典型地返回-1,並設定全域變數errno來表示什麼出錯了。
if((pid=fork()<0){ fprintf(stderr,"fork error: %s\n", strerror(errno)); exit(0);}
- strerror 函數返回一個文本串,描述了個某個errno值相關聯的錯誤。
8.4 進程式控制制8.4.1 擷取進程ID
#include<sys/types.h>#include<unistd.h>pid_t getpid(void);pid_t getppid(void);
- PID是每個進程唯一的正數。
getpid()返回調用進程的PID,getppid()返回它的父進程的PID。
- 返回一個類型
pid_t的值,在Linux系統下在type.h被定義為int
8.4.2 建立和終止進程
進程總是處於下面三種狀態
- 運行。進程要麼在CPU中執行,要麼等待執行,最終被核心調度。
停止。進程的執行被掛起,且不會被調度。
- 收到
SIGSTOP,SIGTSTP,SIDTTIN或者SIGTTOU訊號,進程就會停止。
- 直到收到一個
SIGCONT訊號,在這個時刻,進程再次開始運行。
訊號是一種軟體中斷的形式。
終止。進程永遠停止。
- 收到一個訊號。訊號預設行為是終止進程。
- 從主程式返回
- 調用exit函數
- exit函數以
status退出狀態來終止進程(另一種設定方式在main中return )
子進程
父進程通過調用fork函數建立一個新的運行子進程
#include<sys/types.h>#include<unistd.h>pid_t fork(void);返回:子進程返回0,父進程返回子進程的PID,如果出錯,返回-1;
新建立的子進程幾乎但不完全與父進程相同。
- 調用一次,返回兩次。
並發執行
- 父進程和子進程是並發啟動並執行獨立進程。
- 核心可能以任意方式覺得執行他們的順序。
- 不能對不同進程中指令的交替執行做任何假設。
相同但是獨立的地址空間
- 在剛調用時,幾乎什麼都是相同的。
- 但是它們都有自己的私人空間,之後對x的改變是相互獨立的。
共用檔案
- 父進程和子進程都把他們的輸出顯示在螢幕上。
- 子進程繼承了父進程所有開啟的檔案。
畫進程圖會有協助。
8.4.3 回收子進程
當一個進程由於某種原因終止時,核心並不是立即把它從系統中清除。相反,進程被保持在一種已終結的狀態,知道被它的父進程 回收(reap)。
當父進程回收已終止的子進程時,核心將子進程的退出狀態傳遞給父進程,然後拋棄已終止的進程。
一個終止了但還未被回收的進程叫做僵死進程
如果父進程沒有回收,而終止了,那麼核心安排init進程來回收它們。
init進程的的PID位1,在系統初始化時由核心建立的。
- 長時間啟動並執行程式,如shell,伺服器,總是應該回收他們的僵死子進程
一個進程可以通過調用waitpid函數來等待它的子進程終止或停止
#include<sys/types.h>#include<sys/wait.h>pid_t waitpid(pid_t pid ,int *status, int options);返回:如果成功,則為子進程的PID,如果WNOHANG,則為0,如果其他錯誤,則為-1.
waitpid函數有點複雜。預設(option=0)時,waitpid掛起調用進程的執行,知道它的等待集合中的一個子進程終止,如果等待集合的一個子進程在調用時刻就已經終止,那麼waitpid立即返回。在這兩種情況下,waitpid返回導致waitpid返回的已終止子進程的PID,並且將這個已終止的子進程從系統中去除。
[CSAPP筆記][第八章異常控制流程]