第8章 異常控制流

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提供了多種向進程發送進程的機制。這些機制是基於進程組的;

  1. 進程組

    • 每個進程只屬於一個進程組,進程組ID號由正整數表示;
    pid_t getpgrp(void);//獲得當前進程的進程組id   
    int setpgid(pid_t pid,pid_t pgid);
    • 當pid爲0時,改變調用進程,非零時,對應進程
    • 當pgid爲0時,調用進程pid作爲進程組號,非零時對應號;
  2. 用/bin/kill程序發送信號

    /bin/kill -9 15213 當-15213時,結束進程組號爲15213的每一個進程

  3. 從鍵盤發送信號
    • 作業: shell對一個命令行求職而創建的進程;
    • 任何時刻,只有一個前臺作業,0個或者多個後臺作業
    • 外殼爲每個作業創建獨立的進程組
    • ctrl+c 導致SIGINT被髮送到外殼,外殼捕獲信號,然後發送到前臺進程中的每個進程
  4. 用kill函數發送信號

    int kill(pid_t pid,int sig);
    • pid > 0,發送給指定進程, pid < 0,給進程組每個進程
  5. 用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第二個參數;
    這裏寫圖片描述

  • 另一個應用,從中斷程序中不返回到打斷位置,而是利用保存的環境,重新啓動;
    這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章