8.1 異常
- 所謂異常就是控制流過程中的突變,用來響應處理器狀態中的某些變化;
- 處理器的狀態的變化稱爲事件(event),事件可能和當前指令的執行相關(虛擬存儲器缺頁,算術溢出,除數爲零),也可能沒有關係(定時器信號,I/O請求完成);
- 當有異常發生時,處理器會通過異常表(exception table),進入異常處理程序(exception handler)。
- 通常異常處理程序處理完成之後會有三種情況:
- 處理程序將控制權返回給當前指令
- 處理程序將控制權返回給下一條指令
- 處理程序終止被中斷的程序
8.1.1 異常處理
- 系統中可能的每種異常都分配了一個唯一的非負整數的異常號(exception number),一部分由處理器設計這分配(被零除、缺頁、存儲器訪問違例、斷點、算術溢出),一部分由操作系統內核設計者分配(系統調用和I/O設備的信號);
- 在啓動系統是,操作系統維護了一個異常表,此異常表爲一個跳轉表;異常表的起始地址存儲在異常表基址寄存器(exception table base register);
- 異常處理程序調用和一般的過程調用的不同之處
- 一般過程調用時會將返回地址壓入棧中,而異常返回時要麼返回當前指令,要麼是下一條指令
- 處理會把一些處理器的狀態壓入棧中,返回時重新恢復在這些狀態;
- 當控制從用戶程序轉移到內核時,所有這些項目都會被壓入到內核的棧中,而不是用戶的棧;
- 異常處理程序運行在內核的模式下,這意味着對所有的系統資源有完全的訪問權限;
8.1.2 異常的類別
異常可以爲四種:中斷(interrupt)、陷阱(trap)、故障(fault)、終止(abort)
- 中斷(中斷是異常的一種,異常不等於中斷)
- 中斷是異步發生的,是來自處理器外部I/O設備信號的結果,而不是由任何一條指令造成的(異步);
- 中斷的過程:
- I/O設備通過向處理的引腳發送一個發送信號,並將異常號放到系統總線上,以觸發中斷,異常號標識了觸發中斷的設備;
- 處理器注意到引腳的電壓發生變化,便從系統總線上讀取異常號,然後調用相應的中斷處理程序;
- 當處理程序結束時,返回到下一條指令;
剩下的異常類型(陷阱、故障和終止)是同步發生的,是執行當前指令的結果。此類指令叫做故障指令(faulting instruction)。
- 陷阱和系統調用
- 陷阱是有意的異常;其用途是在用戶程序和內核之間提供一個像過程一樣的接口,叫系統調用;
- 當用戶程序向內核請求服務時(read、fork、execve、 exit), 爲了允許對這些內核 服務的受控訪問,處理器提供了“syscall n”指令;當用戶想調用服務n時,調用此指令,會導致到一個異常處理程序的陷阱,此異常處理程序對參數解碼(應該是n),並調用相應的內核處理程序;
- 一般的過程調用運行在過程模式(限制了指令的類型、並且在相同的函數棧中),而系統調用運行在內核模式(允許系統調用執行指令(syscall),並且在內核棧中);
- 故障
- 由錯誤引起,可能被修正
- 當故障發生時,它就將控制轉移到故障處理程序;如果程序能夠修正這個錯誤的情況,就返回到引起故障的指令,否則返回到內核的abort例程;
- 終止
- 終止是不可恢復的致命的錯誤造成的,通常爲硬件錯誤;終止程序不應將控制返回給應用程序;
8.2 進程
異常是允許操作系統提供進程(process)的概念所需要的基本構造塊;
進程經典的定義是一個執行中的程序實例;系統中每個程序都是運行在某個進程上下文中的;上下文包括:存儲器中的代碼和數據、通用寄存器中的內容、程序計數器、棧、環境變量以及打開文件描述符的集合。
進程提供兩個關鍵的抽象:
一個獨立的邏輯控制流:它提供一個假象,好像程序獨佔的使用處理器;
一個私有的地址空間:它提供一個假象,好像程序獨佔的使用存儲器系統;
8.2.1 邏輯控制流
邏輯控制流就是PC值對應的指令序列;
一個處理器的物理流可以分成多個邏輯流;
8.2.2 併發流
計算機中流有不同的形式:異常處理程序、進程、線程、信號處理程序;
併發流:一個邏輯流在執行時間上與另一個流重疊;
併發:多個流併發的執行的現象;
多任務:一個進程和其他進程輪流執行的概念稱爲多任務;
時間片:一個進程執行它的控制流的一部分的每一時間段;多任務也叫時間分片
並行流:併發的真子集,如果兩個流併發的運行在不同的處理器核或者計算機上,稱爲並行流;
8.2.3 私有地址空間
- 一個進程爲每個程序提供提供他自己的私有地址空間
- 和此空間中某個地址相關聯的存儲器字節時不能被其他進程讀寫的;
- 每個私有地址空間的內容不同,但有相同的通用結構;
- 32位進程代碼段從0x08048000開始,64位進程從0x00400000開始;
- 地址空間頂部保留給內核,包含內核的代碼、數據和棧;(例如執行一個系統調用);
8.2.4 用戶模式和內核模式
- 處理器通常使用某個控制寄存器中的一個模式位(mode bit)來切換用戶模式或者內核模式
- 內核模式可以執行所有的指令和訪問任何存儲器位置
- 用戶模式則沒有上述功能;任何嘗試都會導致一般保護故障;必須通過系統調用接口訪問內核代碼和數據;
8.2.5 上下文切換
操作系統使用上下文切換這種較高形式的異常控制流來實現多任務;上下文切換機制是建立在前面所述的較低層異常機制之上的;
內核爲每個進程維護一個上下文(內核重新啓動一個被搶佔的進程所需要的狀態),上下文由:通用目的寄存器、浮點寄存器、程序計數器、用戶棧、處理器狀態寄存器、內核棧和內核數據結構(描繪地址空間的頁表、有關當前進程信息的進程表、包含進程已打開文件的信息的文件表)
內核重新啓動一個之前被搶佔的進程叫調度,內核中稱爲調度器;
上下文切換:
- 保存當前進程的上下文;
- 恢復某個先前被搶佔的進程的上下文;
- 將控制權交給新恢復的進程
發生上下文切換的情況:
當內核代表用戶執行系統調用時,可能發生上下文切換;比如系統調用因爲某個事件而阻塞,內核可以讓該進程休眠,切換到另一個進程(比如read訪問磁盤、sleep);在此種情況下,在內核中的工作一部分代表進程A,一部分代表進程B。
一般而言,即使系統調用沒有發生阻塞,內核也可以決定執行上下文切換;
中斷也可以發生上下文切換;比如所有系統都有產生某種週期性定時器中斷的機制(1ms或者10ms),每當定時器中斷時,切換新的進程;
8.3 系統調用錯誤處理
- 當Unix系統級函數遇到錯誤時,典型的會返回-1,並設置全局變量errno;
if((pid = fork()) < 0){
fprintf(stderr,"fork error: %s\n",strerror(errno)); //strerror()函數返回錯誤關聯的字符串;
exit(0);
}
但這樣使得代碼繁瑣,簡單一層包裝:
if((pid = fork()) < 0)
unix_error("fork error");
void unix_error(char * msg){
fprintf(stderr,"%s,%s\n",msg,strerror(errno));
exit(0);
}
使用錯誤包裝處理函數:
pid_t Fork(void){
pid_t pid;
if((pid = fork()) < 0){
unix_error("fork error");
}
return pid;
}
8.4 進程控制
8.4.1 獲取進程狀態
pid_t getpid(void); //獲取當前進程
pid_t getppid(void); //獲取父進程
8.4.2 創建和終止進程
進程的三種主要狀態:
運行 要麼再CPU上運行,要麼等待且最終被CPU執行;
停止 進程的執行被掛起,且不會被調度;
終止 進程永久停止
收到一個信號,改信號默認終止進程
從主程序返回 返回值爲程序的最終返回狀態
調用exit()函數 參數值爲最終的返回狀態
創建進程:
pid_t fork(void);
子與父進程的同與不同:
子進程得到父進程虛擬地址空間的一份拷貝,但是是獨立的,包括代碼段、數據段、推以及用戶棧,此外還有文件描述符表(意味着子進程可以操作父進程打開的文件);
子進程與父進程最大的區別在於有不同的pid;
fork()函數的特點:
- 調用一次返回兩次,一次在父進程中,返回子進程pid;一次在子進程中,返回0;
8.4.3 回收子進程
當進程終止時,還是保存在內核中的,等待被其父進程回收;
當父進程回收子進程時,內核將子進程的退出狀態傳遞給父進程,然後拋棄子進程;(要求父進程回收爲了獲取子進程退出狀態,內核會拋棄子進程,維護系統資源);終止卻未被回收的子進程是殭屍進程;
如果父進程沒有回收其子進程就終止,那麼由init進程回收;
waitpid()函數:
pid_t waitpid(pid_t pid,int *status,int options);
參數pid:
- pid > 0 : 等待集合就一個單獨的子進程,就是此pid;
- pid = -1:所有子進程;
- Unix進程組
參數options
- WNOHANG 立即返回,不等待;
- WUNTRACED 掛起調用進程,直到等待集合中一個進程被終止或者停止,返回相關pid;
檢查子進程退出狀態
status參數非空,則此參數保存函數退出狀態;
WIFEXITED(status) 子進程通過exit或者return返回正常終止時,返回真。
- WEXITSTATUS(status)返回一個正常終止的子進程的狀態,依賴於上一個函數返回真;
WIFSIGNALED(status) 如果子進程因爲一個未捕獲的信號終止,返回真;
- WTERMSIG (status),返回導致子進程終止的信號的數量,之上上一個函數返回真時;
WIFSTOPPED(status)如果返回的子進程當前是被停止的,返回真
- WSTOPSIG(status)返回引起子進程停止的信號的數量;依賴上一個函數返回真;
調用錯誤
當調用進程沒有子進程時,返回-1,並且設置errno爲ECHILD;
函數被一個信號中斷,返回-1,並設置errno爲EINTR;
wait函數
pid_t wait(int* status); //等價調用waitpid(-1,&status,0);
8.4.4 讓進程休眠
unsigned int sleep(unsigned int secs);
- 如果請求時間到,則返回0;否則(函數被信號中斷而過早的返回),返回剩下要休眠的秒數;
int pause(void);
- 讓調用進程休眠,直到該進程收到信號;
8.4.5 加載並運行程序
int execve(const char* filename,const char argv[],const char *envp[]); //調用一次,成功不返回
參數 argv指向一個指針數組,每個指針指向一個參數串;
參數envp指向一個指針數組,每個指針指向一個環境變量串;
在加載了filename之後,execve調用啓動代碼,啓動代碼設置棧,並將控制傳遞給新程序的主函數;
8.5 信號
每種信號類型都對應某種系統事件;底層的硬件異常,是由內核異常處理程序處理的;信號提供了一種機制,通知用戶進程發生了這些異常;
8.5.1 信號術語
發送信號
- 內核監測到一個系統事件;
- 一個進程調用kill函數,顯式要求內核發送一個信號給目的進程;一個進程可以給自己發送信號。
接收信號
- 當目的進程被內核強制以某種方式對內核做出反應時,目的進程就接收了信號;
- 進程可以忽略這個信號,終止或者通過執行用戶層函數信號處理程序;
一個只要發出而沒有被接收的信號,叫待處理信號;
- 在任何時刻,一種類型,至多隻有一個待處理信號,多餘的同類型信號會被直接拋棄;
- 一個進程可以有選擇的阻塞接收某種信號,此信號仍可被髮送,但待處理信號不會被接收,直到進程取消對這種信號的阻塞;
- 帶處理信號最多隻能被處理一次,內核爲每個進程維護着待處理信號的集合和被阻塞信號的集合;當接收到一個信號時,在集合中標記,當接收時,消除;
8.5.2 發送信號
Unix提供了多種向進程發送進程的機制。這些機制是基於進程組的;
進程組
- 每個進程只屬於一個進程組,進程組ID號由正整數表示;
pid_t getpgrp(void);//獲得當前進程的進程組id
int setpgid(pid_t pid,pid_t pgid);
- 當pid爲0時,改變調用進程,非零時,對應進程
- 當pgid爲0時,調用進程pid作爲進程組號,非零時對應號;
- 用/bin/kill程序發送信號
/bin/kill -9 15213 當-15213時,結束進程組號爲15213的每一個進程
- 從鍵盤發送信號
- 作業: shell對一個命令行求職而創建的進程;
- 任何時刻,只有一個前臺作業,0個或者多個後臺作業
- 外殼爲每個作業創建獨立的進程組
- ctrl+c 導致SIGINT被髮送到外殼,外殼捕獲信號,然後發送到前臺進程中的每個進程
用kill函數發送信號
int kill(pid_t pid,int sig);
- pid > 0,發送給指定進程, pid < 0,給進程組每個進程
- 用alarm函數發送信號
unsigned int alarm(unsigned int secs);
- alarm函數和sleep函數由相似之處,被打斷時都會返回剩餘秒數
- 對alarm的調用都會取消正在等待的alarm,並返回秒數;沒有真在等待的就返回0;
8.5.3 接收信號
當內核從異常程序返回時,會檢查進程的未被阻塞的待處理信號的集合(pending&~block);原因:被阻塞說明條件還不滿足,遂不予處理;
- 當集合爲空時,說明沒有待處理的消息,直接將邏輯控制流傳遞給下一條指令;
- 當集合不爲空時,內核會選擇集合中的某個信號(通常爲值最小的那個),並強制進程接收信號,觸發對應此信號的處理程序;處理完成後,控制流給下一指令;
- 信號的默認行爲:
- 進程終止 (SIGKILL)
- 進程終止並轉儲存儲器()
- 進程停止,直到被SIGCONT信號重啓
- 進程忽略該信號(SIGCHLD)
進程可以使用signal函數修改信號的默認處理行爲;
注意: SIGSTOP和SIGKILL是不可忽略的的;
此函數有三種修改關聯方法:- handler爲SIG_IGN,忽略該信號;
- handler爲SIG_DEF,默認行爲
- handler爲用戶定義的函數地址,則調用此信號處理程序;
當處理程序處理完時,控制通常會返回給下一個指令;但在某些系統中,被中斷的系統掉用,會立即返回一個錯誤;(下面將講述);
- 信號處理程序的邏輯控制流與主函數的邏輯控制流重疊;
8.5.4 信號處理問題
- 待處理信號被阻塞: 噹噹前的正在處理的信號處理程序是剛到達的信號類型時,待處理信號會被阻塞;
- 待處理信號不會排隊等待:當有兩個同類型的信號到達,且此類型的處理程序正在運行,信號被阻塞,且第二個信號直接被丟棄;所以不能用消息進行事件計數
- 系統調用可以被中斷:像read、wait、accept這樣會阻塞進程一段時間的(慢速系統調用),當處理程序捕獲到一個信號時,被中斷的系統調用返回時不再繼續,直接返回錯誤;
8.5.5 可移植信號處理
- 不同系統對於信號的處理語義是有差異的(中斷的慢速系統調用是重啓還是放棄);
- sigaction函數指明瞭信號處理語義:
- sigaction包裝函數:Signal
此函數信號處理語義如下: - 當前處理信號被屏蔽
- 信號不會排隊(不會有超過1個的相同信號在排隊)
- 只要有可能,被中斷的系統調用會重啓;
- 一旦被設置了處理函數,就一直會存在,除非將handler參數設置爲SIG_DEF或者SIG_IGN;
8.5.6 顯示地阻塞和取消阻塞信號
- sigprocmask函數改變當前已阻塞信號的集合,具體行爲依賴於how值:
- SIG_BLOCK:添加set中的信號到block集合中;
- SIG_UNBLOCk:從block集合中刪除se集合中的信號;
- SIG_SETMASK:將block集合設置爲set集合;
8.5.7同步流以避免討厭的併發錯誤
- 當多個控制流對同一資源進行訪問時,容易發生競爭;
- 處理:
- 對於信號造成的,可以先屏蔽信號
- 對於調度策略造成的可以隨機休眠;
8.6 非本地跳轉
- C語言提供用戶級異常控制流形式,非本地跳轉;從一個函數轉移到另一個函數,無需經過調用-返回序列;
- longjump函數從env中恢復調用環境,然後觸發最近一次調用的setjump返回,返回longjump第二個參數;longjump從不返回;
注意: setjump調用一次,返回多次
- 第一次調用時,保存env環境,返回0;
第二次調用longjump時,setjump返回longjump第二個參數;
另一個應用,從中斷程序中不返回到打斷位置,而是利用保存的環境,重新啓動;