第8章 異常控制流
異常控制流ECF的作用:
1、ECF是作業系統用來實作I/O、程序和虛拟存儲器的基本機制。
2、應用軟體通過一個人叫做陷阱或者系統調用的ECF形式,向作業系統請求服務。
3、作業系統給應用系統提供了強大的ECF機制,用來建立新程序、等待程序終止、通知其他程序系統中的異常事件。
4、ECF是計算機系統中實作并發的基本機制。
5、應用層ECF中的非本地跳轉是程式響應錯誤情況的方式。
8.1 異常
異常是異常控制流的一種形式,一部分由硬體實作,一部分由作業系統實作。
異常是控制流中的突變,用在響應處理器狀态中的某些變化。在處理器中,狀态被編碼成不同的位和信号,狀态變化稱為事件,事件可能和目前指令的執行直接相關。
當處理器檢測到有事件發生時,它會通過一張叫做異常表的跳轉表,進行一個間接過程調用(異常),到一個專門設計用來處理這類事件的作業系統子程式(異常處理程式)。
當異常處理程式完成處理後,根據引起異常的事件的類型,會發生下面三種情況中的一種:
1、處理程式将控制傳回給目前指令Icurr,即當時間發生時正在執行的指令。
2、處理程式将控制傳回給Inext,即如果沒有發生異常将會執行的下一條指令。
3、處理程式終止被中斷的程式。
8.1.1 異常處理
當系統啟動時(當計算機重新開機或者加電時),作業系統配置設定和初始化一張稱為異常表的跳轉表,使得條目k包含異常k的處理程式的位址。
下圖是一張異常表的格式:

