圖解計算機中斷

現代計算機具有多任務處理的能力,可以同時運行着幾十上百的任務,如今很難想象,當我們點擊鼠標,需要等待計算機中的其他程序全部執行完畢

1956 年,IBM 7049 機器上首先使用了中斷技術,提升了計算機具備應對處理突發事件的能力,並開始使用 “中斷” 這一術語

中斷,英文爲Interrupt,即打斷。當 CPU 在正常運行程序執行任務時,接收到硬件傳過來的中斷信號 (interrupt request,IRQ),CPU 會中斷執行當前的工作任務 (被打斷),轉而去處理其他任務,等處理完後再回來繼續執行剛纔被暫時中斷的任務

常見的中斷類型

外部中斷和內部中斷

廣義上中斷按照中斷來源,可分爲外部中斷和內部中斷

與 CPU 執行指令無關,中斷信號來自 CPU 外部,一般指指由計算機外設發出的中斷請求,如:鍵盤中斷、打印機中斷、定時器中斷等。外部中斷既有可屏蔽的中斷也有不可屏蔽的中斷,也是狹義上的中斷(interrupt)

它不是由任何一條專門的指令造成的。比如硬盤,打印機,網絡適配器, 磁盤控制器等外部設備等硬件設備,通過向 CPU 上的引腳 (NMIINTR) 發信號,並將異常號放在系統總線上,來觸發中斷。

中斷是異步發生的(不同於同步:執行一條指令的結果),中斷處理程序總是返回到當前指令的下一條指令

與 CPU 執行指令有關,中斷信號來自 CPU 內部,一般指通過軟件調用的中斷,以及由執行指令過程中發生的錯誤所引起的中斷,所以也稱爲異常(exception),如:trap 指令、地址越界、算術溢出、虛存系統的缺頁;

我們下文會具體講講 x86 下的異常,接下來還是會繼續講講中斷的其他分類

不可屏蔽中斷和可屏蔽中斷

中斷按照是否可被屏蔽,可分爲 2 類:不可屏蔽中斷和可屏蔽中斷

不可屏蔽中斷就是當不可屏蔽中斷源一旦提出請求,表明問題非常嚴重或者系統發生了致命的錯誤,CPU 必須立即無條件響應

另外不可屏蔽中斷從源頭還可以分爲,既可由 CPU 內部產生,也可由外部 NMI 引腳產生,比如因運算出錯(協處理器運算出錯、除數爲零、運算溢出、單步中斷等)或 因硬件出錯(如電源掉電,硬件線路故障等)所引起的中斷

那什麼是 NMI 引腳?

其實 NMI 和下面的 INTR 都是 CPU 上的引腳,INTR(Interrupt Require) 表示可屏蔽中斷請求NMI(Nonmaskable Interrupt) 表示不可屏蔽中斷請求,我們來看下 8086CPU 的引腳圖:

NMI 和 INTR 在上圖左下角

所以不可屏蔽中斷除了可由 CPU 內部產生,還可以由外部硬件的中斷通過 NMI 這根信號線來通知 CPU 產生

可屏蔽中斷就是當可屏蔽中斷源提出請求**,**CPU 可以響應,也可以不響應;一般是由外部硬件的中斷通過 **INTR** 這根信號線來通知 CPU 產生的,比如硬盤,打印機,網卡等外部設備產生中斷,這類中斷並不會影響計算機的正常運行。不像不可屏蔽中斷,它是沒有內部中斷的,因爲**內部中斷是不可屏蔽的中斷**

對於可屏蔽中斷,除了受本身的屏蔽位的控制外,還都要受一個總的控制,即 CPU 標誌寄存器中的中斷允許標誌位 IF(Interrupt Flag) 的控制,若IF位爲 1,可以得到 CPU 的響應,否則得不到響應。而不可屏蔽中斷是不受中斷標誌位 IF 的影響,不管 IF 是什麼,CPU 都必須響應

隨着保護模式的流行,Intel意識到使用中斷來控制固件已不再是一種解決方案,引入系統管理模式 SMM 添加到 CPU 中,與正常中斷相反,SMM是 CPU 的一種特殊模式;要想要輸入SMM,必須生成一個系統管理中斷 SMI,其是在 80386 的更高版本中引入的,可以用於透明地轉換硬件接口

