第八章 異常控制流
一、控制流:控制轉移序列。
二、控制轉移:從一條指令到下一條指令。
三、異常控制流:現代作業系統通過使控制流發生突變來對系統狀态做出反應,這些突變稱為異常控制流。
四、平滑:指在存儲器中指令都是相鄰的。
五、突變:出現不相鄰,通常由諸如跳轉、調用、和傳回等指令造成。異常控制流ECF:即這些突變。
8.1 異常
一、異常是異常控制流的一種形式,由硬體和作業系統實作。簡單來說,就是控制流中的突變,用來響應處理器狀态中的某些變化。
二、事件:即狀态變化,與目前指令的執行可能直接相關,也可能沒有關系。
8.1.1 異常處理
一、異常号:系統為每種類型的異常配置設定的唯一的非負整數。
二、異常表:系統啟動時作業系統就會初始化一張條轉變,使得條目k包含異常k的處理程式的位址。
三、異常号是到異常表中的索引,異常表的起始位址放在異常表基址寄存器。
四、異常類似于過程調用,但有一些重要的不同之處:
1.處理器壓入棧的傳回位址,是目前指令位址或者下一條指令位址。
2.處理器也把一些額外的處理器狀态壓到棧裡
3.如果控制一個使用者程式到核心,所有項目都壓到核心棧裡。
4.異常處理程式運作在核心模式下,對所有的系統資源都有完全的通路權限。
8.1.2 異常的類别
一、異常的類别——中斷、陷阱、故障和終止

