第八章 異常控制流
控制流:控制轉移序列。
控制轉移:從一條指令到下一條指令。
異常控制流:現代作業系統通過使控制流發生突變來對系統狀态做出反應,這些突變稱為異常控制流。
8.1 異常
8.1.1 異常處理
- 異常表:當處理器檢測到有事件發生時,它會通過跳轉表,進行一個間接過程調用(異常),到異常處理程式。
- 異常号:系統中可能的某種類型的異常都配置設定了一個唯一的非負整數的異常号。異常号是到異常表中的索引。
- 異常類似于過程調用,但有一些重要的不同之處:
(1)過程調用時,在跳轉到處理程式之前,處理器将傳回位址壓入棧中,然而,根據異常的類型,傳回位址要麼是目前指令(當事件發生時正在執行的指令)要麼是下一條指令(如果事件不發生,将會在目前指令後執行的指令)。
(2)處理器也把一些額外的處理器狀态壓到棧裡,在處理程式傳回時,重新開始被中斷的程式會需要這些狀态。比如,一個A32系統将包含目前條件碼和其他内容的EHAGS寄存器壓入棧中。
(3)如果控制從一個使用者程式轉移到核心,那麼所有這些項目都被壓到核心棧中,而不是壓到使用者棧中。 (4)異常處理程式運作在核心模式下(見824節)這意味着它們對所有的系統資源都有完全的通路權限
8.1.2 異常的類别
異常的類别——中斷、陷阱、故障和終止
- 中斷:中斷是異步發生的,硬體中斷的異常處理程式通常稱為中斷處理程式。
- 陷阱和系統調用:系統調用是一些封裝好的函數,内部通過指令int n實作。 陷阱最重要的用途是提供系統調用。系統調用運作在核心模式中,并且可以通路核心中的棧。 系統調用的參數是通過通用寄存器而不是棧來傳遞的,如,%eax存儲系統調用号,%ebx,%ecx,%edx,%esi,%edi,%ebp最多存儲六個參數,%esp不能用,因為進入核心模式後,會覆寫掉它。
- 故障 :一個經典的的故障示例是缺頁異常,當指令引用一個虛拟位址,而該虛拟位址相對應的實體頁面不在存儲器中,是以必須從磁盤中取出時,就會發生故障。
- 終止: 終止是不可恢複的緻命錯誤造成的結果,通常是一些硬體錯誤,比如DRAM或者SRAM位被損壞時發生的奇偶錯誤。終止處理程式從不将控制傳回給應用程式。處理程式将控制傳回給一個abort例程,該例程會終止這個應用程式。
8.1.3 linux/ia32系統中的異常
- Linux/IA32故障和終止
除法錯誤 、一般保護故障 、缺頁 、機器檢查
- Linux/IA32系統調用 :每個系統調用都有一個唯一的整數号,對應于一個到核心中跳轉表的偏移量。
8.2 程序
- 程序(作業系統層):邏輯控制流,私有位址空間,多任務,并發,并行,上下文,上下文切換,排程。
- 程序就是一個執行中的程式執行個體。系統中的每個程式都是運作在某個程序的上下文中的。
- 程序提供給應用程式的關鍵抽象:a)一個獨立的邏輯控制流 ;b)一個私有的位址空間。
8.2.1 邏輯控制流
程式計數器(PC)值的序列叫做邏輯控制流,簡稱邏輯流。
8.2.2 并發流
- 并發流:并發流一個邏輯流的執行在時間上與另一個流重疊,叫做并行流
- 并發:多個流并發執行的一般現象稱為并發。
- 多任務:多個程序并發叫做多任務。
- 并行:并發流在不同的cpu或計算機上,叫做并行。
8.2.3 私有位址空間
一個程序為每個程式提供它自己的私有位址空間。
8.2.4 使用者模式和核心模式
- 運作應用程式代碼的程序初始時是在使用者模式中的。程序從使用者模式變為核心模式的唯一方法是通過異常。
- linux提供了/proc檔案系統,它允許使用者模式程序通路核心資料結構的内容。
8.2.5 上下文切換
- 上下文切換:作業系統核心使用叫上下文切換的異常控制流來實作多任務。
- 上下文切換:a)儲存目前程序的上下文;b)恢複某個先前被搶占的程序被儲存的上下文; c)将控制傳遞給這個新恢複的程序
- 排程:核心中的排程器實作排程。
- 當核心代表使用者執行上下文切換時,可能會發生上下文切換。如果系統調用發生阻塞,那麼核心可以讓目前程序休眠,切換到另一個程序,如read系統調用,或者sleep會顯示地請求讓調用程序休眠。一般,即使系統調用沒有阻塞,核心亦可以決定上下文切換,而不是将控制傳回給調用程序。
- 中斷也可能引起上下文切換。如,定時器中斷。
8.3 系統調用錯誤處理
- 當Unix系統級函數遇到錯誤時,它們典型地會傳回―1,并設定全局整數變量errno來表示什麼出錯了。程式員應該總是檢查錯誤,但是不幸的是,許多人都忽略了錯誤檢查,因為它使代碼變得臃腫,而且難以讀懂。
- 通過使用錯誤處理包裝函數,我們可以更進一步地簡化我們的代碼。對于一個給定的基本函數foo,我們定義一個具有相同參數的包裝函數Foo,但是第一個字母大寫了。包裝函數調用基本函數,檢查錯誤,如果有任何問題就終止。
8.4 程序控制
8.4.1 擷取程序id492

