標籤:計劃執行 for order 筆記 進入 改變 處理器 會計 地址
第八章 異常控制流程概述
控制轉移序列叫做控制流程。目前為止,我們學過兩種改變控制流程的方式:
1)跳轉和分支;
2)調用和返回。
但是上面的方法只能控製程序本身,發生以下系統狀態的變化複雜問題時就沒法使用上面的方法控制:
- 資料從磁碟或者網路介面卡到達
- 指令除以了零
- 使用者按下 ctrl+c
- 系統的計時器到時間
現代系統通過使控制流程發生突變來對系統狀態的變化做出反應,這些突變稱為異常控制流程。
異常控制流程有四種實現機制:
1)異常(低層級);2)進程環境切換;3)訊號;4)非本地跳轉。(2-4高層級)
8.1 異常
異常(exception)就是控制流程中的突變,用來響應處理器狀態中的某些變化。這裡的異常是指將控制交給系統核心來處理某些事情。
核心是作業系統常駐記憶體的部分。
任何情況下,當處理器檢測到有事件發生時,它會通過一張就做異常表的跳轉表,進行一個間接過程的調用,使用例外處理常式(運行在核心模式下)來處理這類事件。這類事件包括被零除、缺頁、算術溢位、I/O請求完成。例外處理常式完成處理後,會發生以下三種情況:
1)處理常式將控制放回給當前指令Icurr,即事件發生時正在執行的指令。
2)將控制返回給Inext;
3)終止被中斷的程式。
系統中為每種類型的異常都分配了一個唯一的非負整數的異常號,系統會通過異常表來確定跳轉的位置,每個事件都有對應的異常號,發生對應事件就調用對應的異常處理代碼。
異常的類別
異常分為非同步異常和同步異常。非同步異常是硬體中斷(稱為中斷處理常式),是有外部IO裝置造成的,同步異常是執行當前指令的結果(稱為故障指令)。
陷阱屬於系統調用,運行在核心模式中,而普通的程式調用運行在使用者模式中,限制了函數可以執行的指令的類型。
類別 |
原因 |
非同步/同步 |
返回行為 |
中斷 |
來自I/O裝置的訊號 |
非同步 |
總是返回到下一條指令 |
陷阱 |
有意的異常(read,exit) |
同步 |
總是返回到下一條指令 |
故障 |
潛在可恢複的錯誤 |
同步 |
可能返回到當前指令 |
終止 |
不可恢複的錯誤 |
同步 |
不會返回 |
這裡重點分析一下故障異常,以缺頁異常故障為例:
缺頁異常發生的條件是:指令引用一個虛擬位址,但是與該地址相對應的物理頁面不在記憶體中,因此必須從磁碟中讀取,就會發生故障。
int a[1000];main (){ a[500] = 13;}
那麼系統會通過 Page Fault 把對應的部分載入到記憶體中,然後重新執行指派陳述式:
8.2 進程
通俗的定義是:佔用記憶體空間的正在啟動並執行程式。經典的定義是:一個執行中的程式的執行個體。進程提供給應用程式的關鍵抽象:1)一個獨立的邏輯控制流程;2)一個私人的地址空間。
並發流的概念:流X和Y互相併發,若且唯若X在Y開始之後和Y結束之前進行。或者Y在X開始之後和X結束之前開始。
環境切換:1)儲存當前進程的上下文;2)恢複某個先前被搶佔的進程被儲存的上下文;3)將控制傳遞給這個新恢複的進程。
8.3 進程式控制制
擷取進程ID:
#include<sys/types.h>#include<unistd.h>pid_t getpid(void);pid_t getppid(void);
getpid返回調用進程的PID,getppid返回它的父進程的PID。
我們可以認為,進程有三個主要狀態:
- 運行 Running
- 停止 Stopped
- 終止 Terminated
另外的兩個狀態稱為建立(new)和就緒(ready),這裡不再贅述。
fork()函數建立進程。
#include<sys/types.h>#include<unistd.h>pid_t fork(void);//子進程返回0,父進程返回子進程的PID,如果出錯,則返回-1
需要注意:
- 調用一次,返回兩次;
- 並發執行;
- 相同但是獨立的地址空間;
- 共用檔案。
進程圖
通過畫進程圖來理解fork函數:
- 每個節點代表一條執行的語句
- a -> b 表示 a 在 b 前面執行
- 邊可以用當前變數的值來標記
printf
節點可以用輸出來進行標記
- 每個圖由一個入度為 0 的節點作為起始
int main(){ pid_t pid; int x = 1; pid = Fork(); if (pid == 0) { // Child printf("child! x = %d\n", --x); exit(0); } // Parent printf("parent! x = %d\n", x); exit(0);}
在下面三種情況時,進程會被終止:
- 接收到一個終止訊號
- 返回到
main
- 調用了
exit
函數
exit
函數會被調用一次,但從不返回,具體的函數原型是
// 以 status 狀態終止進程,0 表示正常結束,非零則是出現了錯誤void exit(int status)
8.4 回收子進程
一個終止了但是沒有被回收的進程稱為殭屍進程。如果父進程已經終止了,那麼對應的沒終止的子進程就稱為孤兒進程。對於孤兒進程,核心會安排init進程稱為他的孤兒進程的養父。init進程的PID是1,是在作業系統啟動後由核心建立的,init進程會回收孤兒進程。
一個進程可以調用waitpid函數來等待它的子進程終止或者停止。
#include<sys/types.h>#include<sys.wait.h>pid_t waitpid(pid_t pid,int *statusp,int options);//pid 等待終止的目標子進程的ID,如果傳遞-1,則與wait函數相同,可以等待任意子進程終止//statusp 傳入變數的地址值//如果設定為WNOHANG,即使沒有終止的子進程也不會進入阻塞狀態,而是返回0並且退出
//如果成功則返回子進程的ID,如果是WNOHANG,則為0,出錯則為-1
- WIFEXITED子進程正常終止時則返回真;
- WEXITSTATUS返回子進程的傳回值。
if(WIFEXITED(statusp)){//是正常終止嗎? puts("Normal termination!"); printf("Child pass num:%d",WEXITSTATUS(statusp));//那麼傳回值是多少?}
如果想在子進程載入其他的程式,就需要使用 execve
函數,具體可以查看對應的 man page,這裡不再深入。
8.5 訊號
Linux 的進程樹,可以通過 pstree
命令查看。
對於前台進程來說,我們可以在其執行完成後進行回收,而對於後台進程來說,因為不能確定具體執行完成的時間,所以終止之後就成為了殭屍進程,無法被回收並因此造成記憶體泄露。
編號 |
名稱 |
預設動作 |
對應事件 |
2 |
SIGINT |
終止 |
使用者輸入 ctrl+c |
9 |
SIGKILL |
終止 |
終止程式(不能重寫或忽略) |
11 |
SIGSEGV |
終止且 Dump |
段衝突 Segmentation violation |
14 |
SIGALRM |
終止 |
時間訊號 |
17 |
SIGCHLD |
忽略 |
子進程停止或終止 |
CSAPP讀書筆記--第八章 異常控制流程