在運作時,處理器檢測到發生了一個事件,并且确定了相應的異常号k,随後,處理器觸發異常,方法是執行間接過程調用,通過異常表的條目k轉到相應的處理程式,下圖是處理器如何使用異常表形成适當的異常處理程式的位址:
異常和過程調用的對比:
1、過程調用時,在跳轉到處理程式之前,處理器将傳回位址壓入棧中,異常則根據類型,傳回位址位目前指令或下一條指令。
2、處理器會把一些額外的處理器狀态壓到棧裡,處理程式傳回時,重新開始被中斷的程式會需要這些狀态。
3、如果控制從一個使用者程式轉移到核心,那麼所有這些項目都被壓到内棧中,而不是壓到使用者棧中。
4、異常處理程式運作在核心模式下,它們對所有的系統資源都有完全的通路權限。
8.1.2 異常的類别
中斷——異步,陷阱 故障 終止——同步
一、中斷
中斷時異步發生的,是來自處理器外部的I/O裝置的信号的結果。硬體中斷不是由任何一條專門的指令造成的。硬體中斷的異常處理程式通常稱為中斷處理程式。
下表是異常的類别:
下圖概述了一個中斷的處理:
二、陷阱和系統調用
陷阱是有意的異常,是執行一條指令的結果。陷阱最重要的用途是在使用者程式和核心之間提供一個像過程一樣的接口,叫做系統調用。
下圖是一個陷阱處理:
三、故障
故障由錯誤情況引起,它可能能夠被故障處理程式修正,故障發生時,處理器将控制轉移給故障處理程式,如果處理程式能夠修正,那麼它控制傳回到引起故障的指令,重新執行它,否則處理程式傳回到核心中的abort例程,abort例程會終止引起故障的應用程式。
下圖是一個故障的處理:
四、終止
1、linux/IA32故障和終止
2、linux/IA32系統調用
8.2 程序
異常是允許作業系統提供程序的概念所需要的基本構造快。
程序的經典定義就是一個執行中的程式的執行個體.系統中的每個程式都是運作在某個程序的上下文中的。上下文是由程式正确運作所需的狀态組成的。
每次使用者通過向外殼輸入一個可執行目标檔案的名字,并運作一個程式時,外殼就會建立一個新的程序,然後在這個新程序的上下文中運作這個可執行目标檔案。應用程式也能夠建立新程序,且在這個新程序的上下文中運作它們自己的代碼或其他應用程式。
應用程式的關鍵抽象:
1、一個獨立的邏輯控制流,它提供一個假象,好像我們的程式獨占地使用處理器。
2、一個私有的位址空間,它提供一個假象,好像我們的程式獨占地使用存儲器系統。
8.2.1 邏輯控制流
調用調試器單步執行程式,會看到一系列程式計數器(PC)的值,這些值唯一的對應于包含程式的可執行目标檔案中的指令,或者是包含在運作時動态連結到程式的共享對象中的指令,這個PC值的序列叫做邏輯控制流。
上圖中的關鍵在于程序是輪流使用處理器的,每個程序執行它的流的一部分,然後被強占(暫時挂起),然後輪到其他程序。對于一個運作在這些程序之一的上下文中的程式,它看上去就像是在獨占地使用處理器。
8.2.2 并發流
一個邏輯流的執行在時間上與另一個流重疊,稱為并發流。
多個流并發地執行的一般現象稱為并發。一個程序和其他程序輪流運作的概念稱為多任務. 一個程序執行它的控制流的一部分的每一時間段叫做時間片 。是以,多任務也叫做時間分片 。如果兩個流并發地運作在不同的處理器核或者計算機上,那麼我們稱它為并行流,它們并行地運作且執行。
8.2.3 私有位址空間
一個程序為每個程式提供它自己的私有位址空間,一般而言,和這個空間中某個位址相關聯的那個存儲器位元組是不能被其他程序讀或寫的,從這個意義上說,這個位址空間是私有的。
每個私有位址空間有着相同的通用結構,位址空間底部留給使用者程式,包括通常的文本,資料,堆和棧段,位址空間頂部保留給核心,這個部分包含核心在代表程序執行指令時使用的代碼、資料和棧。
8.2.4 使用者模式和核心模式
為了使作業系統核心提供一個無懈可擊的程序抽象,處理器提供了一種機制,限制一個應用可以執行的指令以及它可以通路的位址空間範圍。
處理器通常用某個控制寄存器中的一個模式位來提供這種功能的,該寄存器描述了程序目前享有的特權。
當設定了模式位時,程序就運作在核心模式中,一個運作在核心模式的程序可以執行指令集中的任何指令,并且可以通路系統中任何存儲器的位置。
沒有設定模式位時,程序就運作在使用者模式中,使用者模式中的程序不允許執行特權指令也不允許程序直接引用位址空間中核心區内的代碼和資料。使用者程式必須通過系統調用接口間接的通路核心代碼和資料。
運作應用程式代碼的程序初始時是在使用者模式中的,程序從使用者模式變為核心模式的唯一方法是通過諸如中斷、故障或者陷入系統調用這樣的異常,異常發生時,控制傳遞到異常處理程式,處理器将模式從使用者模式變為核心模式,處理程式運作在核心模式中,當它傳回到應用程式代碼時,處理器就把模式從核心模式改回到使用者模式。
Linux 提供了一種聰明的機制,叫做/proc 檔案系統,它允許使用者模式程序通路核心資料結構的内容。/proc 檔案系統将許多核心資料結構的内容輸出為一個使用者程式可以讀的文本檔案的層次結構。比如,你可以使用/proc 檔案系統找出一般的系統屬性。
8.2.5 上下文切換
作業系統核心使用上下文切換這種較高層次的一場控制流來實作多任務。
核心位每個程序維持一個上下文,上下文就是核心重新啟動一個被強占的程序所需的狀态,它由一些對象的值組成,這些值包括通用目的寄存器,浮點寄存器,程式計數器,使用者棧,狀态寄存器,核心棧和各種核心資料結構,比如描繪位址空間的頁表,包含有關相關程序資訊的程序表,以及包含程序已打開檔案資訊的檔案表。在程序執行的某些時刻,核心可以決定搶占目前程序,并重新開始一個先前被搶占的程序。這種決定就叫做排程 ,是由核心中稱為排程器的代碼處理的。當核心選擇一個新的程序運作時,我們就說核心調皮了這個程序。在核心排程了一個新的程序運作後,它就搶占目前程序,并使用一種稱為土下丈切換的機制來将控制轉移到新的程序。
上下文切換:
1)儲存目前程序的上下文,
2) 恢複某個先前被搶占的程序被儲存的上下文,
3) 将控制傳遞給這個新恢複的程序。
當核心代表使用者執行系統調用時,可能會發生上下文切換。如果系統調用因為某個等待時間發生而阻塞,那麼核心可以讓目前程序休眠,切換到另一個程序。
中斷也可以發生上下文切換。
程序上下文切換的示例:
程序A運作在使用者模式中,直到它通過執行系統調用read陷入到核心,核心中的陷阱處理程式請求來自磁盤控制器的DMA傳輸,并且安排在磁盤控制器完成從磁盤到存儲器的資料傳輸後,磁盤中斷處理器。
8.4 程序控制
8.4.1 擷取程序ID
每個程序都有一個唯一的正數程序ID(PID)。getpid函數傳回調用程序的PID。getppid函數傳回它的父程序的PID。
#include <sys/types.h>
#include<unistd.h>
pid_t getpid(void)'
pid_t getppid(void);
8.4.2 建立和終止程序
程序總是處于下面三種狀态之一:
1、運作。程序要麼在CPU上執行,要麼在等待被執行且最終會被核心排程。
2、停止。程序的執行被挂起,且不會被排程。當收到SIGSTOP\SIGTSTP\SIDTTIN或者SIGTTOU信号時,程序就停止,并且保持停止直到它收到一個SIGCONT信号,在這個時刻,程序再次開始運作。
3、終止。程序永遠的停止了。程序會因為三種原因停止:1.收到一個信号,該信号的預設行為是終止程序。2.從主程式傳回3.調用EXIT函數
exit函數以status退出狀态來終止程序:
#include <stdlib.h>
void exit(int status);
父程序通過調用fork函數建立一個子程序:
#include <sys/types.h>
#include<unistd.h>
pid_t fork(void);
新建立的子程序幾乎但不完全與父程序相同。當父程序調用FORK時,子程序可以讀寫父程序中打開的任何檔案。父程序和新建立的子程序之間最大的差別在于它們有不同的PID。
FORK函數隻被調用一次,卻會傳回兩次:一次是在調用父程序中,一次是在新建立的子程序中。在父程序中,FORK傳回子程序的PID。在子程序中,FORK傳回0.因為子程序的PID總是非零的,傳回值就提供一個明确的方法來分辨程式是在父程序還是子程序中執行的。
一個調用fork函數的例子:
8.4.3 回收子程序
當一個程序由于某種原因終止時,核心并不是立即把它從系統中清除。相反,程序被保持在一種已終止的狀态中,直到被它的父程序回收。
當父程序回收已終止的子程序時,核心将子程序的退出狀态傳遞給父程序,然後抛棄已終止的程序。一個終止了但還未被回收的程序稱為僵死程序。
一個程序可以通過調用WAITPID函數來等待它的子程序終止或者停止。
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
1、判定等待集合的成員
等待集合的成員是由參數PID來确定的:
1、如果PID>0,那麼等待集合就是一個單獨的子程序,它的程序ID等于PID。
2、如果PID=—1,那麼等待集合就是由父程序所有的子程序組成的。
2、修改預設行為
可以通過将optioins 設定為常量WNOHANG 和WUNTRACED的各種組合,修改預設行為:
WNOHANG: 如果等待集合中的任何子程序都還沒有終止,那麼就立即傳回(傳回值為0)。預設的行為是挂起調用程序,直到有子程序終止。在等待子程序終止的同時,如果還想做些有用的工作,這個選項會有用。
WUNTRACED :挂起調用程序的執行,直到等待集合中的一個程序變成已終止或者被停止。傳回的PID 為導緻傳回的己終止或被停止子程序的PID。預設的行為是隻傳回己終止的子程序。當你想要檢查已終止和被停止的子程序時,這個選項會有用。
WNOHANG陽UNTRACED: 立即傳回,如果等待集合中沒有任何子程序被停止或已終止,那麼傳回值為0 ,或者傳回值等于那個被停止或者己終止的子程序的PID 。
3、檢查已回收子程序的退出狀态
如果status參數是非空的,那麼waitpid就會在status參數中放上關于導緻傳回的子程序的狀态資訊。wait.h頭檔案定義解釋了status參數的幾個宏:
WIFEXITED:如果子程序通過調用exit或者一個傳回正常終止,就傳回真。
WEXITSTATUS:傳回一個正常終止的自己成的退出狀态,隻有在WIFEXITED傳回為真時,才會定義這個狀态。
WIFSIGHALED:如果子程序是因為一個未被捕獲的信号終止的,那麼就傳回為真。
WTERMSIG:傳回導緻子程序終止的信号的編号,隻有在WIFSIGHALED傳回為真時,才定義這個狀态。
WIFSTOPPED:如果引起傳回的子程序目前是被停止的,傳回為真。
WSTOPSIG:傳回引起子程序停止的信号的數量,隻有在WIFSTOPPED傳回為真時,才定義這個狀态。
4、錯誤條件
如果調用程序沒有子程序,那麼WAITPID傳回-1,并且設定ERRNO為ECHILD。如果WAITPID函數被一個信号中斷,那麼它傳回-1.并設定ERRNO為EINTR。
5、wait函數
wait函數是waitpid函數的簡單版本
pid_t wait(int *status);
調用wait(&status)等價于調用waitpid(-1,&status,0)。
8.4.4 讓程序休眠
sleep函數将一個程序挂起一段指定的時間
unsigned int sleep(unsigned int secs);
如果請求的時間量已經到了,傳回0,否則傳回剩下的要休眠的秒數。
pause函數讓調用函數休眠,直到該程序收到一個信号:
#include<unsitd.h>
int pause(void);
8.4.5 加載并行程式
execve函數在目前程序的上下文中加載并運作一個新程式
int execve(const char *filename, const char *argv[], const char *envp[]);
execve函數加載并運作可執行目标檔案filename,且帶參數清單argv和環境變量清單envp,隻有當出現錯誤時,才會傳回到調用程式,是以與fork調用一次傳回兩次不同,execve調用一次并從不傳回。
參數清單由下圖表示.ARGV變量指向一個以NULL結尾的指針數組,其中每個指針都指向一個參數串。ARGV[0]是可執行目标檔案的名字。
ENVP變量指向一個以NULL結尾的指針數組,其中每個指針指向一個環境變量串。
在execve加載了filename後,它調用啟動代碼,啟動代碼設定棧,并将控制傳遞給新的主函數,主函數形式如下:
int main(int argc,char **argv,char **envp);
UNIX提供了幾個函數來操作環境數組:
getenv函數在環境數組中搜尋字元串"name = value".如果找到了,傳回一個指向VALUE的指針,否則傳回NULL。
#include<stdlib.h>
char *getenv(const char *name);
如果環境數組包含一個形如“name=oldvalue”的字元串,那麼unsetenv會删除它,而sentenv會用newvalue代替oldvalue,但是隻有在overwrite非零時才會這樣,如果name不存在,那麼setenv就把“name=oldvalue”添加到數組中。
int serenv(const char *name,const char *newvalue, int overwrite);
void unsetenv(const char *name);
8.4.6 利用fork和execve運作程式
外殼是一個互動性的應用及程式,它代表使用者運作其他程式,最早的歪歌是sh程式。
外殼執行一系列的讀、求值步驟,然後終止。讀步驟讀取來自使用者的一個指令行,求值步驟解析指令行,并代表使用者運作程式。
8.5 信号
一個信号就是一條小消息,它通知程序系統中發生了一個某種類型的事件。更高層的軟體形式的異常linux信号允許程序中斷其他程序。
下圖是一些linux信号
8.5.1 信号術語
傳送一個信号到目的的程序由兩個步驟組成:
1、發送信号:核心通過更新目的程序上下文中的某個狀态,發送一個信号給目的程序。
發送信号的原因:1)核心檢測到一個系統事件;2)一個程序調用了kill函數
2、接收信号:程序可以忽略,終止,或者通過執行一個稱為信号處理程式的使用者層一個資金函數捕獲這個信号。
一個隻發出而沒有被接受的信号叫做待處理信号。
一個類型至多隻有一個待處理信号,如果已經有這個類型的待處理信号,那麼後來的這種類型的信号都會被簡單的丢棄。
一個程序可以選擇性的阻塞接受某種信号,被阻塞仍可以被發送,但是不會被接收。
一個待處理信号最多隻能被接收一次。
8.5.2 發送信号
一、程序組
每個程序都隻屬于一個程序組,程序組是由一個正整數程序組ID來辨別的,一個子程序和它的父程序屬于同一個程序組。
getpgrp函數傳回目前程序的程序組ID,setpgid函數改變自己或者其他程序的程序組。
二、用/bin/kill程式發送信号
/bin/kill程式可以向另外的程序發送任意的信号,如:
/bin/kill -9 15213
發送信号9給程序15213.
一個負的PID會導緻信号被發到PID組的每一個程序。
三、從鍵盤發送信号
任何時刻都隻有一個前排作業和0個或者多個背景作業。
程序組ID是取自作業中父程序中的一個。
四、用kill函數發送信号
程序調用kill函數發送信号給别的程序(包括自己)。
如果pid大于0,kill函數發送信号sig給程序pid,如果pid小于0,那麼kill發送信号sig給程序組abs(pid)中的每個程序。
五、用alarm函數發送信号
程序調用alarm函數向自己發送SIGALRM信号。
#include <unistd.h>
unsigned int alarm(unsigned int secs);
傳回前一次鬧鐘剩餘的秒數,若沒有傳回0.
8.5.3 接收信号
當核心從一個異常處理程式傳回,準備将控制傳遞給程序p 時,它會檢查程序p 的未被阻塞的待處理信号的集合 。如果這個集合為空(通常情況下),那麼核心将控制傳遞到p 的邏輯控制流中的下一條指令。
然而,如果集合是非空的,那麼核心選擇集合中的某個信号k ( 通常是最小的k) , 并且強制p 接收信号k. 收到這個信号會觸發程序的某種行為。一旦程序完成了這個行為,那麼控制就傳遞回p 的邏輯控制流中的下一條指令 。每個信号類型都有一個預定義的預設行為,是下面中的一種:
程序終止。
程序終止并轉儲存儲器(dump core) 。
程序停止直到被SIGCONT 信号重新開機.
程序忽略該信号。
signal函數:
#include<signal.h>
typedf void(*sighandler_t)(int);
sighandler_t signal(int sighum, sighandlei_t handlei);
若成功指向前次處理程式的指針,若出錯則為SIG_ERR(不設定errno)
signal函數可以通過下面三種方法之一改變信号和信号signum相關聯的行為:
如果handler 是SIG_IGN. 那麼忽略類型為signum 的信号。
如果handler 是SIG_DFL,那麼類型為signum 的信号行為恢複為預設行為。
否則, handler 就是使用者定義的函數的位址,這個函數稱為信号處理程式,隻要程序接收到一個類型為signurn 的信号,就會調用這個程式。通過把處理程式的位址傳遞到signal 函數進而改變預設行為,這叫做設定信号處理程式。調用信号處理程式稱為捕獲信号。執行信号處理程式稱為處理信号。
當一個程序捕獲了一個類型為k的信号時,為信号k設定的處理程式被調用,一個整數參數被設定為k,這個參數允許同意同一個處理函數捕獲不同類型的信号。
8.5.4 信号處理問題
信号處理問題:
1、待處理信号被阻塞:Unix 信号處理程式通常會阻塞目前處理程式正在處理的類型的待處理信号。比如,假設一個程序捕獲了一個SIGINT 信号,并且目前正在運作它的SIGINT 處理程式。如果另一個SIGINT 信号傳遞到這個程序,那麼這個SIGINT 将變成待處理的,但是不會被接收,直到處理程式傳回。
2、待處理信号不會排隊等待。任意類型至多隻有一個待處理信号。是以,如果有兩個類型為k 的信号傳送到一個目的程序,而由于目的程序目前正在執行信号k 的處理程式,是以信号k 是阻塞的,那麼第二個信号就被簡單地丢棄,它不會排隊等待。關鍵思想是存在一個待處理的信号僅僅表明至少已經有一個信号到達了。
3、系統調用可以被中斷。像read 、wait 和accept 這樣的系統調用潛在地會阻塞程序一段較長的時間,稱為慢速系統調用。在某些系統中,當處理程式捕獲到一個信号時,被中斷的慢速系統調用在信号處理程式傳回時不再繼續,而是立即傳回給使用者一個錯誤條件,并将errno。設定為EINTR。
注意:不能用信号對其他程序中發生的事件計數。
8.5.5 可移植的信号處理
不同系統之間,信号處理語義的差異〈比如一個被中斷的慢速系統調用是重新開機還是永久放棄)是Unix 信号處理的一個缺陷。為了處理這個問題, Posix 标準定義了sigaction 函數,它允許像Linux 和Solaris 這樣與Posix 相容的系統上的使用者,明确地指定他們想要的信号處理語義。
int sigaction(int sighnum, struct sigaction *act, strut sigaction *oldact);
若成功為0,出錯為-1
sigaction 函數運用并不廣泛,因為它要求使用者設定多個結構條目。一個要簡潔的方式,就是定義一個包裝函數,稱為Signal ,它調用sigaction 。Signal 的調用方式與signal 函數的調用方式一樣。Signal 包裝函數設定了一個信号處理程式,其信号處理語義如下:
隻有這個處理程式目前正在處理的那種類型的信号被阻塞。
和所有信号實作一樣,信号不會排隊等待。
隻要可能,被中斷的系統調用會自動重新開機。
一旦設定了信号處理程式,它就會一直保持,直到Signal 帶着handler 參數為SIG_IGN 或者SIG_DFL 被調用。(一些比較老的'Unix 系統會在一個處理程式處理完一個信号之後,将信号行為恢複為它的預設行為。
8.5.6 顯式地阻塞和取消阻塞信号
應用程式使用sigprocmask函數顯式地阻塞和取消阻塞信号
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
sigprocmask 函數改變目前已阻塞信号的集合(8.5.1 節中描述的blocked 位向量〉。具體的行為依賴于how 的值:
SIG_BLOCK: 添加set 中的信号到blocked 中(blocked = blocked I set).
SIg_.UNBLOCK: 從blocked 中删除set 中的信号(blocked = blocked & -set) 。
SIG_SEtMASK : blocked = set 。
如果oldset 非空, blocked 位向量以前的值會儲存在oldset 中。
可以使用下列函數操作像set 這樣的信号集合。sigemptyset 初始化set 為空集。
sigfillset 函數将每個信号添加到set 中。sigaddset 函數添加signum 到set , sigdelset從set 中删除signum,如果signum 是set 的成員,那麼sigismember 傳回1 ,否則傳回0。
8.6 非本地跳轉
非本地跳轉:控制直接從一個函數轉移到另一個目前正在執行的函數,不經過正常的調用-傳回序列。
非本地跳轉是通過setjmp和longjmp 函數來提供的。
setjmp函數在ecv緩沖區中儲存目前調用環境,以供後面longjmp使用,并傳回0,調用環境包括程式計數器、棧指針、通用目的寄存器。
longjmp函數從env緩沖區中恢複調用環境,然後觸發一個從最近一次初始化env的setjmp調用的傳回,然後setjmp傳回,并帶有非零的傳回值retval。
setjmp函數隻被調用一次,但傳回多次,longjmp函數被調用一次,但從不傳回。
8.7 操作程序的工具
STRACE:列印一個正在運作的程式和他的子程式調用的每個系統調用的軌迹。
PS:列出目前系統中的程序,包括僵死程序。
TOP:列印出關于目前程序資源使用的資訊。
PMAP:顯示程序的存儲器映射。
參考資料
教材