1、中斷
異步發生,是來自處理器外部的I/O裝置的信号的結果
傳回下一條指令
2、陷阱
陷阱是有意的異常,是執行一條指令的結果
最重要的用途——系統調用
3、故障
由錯誤狀況引起,可能能夠被故障處理程式修正
結果要麼重新執行指令(就是傳回目前指令位址),要麼終止
典型示例:缺頁異常
4、終止
是不可恢複的緻命錯誤造成的結果,通常是一些硬體錯誤
8.1.3 Linux/IA32系統中的異常
一、一共有256種不同的異常類型。
二、Linux/IA32故障和終止
除法錯誤/浮點異常 異常0 終止程式
一般保護故障/段故障 異常13 終止程式
缺頁 異常14 傳回目前位址
機器檢查 異常18 終止程式
三、Linux/IA32系統調用
每一個系統調用都有一個唯一的整數号,對應于一個到核心中跳轉表的偏移量。
四、系統調用的實作方法:
在IA32中,系統調用通過一條稱為int n的陷阱指令提供:
系統調用和與它們相關聯的包裝函數稱為系統級函數,這兩個術語可以互換地使用。
所有的到Linux系統調用的參數都是通過寄存器傳遞的。慣例如下:
%eax:包含系統調用号
%ebx,%ecx,%edx,%esi,%edi,%ebp:包含最多六個任意參數
%esp:棧指針,不能使用。因為當進入核心模式時,核心會覆寫它。
8.2 程序
一、程序就是一個執行中的程式執行個體。系統中的每個程式都是運作在某個程序的上下文中的。
二、上下文:由程式正确運作所需的狀态組成的。
三、程序提供給應用程式的關鍵抽象:
1、一個獨立的邏輯控制流:獨占的使用處理器
2、一個私有的位址空間:獨占的使用存儲器系統
8.2.1 邏輯控制流
一、含義
一系列的程式計數器PC的值,分别唯一的對應于包含子啊程式的可執行目标檔案中的指令,或者是包含在運作時動态連結到程式的共享對象中的指令,這個PC值的序列就叫做邏輯控制流。
二、
參見圖8-12,關鍵在于:程序是輪流使用處理器的。每個程序執行它的流的一部分,然後被搶占,然後輪到其他程序。但是程序可以向每個程式提供一種假象,好像它在獨占的使用處理器。
三、邏輯流示例
異常處理程式、程序、信号處理程式、線程、Java程序
8.2.2 并發流
一個邏輯流的執行在時間上與另一個流重疊,稱為并發流,這兩個流被稱為并發地運作。
二、幾個概念
并發:多個流并發執行的一般現象稱為并發。
多任務:一個程序和其他程序輪流運作的概念稱為多任務(也叫時間分片)。
時間片:一個程序執行它的控制流的一部分的每一時間段叫做時間片。
三、并行
兩個流并發的運作在不同的處理機核或者計算機上,稱它們為并行流,它們并行地運作,且并行地執行。
8.2.3 私有位址空間
一、程序為程式提供的假象,好像它獨占的使用系統位址空間。一個程序為每個程式提供它自己的私有位址空間。
8.2.4 使用者模式和核心模式
一、使用者模式和核心模式的差別就在于使用者的權限上,權限指的是對系統資源使用的權限。具體的差別是有無模式位,有的話就是核心模式,可以執行指令集中的所有指令,通路系統中任何存儲器位置;沒有就是使用者模式。
二、程序從使用者模式變為核心模式的唯一方法是通過異常——中斷,故障,或者陷入系統調用。
三、linux提供了/proc檔案系統,它允許使用者模式程序通路核心資料結構的内容。将許多核心資料結構的内容輸出為一個使用者程式可以讀的文本檔案的層次結構。
8.2.5 上下文切換
一、作業系統核心使用上下文切換這種較高層形式的異常控制流來實作多任務。上下文切換機制建立在較底層異常機制之上。
二、上下文:核心重新啟動一個被搶占的程序所需的狀态。
由一些對象的值組成,這些對象包括:通用目的寄存器、浮點寄存器、程式計數器、使用者棧、狀态寄存器、核心棧和各種核心資料結構(頁表、程序表、檔案表)。
三、排程和排程器
在程序執行的某些時刻,核心可以決定搶占目前程序,并重新開始一個先前被搶占的程序,這種決定就叫做排程。是由核心中稱為排程器的代碼處理的。
四、上下文切換機制
1.儲存目前程序的上下文
2.恢複某個先前被搶占的程序被儲存的上下文
3.将控制傳遞給這個新恢複的程序。
五、可能發生上下文切換的原因:
1、核心代表使用者執行系統調用時
2、中斷
8.3 系統調用錯誤處理
一、當Unix系統級函數遇到錯誤時,它們典型地會傳回-1,并設定全局整數變量errno來表示什麼出錯了。
二、通過使用錯誤處理包裝函數,可以進一步地簡化我們的代碼。
三、包裝函數調用基本函數,檢查錯誤,如果有任何問題就終止。
8.4 程序控制
8.4.1 擷取程序ID
每個程序都有一個唯一的正數程序ID(PID)。
#include <sys/types.h>
#include <unistd.h>
pid_t
getpid(void); 傳回調用程序的PID
getppid(void); 傳回父程序的PID(建立調用程序的程序)
8.4.2 建立和終止程序
一、程序的三種狀态:
1、運作。程序在CPU上執行,或等待被執行(會被排程)。
2、停止:程序被挂起(不會被排程)。收到 SIGSTOP 、 SIGTSTP 、 SIDTTIN 、 SIGTTOU 信号,程序停止,收到 SIGCONT 信号,程序再次開始運作。
3、終止:永遠停止。
原因:
1、收到信号,預設行為為終止程序
2、從主程式傳回
3、調用exit函數
二、建立程序
父程序通過調用fork函數來建立一個新的運作子程序:父程序與子程序有相同(但是獨立的)位址空間,有相同的檔案藐視符集合。
fork(void);
建立新程序可以使用 fork 函數。新建立的子程序和父程序幾乎相同,它獲得父程序使用者級虛拟位址空間和檔案描述符的副本,主要差別是它們的PID不同。
fork 函數調用一次,傳回兩次;父子程序是并發運作的,不能假設它們的執行順序;兩個程序的初始位址空間相同,但是是互相獨立的;它們還共享打開的檔案。因為有相同的程式代碼,是以如果調用 fork 三次,就會有八個程序。
調用fork函數n次,産生2的n次方個程序。
三、終止程序
用exit函數。
#include <stdlib.h>
void exit(int status);
exit函數以status退出狀态來終止程序。
8.4.3 回收子程序
一、回收:當一個程序終止時,核心并不立即把它從系統中清除。相反,程序被保持在一種已終止的狀态中,直到被它的父程序回收。
二、僵死程序:一個終止了但是還未被回收的程序稱為僵死程序。父程序回收終止的子程序時,核心将子程序退出狀态傳給父程序,然後抛棄該程序。如果回收前父程序已經終止,那麼僵死程序由 init 程序回收。
三、回收子程序的兩種方法:
1.核心的init程序 ;
2.父程序waitpid函數
1)如果父程序沒有回收它的僵死子程序就終止了,那麼核心就會安排init進城來回收它們。init程序的PID為1,并且是在系統初始化時建立的。
2)一個程序可以通過調用waitpid函數來等待它的子程序終止或停止。
waitpid函數有點複雜,預設地(當options=0時),waitpid挂起調用程序的執行,知道它的等待集合中的一個子程序終止。
#include <sys/wait.h>
waitpid(pid_t pid, int *status, int options);
成功傳回子程序PID,如果WNOHANG,傳回0,其他錯誤傳回-1.
四、錯誤條件
如果調用程序沒有子程序,那麼waitpid傳回-1,并且設定errno為ECHILD。
如果waitpid被一個信号中斷,那麼他傳回-1,并且設定errno為EINTR。
五、wait函數
wait函數是waitpid函數的簡單版本,wait(&status)等價于waitpid(-1,&status,0).
#include <sys/wait.h>
wait(int *status);
成功傳回子程序pid,出錯傳回-1
8.4.4 讓程序休眠
一、sleep函數
sleep函數使一個程序挂起一段指定的時間。定義如下:
unsigned int sleep(unsigned int secs);
傳回值是剩下還要休眠的秒數,如果到了傳回0.
二、pause函數
int pause(void);
讓調用函數休眠,直到該程序收到一個信号。
8.4.5 加載并運作程式
int execve(const char *filename, const char *argv[], const char *envp[]);
成功不傳回,失敗傳回-1.
execve函數調用一次,從不傳回。
filename:可執行目标檔案
argv:參數清單
envp:環境清單
新程式開始時:
getnev函數
char *getenv(const char *name);
若存在則為指向name的指針,無比對是null
在環境數組中搜尋字元串"name=value",如果找到了就傳回一個指向value的指針,否則傳回null。
setenv和unsetenv函數
int setenv(const char *name, const char *newvalue, int overwrite);
若成功傳回0,錯誤傳回-1
void unsetenv(const char *name);
無傳回值
如果環境數組包含"name=oldvalue"的字元串,unsetenv會删除它,setenv會用newvalue代替oldvalue,隻有在overwrite非零時成立。
如果name不存在,setenv會将"name=newvalue"寫進數組。
8.4.6 利用fork和execve運作程式
一、fork函數是建立新的子程序,是父程序的複制體,在新的子程序中運作相同的程式,父程序和子程序有相同的檔案表,但是不同的PID
二、execve函數在目前程序的上下文中加載并運作一個新的程式,會覆寫目前程序的位址空間,但是沒有建立一個新程序,有相同的PID,繼承檔案描述符。
8.5 信号
Unix信号:更高層的軟體形式的異常允許程序中斷其他程序。
8.5.1 信号術語
一、在作業系統和應用程式之間:程序之間傳送信号
二、一種更高層次的軟體形式的異常,稱為unix信号,它允許程序中斷其他程序。
三、低層的硬體異常是由核心異常處理程式處理的,正常情況下,對使用者程序而言是不可見的。信号提供了一種機制,通知使用者程序發生了這些異常。
四、發送信号的原因:
1.核心檢測到一個系統事件
2.一個程序調用了kill函數,顯式的要求核心發送一個信号給目的程序。
五、接收信号:
1.忽略
2.終止
3.執行信号處理程式,捕獲信号
六、待處理信号:
隻發出沒有被接收的信号
任何時刻,一種類型至多隻會有一個待處理信号,多的會被直接丢棄
一個程序可以選擇性的阻塞接受某種信号,被阻塞仍可以被發送,但是不會被接收
一個待處理信号最多隻能被接收一次。
8.5.2 發送信号
一、程序組
程序組:每個程序都隻屬于一個程序組,程序組是由一個程序組ID來辨別的。預設的,一個子程序和它的父程序同屬于一個程序組。
在任何時刻,至多隻有一個前台作業和0個或多個背景作業。外殼為每個作業建立一個獨立的程序組,一個作業對應一個程序組。
二、用/bin/kill程式發送信号
/bin/kill程式可以向另外的程序發送任意的信号,格式是:
/bin/kill -n m
n是信号,m是程序或程序組
當n>0時,發送信号n到程序m
當n<0時,使信号|n|發送到程序組m中的所有程序。
三、從鍵盤發送信号
程序組PID是取自作業中父程序的一個
作業:為對一個指令行求值而建立的程序。
四、用kill函數發送信号
用kill函數發送信号:發送SIGKILL信号
五、用alarm函數發送信号
程序可以通過調用alarm函數向它自己發送SIGALRM信号。
unsigned int alarm(unsigned int secs);
傳回前一次鬧鐘剩餘的秒數,若沒有傳回0.
8.5.3 接收信号
程序可以通過使用signal函數來修改和信号相關的預設行為。唯一的例外是SIGSTOP和SIGKILL,它們的預設行為不能被修改。
8.5.4 信号處理問題
一、當一個程式捕獲多個信号時,容易有一些細問問題:
二、信号處理有以下特性:
1.信号處理程式阻塞目前正在處理的類型的待處理信号。
2.同種類型至多有一個待處理信号。
3.會潛在阻塞程序的慢速系統調用被信号中斷後,在信号處理程式傳回時不再繼續,而傳回一個錯誤條件,并将 errno 設為 EINTR 。
*對于第三點,Linux系統會重新開機系統調用,而Solaris不會。不同系統之間,信号處理語義存在差異。Posix标準定義了 sigaction 函數,使在Posix相容的系統上可以設定信号處理語義。
8.5.5 可移植的信号處理
一、不同系統之間,信号處理語義的差異是Unix信号處理的一個缺陷。為了處理這個問題,Posix标準定義了sigaction函數。
二、sigaction函數運用并不廣泛,因為它要求使用者設定多個結構條目。一個更簡潔的方式就是定義一個包裝函數,稱為Signal,它調用sigaction。Signal包裝函數設定了一個信号處理程式,其信号處理語義如下:
1、隻有這個處理程式目前正在處理的那種類型的信号被阻塞
2、和所有信号實作一樣,信号不會排隊等候。
3、隻要可能,被中斷的系統調用會自動重新開機。
4、一旦設定了信号處理程式,它就會一直保持,直到Signal帶着handler參數為SIG_IGN或者SIG_DFL被調用。
8.5.6 顯式地阻塞和取消阻塞信号
應用程式可以使用sigprocmask函數顯式地阻塞和取消阻塞選擇的信号。
8.5.7 同步流以避免讨厭的并發錯誤
以某種方式同步并交流,進而得到最大的可行的交錯的集合,每個可行的交錯都能得到正确的結果。
8.6 非本地跳轉
一、c語言提供了一種使用者級異常控制流形式,稱為非本地跳轉。通過setjmp和longjmp函數來提供。setjump函數在env緩沖區中儲存目前調用環境,以供後面longjmp使用,并傳回0.
二、調用環境:程式計數器,棧指針,通用目的寄存器
三、longjmp函數從env緩沖區中恢複調用環境,然後觸發一個從最近一次初始化env的setjmp調用的傳回。然後setjmp傳回,并帶有非零的傳回值retval。
四、setjmp函數隻被調用一次,但傳回多次:一次是當第一次調用setjmp,而調用環境儲存在緩沖區env中時,一次是為每個相應的longjmp調用。另一方面,longjmp隻調用一次,但從不傳回。sig—函數是setjmp和longjmp函數的可以被信号處理程式使用的版本。
五、非本地跳轉的一個重要應用就是允許從一個深層嵌套的函數調用中立即傳回,通常是由檢測到某個錯誤情況引起的。另一個重要應用是使一個信号處理程式分支到一個特殊的代碼位置,而不是傳回到達中斷了的指令位置。
8.7 操作程序的工具
STRACE:列印一個正在運作的程式和他的子程式調用的每個系統調用的痕迹
PS:列出目前系統中的程序,包括僵死程序
TOP:列印出關于目前程序資源使用的資訊
PMAP:顯示程序的存儲器映射
參考資料
《深入了解計算機系統》第八章 異常控制流
總結
通過一周的時間學習了課本第八章的内容,感覺有點兒吃力,概念模糊,了解不到位,接下來的一周将繼續深入學習,了解到位。