隨着奔騰系列的問世,英特爾推出了 LAPIC(本地高級可編程中斷控制器),INTR和NMI消失了,取而代之的是LINT0LINT1(本地中斷),大家瞭解一下即可,本文的中斷還是基於INTR和NMI

硬件中斷和軟件中斷

根據中斷源的不同,可以把中斷分爲硬件中斷軟件中斷兩大類

硬件中斷是由硬件設備觸發的中斷,如時鐘中斷、串口接收中斷、外部中斷等。當硬件設備有數據或事件需要處理時,會向CPU發送一箇中斷請求,CPU在收到中斷請求後,會立即暫停當前正在執行的任務,進入中斷處理程序中處理中斷請求。硬件中斷具有實時性強、可靠性高、處理速度快等特點

軟件中斷不是由硬件設備觸發的,而是由軟件程序主動發起的,如系統調用、軟中斷、異常、鍵盤管理中斷、顯示器管理中斷、打印機管理中斷等;軟件中斷需要在程序中進行調用,其響應速度和實時性相對較差,但是具有靈活性和可控性高的特點

與之對應的還有軟中斷和硬中斷

  1. 硬中斷是由外部事件引起的因此具有隨機性和突發性;硬中斷是否可以嵌套的,是否有優先級,由硬件設計體系決定的

  2. 軟中斷是執行中斷指令產生的,無面外部施加中斷請求信號,因此中斷的發生不是隨機的而是由程序安排好的。軟中斷是一種推後執行的機制

操作系統爲了提高中斷的處理效率,一般當中斷髮生的時候,硬中斷處理那些短時間,就可以完成的工作,而將那些比較耗時的任務,放到中斷之後來完成,也就是軟中斷來完成

中斷控制器

中斷控制器是計算機系統中的一個重要組成部分,**用於管理和控制中斷請求。**常見的中斷控制器有 Intel 8259A 芯片,我們簡單瞭解一下這個芯片:

Intel 處理器允許 256 箇中斷,中斷號的範圍是0~2558259A負責提供其中的 15 個,但中斷號並不固定,允許軟件根據自己的需要靈活設置中斷號,以防止發生衝突。該中斷控制器芯片有自己的端口號,可以像訪問其他外部設備一樣用 in 和 out 指令來改變它的狀態,包括各引腳的中斷號。所以又被稱爲可編程中斷控制器 PIC

上圖來源於百度百科

一個8259A芯片的組成可以分爲 5 個主要的邏輯控件:中斷屏蔽寄存器(IMR)、中斷請求寄存器(IRR)、優先級仲裁單元(PR)、中斷向量寄存器(ISR)和控制邏輯單元(Control Logic)

一個 8259A 芯片有IRQ0~IRQ7七個IRQ引腳,一個IRQ對應着一箇中斷號,一箇中斷號對應着一箇中斷向量,一箇中斷向量對應着一箇中斷處理子程序(ISR,Interrupt Service Routine)

8259A只適合單 CPU 的情況,爲了充分挖掘SMP體系結構的並行性,能夠把中斷傳遞給系統中的每個 CPU 至關重要。Intel引入了一種名爲I/O高級可編程控制器的新組件,來替代老式的8259A可編程中斷控制器 - 高級可編程中斷控制器(APIC),大家感興趣地自行去了解一下

陷阱、故障和終止

我們再回到上文的異常這塊,來了解一下 X86 下常見異常的類別:陷阱、故障和終止

  1. 陷阱 trap:是有意的異常,一般用來在用戶態和內核態之間提供系統調用接口,陷阱是同步異常,是執行一條指令的結果;陷阱程序總返回到當前指令的下一條指令,比如 C 語言中的 printf 函數,底層的實現中會有一條 int 0x80 指令,就是陷阱,即使用 0x80 號中斷實現系統調用

  2. 故障 fault:是由錯誤引起,但它可能被故障處理程序修正,故障是同步的,如果修正成功,將返回到當前正在執行的指令,CPU 重新執這條指令,否則將終止故障程序。

