計算機系統中的異常 - 中斷

中斷和異常可以歸結爲一種事件處理機制,通過中斷或異常發出一個信號,然後操作系統會打斷當前的操作,然後根據信號找到對應的處理程序處理這個中斷或異常,處理完畢之後再根據處理結果是否要返回到原程序接着往下執行。

對於異常和中斷不同的書看起來都有不同的定義,但實際上講的都是同一回事,我這裏用 《intel architectures software developer's manual》這本書裏面的定義:

  • • An interrupt is an asynchronous event that is typically triggered by an I/O device.

  • • An exception is a synchronous event that is generated when the processor detects one or more predefined conditions while executing an instruction. The IA-32 architecture specifies three classes of exceptions: faults,traps, and aborts.

我們可以大致上把中斷理解爲是一個被 I/O 設備觸發的異步事件,例如用戶的鍵盤輸入。它是一種電信號,由硬件設備生成,然後通過中斷控制器傳遞給 CPU,CPU 有兩個特殊的引腳 NMI 和 INTR 負責接收中斷信號。

異常是一個同步的事件,通常由程序的錯誤產生的,或是由內核必須處理的異常條件產生的,如缺頁異常或 syscall 等。異常可以分爲錯誤、陷阱、終止。

上面所說的同步是因爲只有在一條指令執行完畢後 CPU 纔會發出中斷,而不是發生在代碼指令執行期間,比如系統調用;而異步意味着中斷能夠在指令之間發生。

當一個異常或中斷髮生時,處理器會停止執行當前程序或任務轉而去執行專門用於處理中斷或異常的程序。處理器會從中斷描述符表(IDT)中獲取到對應的處理程序,當異常或中斷執行完畢之後,會繼續回到被中斷的程序或任務繼續執行。

在 Linux 中,中斷處理程序就是一個 C 函數,只不過這些函數必須按照特定的類型聲明,以便內核能夠以標準的方式傳遞處理程序的信息。中斷處理程序運行於中斷上下文(interrput context)中,該上下文中的執行代碼不可阻塞。

在 x86 系統中,提前在 IDT 裏定好了 256 種異常(interrupt vector),0~31 的號碼對應的是由 Intel 規定的異常,32~255 的號碼對應的是操作系統定義的異常:

MX2Rnn

上面這張表可以在 《intel architectures software developer's manual》裏找到,我上面列舉了一些常見的異常,如除零、一般保護異常、缺頁、機器檢查等。

下面再來看幾種高層的異常和中斷形式:系統調用、上下文切換、信號等。

系統調用 system call

對於操作系統來說,設定了 4 個優先等級:

0 提供對所有硬件資源的直接訪問。限於最低級別的操作系統功能,如 BIOS、內存管理;

1 對硬件資源的訪問受到一些限制。可能會被庫程序和控制 I/O 設備的軟件使用;

2 對硬件資源的訪問更受限制。可能會被控制 I/O 設備的庫程序和軟件使用;

3 沒有對硬件資源的直接訪問。應用程序在這個級別運行;

由於系統的安全性和穩定性,用戶空間是無法直接執行系統調用的,需要切換到內核態執行。所以應用程序會通過int $n指令觸發一個異常 n 表示上面提到的 IDT 表中的異常序號,老版本的 Linux 內核用 int $0x80指令,觸發異常後會導致系統切換到內核態並執行對應的異常處理程序。

在 Linux 上,每個系統調用被賦予了一個系統調用號,內核記錄了系統調用表中的所有已註冊過的系統調用的列表,存儲在 sys_call_table 中。因爲所有系統調用陷入內核方式都一樣,所以系統調用號會通過 eax 寄存器傳遞給內核。在陷入內核之前,用戶空間就把相應的系統調用所對應的號放入 eax 中。

所以系統調用執行流程如下:

  1. 1. 應用程序代碼調用系統調用 (read),該函數是一個包裝系統調用的 庫函數 ;

  2. 2. 庫函數 (read) 負責準備向內核傳遞的參數,並觸發 異常中斷 以切換到內核;

  3. 3. CPU 被 中斷 打斷後,執行 中斷處理函數 ,即 系統調用處理函數 (system_call);

  4. 4. 系統調用處理函數 調用 系統調用服務例程 (sys_read),真正開始處理該系統調用;