8.4.2 建立和終止程序
程序的三種狀态——運作、停止和終止。 程序會因為三種原因終止程序:收到信号,該信号預設終止程序;從主程式傳回;調用exit函數。
8.4.3 回收子程序
- 回收:當一個程序終止時,核心并不立即把它從系統中清除。相反,程序被保持在一種已終止的狀态中,直到被它的父程序回收。
- 僵死程序:一個終止了但是還未被回收的程序稱為僵死程序。
- 回收子程序的兩種方法:1,核心的init程序 2,父程序waitpid函數 1)如果父程序沒有回收它的僵死子程序就終止了,那麼核心就會安排init進城來回收它們。init程序的PID為1,并且是在系統初始化時建立的。 2)一個程序可以通過調用waitpid函數來等待它的子程序終止或停止。
- waitpid函數有點複雜,預設地(當options=0時),waitpid挂起調用程序的執行,知道它的等待集合中的一個子程序終止。
1.判定等待集合的成員 2.修改預設行為 3.檢查已回收子程序的退出狀态 4.錯誤條件 5.wait函數 6.使用waitpid的示例
8.4.4 讓程序休眠sleep
- sleep函數将一個程序挂起一段指定的時間。
- 如果請求的時間量已經到了,sleep傳回0,否則傳回還剩下的要休眠的秒數。後一種情況是可能的,如果因為sleep函數被一個信号中斷而過早地傳回。我們将在8.5節中詳細讨論信号
- pause函數讓調用函數休眠,直到該程序收到一個信号。
8.4.5 加載并運作程式execve
- execve函數加載并運作可執行目标檔案filename,且帶參數清單argv和環境變量清單envp。隻有當出現錯誤時,例如找不到filename,execve才會傳回到調用程式。是以,與fork一次調用傳回兩次不同,execve調用一次并從不傳回。
- 參數中每個指針都指向一個參數串。按照慣例,argv[0]是可執行目标檔案的名字。環境變量的清單是由一個類似的資料結構表示的。envp變量指向一個以null結尾的指針數組,其中每個指針指向個環境變量串,其中每個串都是形如“NAME=VALUE”的名字一值對。
8.4.6 利用fork和execve運作程式
- 像Unix外殼和Web伺服器這樣的程式大量使用了fork和e×ecve函數。外殼是一個互動型的應用程式,它代表使用者運作其他程式。最早的外殼是Sh程式,後面出現了一些變種,比如csh、tcsh、ksh和bash。外殼執行一系列的讀/求值(readeaUte)步驟然後終止。
- 如果builtin_command傳回0,那麼外殼建立一個子程序,并在子程序中執行所請求的程式。如果使用者要求在背景運作該程式,那麼外殼傳回到循環的頂部,等待下一個指令行否則,外殼使用Waitpid函數等待作業終止。當作業終止時,外殼就開始下一輪疊代。注意這個簡單的外殼是有缺陷的,因為它并不回收它的背景子程序。修改這個缺陷就要求使用信号,我們将在下一節中講述信号。
8.5 信号
- 一種更高層次的軟體形式的異常,稱為unix信号,它允許程序中斷其他程序。
- 低層的硬體異常是由核心異常處理程式處理的,正常情況下,對使用者程序而言是不可見的。信号提供了一種機制,通知使用者程序發生了這些異常。
8.5.1 信号術語
- 傳送一個信号到目的程序是由兩個步驟組成的 1.發送信号。核心通過更新目的程序上下文中的某個狀态,發送(遞送)一個信号給目的程序。 發送信号可以有如下兩種原因: 1)核心檢測到一個系統事件。 2)一個程序調用了kill函數,顯式地要求核心發送一個信号給目的程序,一個程序可以發送信号給它自己。 2.接收信号。當目的程序被核心強迫以某種方式的發送做出反應時,目的程序就接收了信号。程序可以忽略這個信号,終止或者通過執行一個稱為信号處理程式的使用者層函數不活這個信号。
- 一個隻發出而沒有被接收的信号叫做待處理信号。在任何時刻,一種類型至多隻會有一個待處理信号。
- 一個程序可以有選擇性地阻塞接收某種信号。當一種信号被阻塞時,他仍可以被發送,但是産生的待處理信号不會被接收,直到程序取消對這種信号的阻塞。
- 一個待處理信号最多隻能被接收一次。
8.5.2 發送信号
1.程序組
- 程序組。每個程序都隻屬于一個程序組,程序組是由一個正整數程序組ID來辨別的。 一個子程序和它的父程序同屬于一個程序組,一個程序組可以通過使用setpgid函數來改變自己或者其他程序的程序組。 2.用/bin/kill程式發送信号
- 用/bin/kill程式可以向另外的程序發送任意的信号。 3.從鍵盤發送信号
- 從鍵盤發送信号外殼為每個作業建立一個獨立的程序組。 4.用kill函數發送信号
- 程序通過調用kill函數發送信号給其他程序(包括它們自己)。 5.用alarm函數發送信号
- 程序可以通過調用alarm函數向他自己發送SIGALRM信号。
8.5.3 接收信号
- 當核心從一個異常處理程式傳回,準備将控制傳遞給程序P時,他會檢查程序P的未被阻塞的處理信号的集合。如果這個集合為空,那麼核心将控制傳遞到P的邏輯控制流中的下一條指令;如果集合是非空的,那麼核心選擇集合中的某個信号K(通常是最小的K0,并且強制P接收信号K。收到這個信号會觸發程序的某種行為。一旦程序完成了這個行為,那麼控制就傳遞回P的邏輯控制流中的下一條指令。
- 每個信号類型都有一個預定的預設行為: (1)程序終止 (2)程序終止并轉儲存儲器 (3)程序停止直到被SIGCONT型号重新開機 (4)程序忽略該信号
- signal函數可以通過下列三種方法之一來改變和信号signum相關聯的行為: (1)如果handler是SIG_IGN,那麼忽略類型為signum的信号 (2)如果handler是SIG_DFL,那麼類型為signum的信号行為恢複為預設行為 (3)否則,handler就是使用者定義的函數的位址,這個函數成為信号處理程式,隻要程序接收到一個類型為signum的信号,就會調用這個程式,通過把處理程式的位址傳遞到signal函數進而改變預設行為,這叫做設定信号處理程式。
- 但一個程序不活了一個類型為K的信号時,為信号K設定的處理程式被調用,一個整數參數被設定為K。這個參數允許同一個處理函數捕獲不同類型的信号。
- 信号處理程式的執行中斷main C函數的執行,類似于底層異常處理程式中斷目前應用程式的控制流的方式,因為信号處理程式的邏輯控制流與主函數的邏輯控制流重疊,信号處理程式和主函數并發地運作。
8.5.4 信号處理問題
- 當一個程式要捕獲多個信号時,一些細微的問題就産生了。
(1)待處理信号被阻塞。Unix信号處理程式通常會阻塞目前處理程式正在處理的類型的待處理信号。
(2)待處理信号不會排隊等待。任意類型至多隻有一個待處理信号。是以,如果有兩個類型為K的信号傳送到一個目的程序,而由于目的程序目前正在執行信号K的處理程 序,是以信号K時阻塞的,那麼第二和信号就簡單地被簡單的丢棄,他不會排隊等待。
(3)系統調用可以被中斷。像read、wait和accept這樣的系統調用潛在地會阻塞程序一段較長的時間,稱為慢速系統調用。在某些系統中,當處理程式捕獲到一個信号時,被中斷的慢速系統調用在信号處理程式傳回時不再繼續,而是立即傳回給使用者一個錯誤的條件,并将errno設定為EINTR。
8.5.5 可移植的信号處理
Signal包裝函數設定的信号處理程式的信号處理語義: (1)隻有這個處理程式目前正在處理的那種類型的信号被阻塞 (2)和所有信号實作一樣,信号不會排隊等候 (3)隻要有可能,被中斷的系統調用會自動重新開機。 (4)一旦設定了信号處理程式,它就會一直保持,知道signal帶着handler參數為SIG_IGN或者SIG_DFL被調用。
8.5.6 顯式地阻塞和取消阻塞信号
8.5.7 同步流以避免讨厭的并發錯誤
- 一般而言,流可能交錯的數量是與指令的數量呈指數關系的。
- 以某種方式同步并交流,進而得到最大的可行的交錯的集合,每個可行的交錯都能得到正确的結果。
- 如何編寫讀寫相同存儲位置的并發流程式的問題,困擾着數代計算機科學家。比如,競争問題。
8.6 非本地跳轉
- c語言提供了一種使用者級異常控制流形式,稱為本地跳轉。通過setjmp和longjmp函數來提供。
- setjmp函數隻被調用一次,但傳回多次:一次是當第一次調用setjmp,而調用環境儲存在緩沖區env中時,一次是為每個相應的longjmp調用。另一方面,longjmp隻調用一次,但從不傳回。sig—函數是setjmp和longjmp函數的可以被信号處理程式使用的版本。
- 非本地跳轉的一個重要應用就是允許從一個深層嵌套的函數調用中立即傳回,通常是由檢測到某個錯誤情況引起的。
- 非本地跳轉的另一個重要應用是使一個信号處理程式分支到一個特殊的代碼位置,而不是傳回到達中斷了的指令位置。
8.7 操作程序的工具
Linux系統提供了大量的監控和操作程序的有用工具:
- STRACE:列印一個正在運作的程式和它的子程序調用的每個系統調用的軌迹。對于好奇的的工具。用-StatiC編譯你的程式,能傳到一個更幹淨的、不帶學生而言,這是一個令人着迷有大量與共享庫相關的輸出的軌迹。
- PS:列出目前系統中的程序(包括僵死程序)
- TOP:列印出關于目前程序資源使用的資訊。
- PMAP:顯示程序的存儲器映射。proc:一個虛拟檔案系統,以ASCII文本格式輸出大量核心數資料結構的内容,使用者程式可 cat 2 / proc / load avg” , 觀察在Linux系統上的平均負載。
參考資料
- 教材:《深入了解計算機系統》