典型的一種故障,比如缺頁異常:當程序試圖訪問已映射在虛擬地址空間中,但是並未被加載在物理內存中的一個分頁時,由中央處理器的內存管理單元所發出的中斷。但缺頁異常是可以被修正的,有着專門的缺頁處理程序,根據缺頁中斷的不同類型會進行不同的處理

  1. 終止 abort:由不可恢復錯誤引起,會直接終止程序;終止是同步的,結束時不會返回任何指令即不會將控制返回給原程序

中斷異常的優先級

本文到現在我們也介紹了許多中斷和異常,他們之間也是有優先級的,我們這裏 Intel 的開發手冊爲例

我們接下來看看操作系統是如何處理中斷的?

中斷向量表 IVT

不同的中斷信號,需要用不同的中斷處理程序來處理。當 CPU 檢測到中斷信號後,會根據中斷信號的類型去查詢 “中斷向量表”,以此來找到相應的中斷處理程序在內存中的存放位置。

中斷向量表就是存放中斷號和中斷處理函數入口地址的表,結構類似數組,我們這裏以Linux0.12爲例,來看看其是如何實現中斷機制的:

實模式下,16 位的中斷機制依賴的是中斷向量表(IVT,Interrupt Vector Table),中斷向量表初始化在0x0000處,位置是固定的,IVT 由 BIOS 程序所使用,定義了 256 種中斷的入口地址,包括 16 位段地址和 16 位段內偏移量,其中將 0 到 31 保留用於異常處理和不可屏蔽中斷。

256種中斷如下:

0-19的中斷向量對應於異常和非屏蔽中斷。
20-31Intel保留
32-127可屏蔽硬件中斷
128用於系統調用的可編程異常
129-238可屏蔽硬件中斷
239本地APIC時鐘中斷
240本地APIC高溫中斷
241-250由Linux留作將來使用
251-253處理器間中斷
254本地APIC錯誤中斷
255本地APIC僞中斷(CPU屏蔽某個中斷時產生的)

當中斷髮生時,處理器要麼自發產生一箇中斷向量,要麼從 ** int n** 指令中得到中斷向量,或者從外部的中斷控制器接受一箇中斷向量。接着該向量作爲索引訪問中斷向量表,尋找對應的中斷處理程序入口地址 (**中斷處理函數的地址爲 = 中斷向量表地址 + 4 * n**),去執行程序

中斷描述符表 IDT

IDT,Interrupt Descriptor Table,即中斷描述符表,和GDT類似,記錄着 0~255 的中斷號和調用函數之間的關係,與中段向量表有些相似,但要包含更多的信息。

其中每一個表項叫做中斷描述符或門描述符(gate descriptor),的含義是指當中斷髮生時,必須先通過這些門,然後才能進入相應的處理程序

除了我們非常熟悉的中斷描述符,IDT 內還可以存放 2 種描述符:任務門描述符,陷阱門描述符

這些參數大家瞭解一下就行

  1. 中斷門 Interrupt Gate:中斷門包含段選擇符和中斷或異常處理程序的段內偏移量。當控制權轉移到一個適當的段時,處理器清 IF 標誌,從而關閉將來會發生的可屏蔽中斷,以避免嵌套中斷的發生。中斷門中的 DPL(Descriptor Privilege Level)爲 0,因此,用戶態的進程不能訪問 Intel 的中斷門。所有的中斷處理程序都由中斷門激活,並全部限制在內核態

什麼叫中斷嵌套?除了同種中斷,linux 任何一個新的硬中斷都可以打斷正在執行的中斷,形如嵌套;軟中斷無法嵌套,但相同類型的軟中斷可以在不同 CPU 上並行執行

  1. 陷阱門 Trap Gate:與中斷門類似,其唯一的區別是,控制權傳遞到一個適當的段時處理器不修改 IF 標誌,即不關中斷;一般中斷門用於處理中斷,而陷阱門用來處理異常

  2. 任務門 Task Gate:段選擇符中存放的是任務狀態段 TSS(Task State Segment)的選擇子,當中斷信號發生時,必須取代當前進程的那個進程的 TSS 選擇符存放在任務門中

實模式下,16 位的中斷機制依賴的是中斷向量表,中斷向量表初始化在0x0000處,位置是固定的。爲了讓操作系統的代碼中的邏輯地址和實際物理地址一致,操作系統啓動時會把 system 模塊搬到零地址處,這樣中斷向量表就會被覆蓋