上下文切換 context switch

上下文切換是一種較高層形式的異常控制流,操作系統通過它來實現多任務。也就是說上下文切換實際上是建立在較低的異常機制之上的。

內核通過調度器(scheduler)來控制當前進程是否可以被搶佔,如果被搶佔那麼內核會選擇一個新的進程運行,將舊進程的上下文保存起來,並恢復新進程的上下文,然後將控制轉交給新進程,這就是上下文切換。

當執行系統調用的時候可能發生上下文切換,例如有個進程因爲 read 系統調用訪問磁盤發生阻塞,那麼可以讓當前進程休眠切換到另一個進程;中斷也可能發生上下文切換,比如系統內部的週期性的定時器發生中斷時,內核覺得當前進程已經運行足夠長時間了,並切換到一個新的進程。

進程切換隻發生在內核態,在執行進程切換之前,用戶態進程使用的所有寄存器內容都已保存在內核態堆棧上。

上圖展示的是 A 和 B 進程之間切換的示例。進程 A 初始在用戶模式中,直到它通過執行系統調用 read 進入到內核,因爲磁盤讀取數據需要一定時間,所以內核執行進程 A 到 B 的切換。隨後磁盤數據 ready 之後磁盤控制器會發出一箇中斷信號,表示數據已經從磁盤傳送到了內存,那麼內核會從內核 B 切換到 A ,並接着執行進程 A 中緊隨在系統調用 read 之後的那條指令。

信號

信號是一種軟件形式的異常,它允許進程和內核中斷其他進程,可以通知進程系統中發生了一個某種類型的事件。每種信號類型都對應某種系統事件。底層的硬件異常是由內核異常處理程序處理的,正常情況下對用戶進程是不可見。但是信號可以通知用戶進程發生了某個異常,如進程試圖除 0 ,那麼內核就會發送給它一個 SIGFPE 信號;進程執行非法指令,內核就會發送一個 SIGILL 信號等等。

信號由兩個步驟組成,發送信號和接收信號。

一個發出而沒有被接受的信號叫做待處理信號(pending signal)。一個類型至多隻會由一個待處理信號,如果一個進程有一個類型爲 k 的待處理信號,那麼接下來發送到這個進程的類型爲 k 的信號都只會被丟棄。

一個進程還可以阻塞接收某種信號。當一個信號被阻塞時,它仍可以被髮送,但是不會被接收,知道進程取消對這種信號的阻塞。

內核爲每個進程在 pending 位向量中維護着待處理信號集合,在 blocked 位向量中維護被阻塞的信號集合。

進程從內核模式切換到用戶模式時,會檢查進程中未被阻塞的待處理信號的集合(pending & ~blocked),如果這個集合爲空,那麼內核將控制傳遞到進程的下一條指令(I_next);如果是非空,那麼內核選擇集合中某個信號 k (通常是最小的 k)強制進程接收,然後進程會根據信號觸發某種行爲,完成之後會回到控制流中執行下一個指令(I_next)。

如上圖,信號處理程序可以被其他信號處理程序中斷。

總結

本篇內容主要介紹了計算機系統中是如何通過異常和中斷來控制程序的執行。異常控制流發生在計算機系統的各個層次,是計算機系統中提供併發的基本機制。在低層次的硬件層,異常是通過異常控制器通過處理器給發送信號實現異常處理,這樣得以形快速的響應 I/O 事件;在操作系統層次上通過異常來使程序陷入內核態,然後進行系統調用;通過中斷還可以打斷當前的進程執行過程,從而實現進程之間的切換,這也是計算機系統中提供併發的基本機制;通過信號,還可以讓進程和內核中斷其他進程,從而實現進程間的控制。

Reference

《深入理解 Linux 內核》

《深入理解計算機原理》

https://bob.cs.sonoma.edu/IntroCompOrg-x64/bookch15.html

https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html

關注 luozhiyun 很酷,和他一起學習👆

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/8Plas3j-e5bavs8xvb_tmQ