圖解計算機中斷
現代計算機具有多任務處理的能力,可以同時運行着幾十上百的任務,如今很難想象,當我們點擊鼠標,需要等待計算機中的其他程序全部執行完畢
1956 年,IBM 7049 機器上首先使用了中斷技術,提升了計算機具備應對處理突發事件的能力,並開始使用 “中斷” 這一術語
中斷,英文爲Interrupt
,即打斷。當 CPU 在正常運行程序執行任務時,接收到硬件傳過來的中斷信號 (interrupt request,IRQ),CPU 會中斷執行當前的工作任務 (被打斷),轉而去處理其他任務,等處理完後再回來繼續執行剛纔被暫時中斷的任務。
常見的中斷類型
外部中斷和內部中斷
廣義上中斷按照中斷來源,可分爲外部中斷和內部中斷
- 外部中斷
與 CPU 執行指令無關,中斷信號來自 CPU 外部,一般指指由計算機外設發出的中斷請求,如:鍵盤中斷、打印機中斷、定時器中斷等。外部中斷既有可屏蔽的中斷也有不可屏蔽的中斷,也是狹義上的中斷(interrupt)
它不是由任何一條專門的指令造成的。比如硬盤,打印機,網絡適配器, 磁盤控制器等外部設備等硬件設備,通過向 CPU 上的引腳 (NMI 和 INTR) 發信號,並將異常號放在系統總線上,來觸發中斷。
中斷是異步發生的(不同於同步:執行一條指令的結果),中斷處理程序總是返回到當前指令的下一條指令
- 內部中斷
與 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
消失了,取而代之的是LINT0
和LINT1
(本地中斷),大家瞭解一下即可,本文的中斷還是基於INTR和NMI
硬件中斷和軟件中斷
根據中斷源的不同,可以把中斷分爲硬件中斷和軟件中斷兩大類
硬件中斷是由硬件設備觸發的中斷,如時鐘中斷、串口接收中斷、外部中斷等。當硬件設備有數據或事件需要處理時,會向CPU
發送一箇中斷請求,CPU
在收到中斷請求後,會立即暫停當前正在執行的任務,進入中斷處理程序中處理中斷請求。硬件中斷具有實時性強、可靠性高、處理速度快等特點
軟件中斷不是由硬件設備觸發的,而是由軟件程序主動發起的,如系統調用、軟中斷、異常、鍵盤管理中斷、顯示器管理中斷、打印機管理中斷等;軟件中斷需要在程序中進行調用,其響應速度和實時性相對較差,但是具有靈活性和可控性高的特點
與之對應的還有軟中斷和硬中斷:
-
硬中斷是由外部事件引起的因此具有隨機性和突發性;硬中斷是否可以嵌套的,是否有優先級,由硬件設計體系決定的
-
軟中斷是執行中斷指令產生的,無面外部施加中斷請求信號,因此中斷的發生不是隨機的而是由程序安排好的。軟中斷是一種推後執行的機制
操作系統爲了提高中斷的處理效率,一般當中斷髮生的時候,硬中斷處理那些短時間,就可以完成的工作,而將那些比較耗時的任務,放到中斷之後來完成,也就是軟中斷來完成
中斷控制器
中斷控制器是計算機系統中的一個重要組成部分,**用於管理和控制中斷請求。**常見的中斷控制器有 Intel 8259A 芯片,我們簡單瞭解一下這個芯片:
Intel 處理器允許 256 箇中斷,中斷號的範圍是0~255
,8259A
負責提供其中的 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 下常見異常的類別:陷阱、故障和終止
-
陷阱 trap:是有意的異常,一般用來在用戶態和內核態之間提供系統調用接口,陷阱是同步異常,是執行一條指令的結果;陷阱程序總返回到當前指令的下一條指令,比如 C 語言中的 printf 函數,底層的實現中會有一條 int 0x80 指令,就是陷阱,即使用 0x80 號中斷實現系統調用
-
故障 fault:是由錯誤引起,但它可能被故障處理程序修正,故障是同步的,如果修正成功,將返回到當前正在執行的指令,CPU 重新執這條指令,否則將終止故障程序。
典型的一種故障,比如缺頁異常:當程序試圖訪問已映射在虛擬地址空間中,但是並未被加載在物理內存中的一個分頁時,由中央處理器的內存管理單元所發出的中斷。但缺頁異常是可以被修正的,有着專門的缺頁處理程序,根據缺頁中斷的不同類型會進行不同的處理
- 終止 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 種描述符:任務門描述符,陷阱門描述符
這些參數大家瞭解一下就行
- 中斷門 Interrupt Gate:中斷門包含段選擇符和中斷或異常處理程序的段內偏移量。當控制權轉移到一個適當的段時,處理器清 IF 標誌,從而關閉將來會發生的可屏蔽中斷,以避免嵌套中斷的發生。中斷門中的 DPL(Descriptor Privilege Level)爲 0,因此,用戶態的進程不能訪問 Intel 的中斷門。所有的中斷處理程序都由中斷門激活,並全部限制在內核態
什麼叫中斷嵌套?除了同種中斷,linux 任何一個新的硬中斷都可以打斷正在執行的中斷,形如嵌套;軟中斷無法嵌套,但相同類型的軟中斷可以在不同 CPU 上並行執行
-
陷阱門 Trap Gate:與中斷門類似,其唯一的區別是,控制權傳遞到一個適當的段時處理器不修改 IF 標誌,即不關中斷;一般中斷門用於處理中斷,而陷阱門用來處理異常
-
任務門 Task Gate:段選擇符中存放的是任務狀態段 TSS(Task State Segment)的選擇子,當中斷信號發生時,必須取代當前進程的那個進程的 TSS 選擇符存放在任務門中
實模式下,16 位的中斷機制依賴的是中斷向量表,中斷向量表初始化在0x0000
處,位置是固定的。爲了讓操作系統的代碼中的邏輯地址和實際物理地址一致,操作系統啓動時會把 system 模塊搬到零地址處,這樣中斷向量表就會被覆蓋
而在保護模式下,中斷機制用的是中斷描述符表IDT
,位置是不固定的,設計操作系統時可以靈活設置,只需最後把其地址賦值給 CPU 中的 IDTR 寄存器。中斷描述符表寄存器 IDTR 是一個 48 位的寄存器,其低 16 位保存中斷描述符表的大小,高 32 位保存 IDT 的基址。
當中斷髮生時,CPU 獲取到中斷向量後,通過IDTR
的值,去查找IDT中斷描述符表
,得到相應的中斷描述符,再根據中斷描述符記錄的信息來作權限判斷,運行級別轉換,最終調用相應的中斷處理程序
IDT 這個我們應該非常熟悉了,之前的文章中頻繁出現,我們再來回顧一下 IDT 中的中斷有哪些:
操作系統中的中斷機制
通常在操作系統中,中斷一般的處理流程如下:
-
外設 將中斷信號發送給中斷控制器
8259A
; -
8259A
中優先級裁決器 PR 根據中斷優先級,有序地將中斷傳遞給 CPU -
CPU 中止執行當前程序流,將 CPU 所有寄存器的數值保存到棧中
-
CPU 根據中斷向量,從中斷向量表 IDT 中查找中斷處理程序的入口地址,繼而執行中斷處理程序(期間還要檢查 IDT 表中門描述符的
DPL
,以保證當前程序有權限使用中斷服務程序) -
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