而在保護模式下,中斷機制用的是中斷描述符表IDT,位置是不固定的,設計操作系統時可以靈活設置,只需最後把其地址賦值給 CPU 中的 IDTR 寄存器。中斷描述符表寄存器 IDTR 是一個 48 位的寄存器,其低 16 位保存中斷描述符表的大小,高 32 位保存 IDT 的基址。

當中斷髮生時,CPU 獲取到中斷向量後,通過IDTR的值,去查找IDT中斷描述符表,得到相應的中斷描述符,再根據中斷描述符記錄的信息來作權限判斷,運行級別轉換,最終調用相應的中斷處理程序

IDT 這個我們應該非常熟悉了,之前的文章中頻繁出現,我們再來回顧一下 IDT 中的中斷有哪些:

操作系統中的中斷機制

通常在操作系統中,中斷一般的處理流程如下:

  1. 外設 將中斷信號發送給中斷控制器8259A

  2. 8259A中優先級裁決器 PR 根據中斷優先級,有序地將中斷傳遞給 CPU

  3. CPU 中止執行當前程序流,將 CPU 所有寄存器的數值保存到棧中

  4. CPU 根據中斷向量,從中斷向量表 IDT 中查找中斷處理程序的入口地址,繼而執行中斷處理程序(期間還要檢查 IDT 表中門描述符的DPL,以保證當前程序有權限使用中斷服務程序)

  5. CPU 恢復寄存器中的數值,返回原程序流停止位置繼續執行

筆者再結合操作系統相關的知識,吐血畫了張圖,幫助大家更加直觀地瞭解中斷流程:

需要注意的是,中斷前後,進程的上下文的保存與恢復,上圖不是很詳細,但這部分我們其實在前一篇文章 Linux0.12 內核源碼解讀 (7)- 陷阱門初始化介紹過:

linux 調用中斷函數的流程:

linux0.12 對應上下文保存與恢復的源碼:

.globl _divide_error,_debug,_nmi,_int3,_overflow,_bounds,_invalid_op
    //.globl xx表示將符號標記爲一個全局符號,以供其他文件訪問!!!
.globl _double_fault,_coprocessor_segment_overrun
.globl _invalid_TSS,_segment_not_present,_stack_segment
.globl _general_protection,_coprocessor_error,_irq13,_reserved
.globl _alignment_check

_divide_error:
 pushl $_do_divide_error # 首先把將要調用的函數地址入棧;_do_divide_error是C函數do_divide_error被編譯後的名字
no_error_code:
   # 保存被中斷的進程的上下文
 xchgl %eax,(%esp) #_do_divide_error的地址→eax,eax被交換入棧
 pushl %ebx
 pushl %ecx
 pushl %edx
 pushl %edi
 pushl %esi
 pushl %ebp
 push %ds           # 16 位的段寄存器入棧後也要佔用 4 個字節
 push %es
 push %fs
 pushl $0     # "error code",將數值 0 作爲出錯碼入棧
 lea 44(%esp),%edx  # 取有效地址,即棧中原調用返回地址處的棧指針位置
 pushl %edx         # 並壓入堆棧(即esp0 指針入棧)

   # 所有段寄存器都設置爲內核數據段選擇符,設置好數據尋址的基址
 movl $0x10,%edx    # 初始化段寄存器ds、es和fs,加載內核數據段選擇符
 mov %dx,%ds
 mov %dx,%es
 mov %dx,%fs
 call *%eax         #* 號表示調用操作數指定地址處的函數,稱爲間接調用,這裏就是執行C語言函數do_divide_error

  # 恢復被中斷進程的上下文
  addl $8,%esp
 pop %fs
 pop %es
 pop %ds
 popl %ebp
 popl %esi
 popl %edi
 popl %edx
 popl %ecx
 popl %ebx
 popl %eax          # 彈出原來eax中的內容
 iret               # 返回中斷處理之前的程序,繼續執行後續指令

參考資料:

英特爾 ® 64 位和 IA-32 架構開發人員手冊:卷 3A - 英特爾 ®

《Linux 內核完全註釋 5.0》

https://www.codenong.com/40583848

https://zhuanlan.zhihu.com/p/651460336


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