中斷的歷史原因
在聊中斷機制之前,我想先和大家聊一聊中斷機制出現(xiàn)的前因后果。最一開始計算機操作系統(tǒng)的設(shè)計是能夠一次性的執(zhí)行所有的計算任務(wù)的,這被稱為順序執(zhí)行,也是批處理操作系統(tǒng)(Batch system)。
順序執(zhí)行的意思是一個任務(wù)接著一個任務(wù)的依次執(zhí)行,就像我們編寫代碼的時候,我們肯定是寫完一行代碼才會寫下一行代碼,此時的計算機也是這樣的,執(zhí)行完一個任務(wù)后才會執(zhí)行下一個。就相當(dāng)于 main 函數(shù)里面只有一個 while(1) ,永不停止。
這樣的操作系統(tǒng)是當(dāng)時最高效的系統(tǒng),但是這種系統(tǒng)會存在兩個問題:
下一個任務(wù)只能在當(dāng)前任務(wù)執(zhí)行完成后才得以執(zhí)行,拿上圖來說就是任務(wù) A 執(zhí)行完成后才會執(zhí)行任務(wù) B,任務(wù) C 在任務(wù) A 和任務(wù) B 執(zhí)行完成后才會得到執(zhí)行,任務(wù) D 同理。當(dāng)任務(wù)執(zhí)行遇到問題或者出錯時,就直接修改當(dāng)前任務(wù)的 PC 指針,將其指向下一個任務(wù)就完事了。
任務(wù)執(zhí)行的次序是單項的,就是只能以 A -> B -> C -> D 這樣的次序執(zhí)行,不能以 D -> C -> B -> A 這樣反向的順序執(zhí)行。
這樣的操作系統(tǒng)無疑是很簡陋的(或者此時不應(yīng)該稱之為操作系統(tǒng),實際上就是一個監(jiān)控系統(tǒng))。
隨著時代的發(fā)展,后來出現(xiàn)了很多計算機,不過此時計算機還沒有改變依次執(zhí)行順序,當(dāng)計算機在做 IO 任務(wù)的時候,計算任務(wù)必須等待;在做計算任務(wù)的時候,IO 任務(wù)必須等待。這顯然是一個急需解決的問題。
一直等到 IBM 開發(fā)的 OS/360 計算機才解決了這個問題,OS/360 可以說算是一個劃時代的標(biāo)志,因為它有一個很重要的特點是能夠允許多道程序運行,并且能夠?qū)崿F(xiàn)多道任務(wù)之間的切換,這些任務(wù)可以是 IO 任務(wù),也可以是計算任務(wù)。但是這些任務(wù)執(zhí)行于何處停止,何時進(jìn)行切換卻沒有一個明確的標(biāo)準(zhǔn)。
后來出現(xiàn)了 MIT 開發(fā)的 MULTICS 操作系統(tǒng),這種操作系統(tǒng)是一種分時系統(tǒng),它允許每個任務(wù)都各自運行一段時間后再進(jìn)行切換,這樣能夠兼顧所有的任務(wù),使他們都能夠得到運行。雖然解決了分時復(fù)用的問題,但是不同任務(wù)所需要的時間并不一定是恒定的,所以 MULTICS 注定了只能是個過度。
后來出現(xiàn)了大名鼎鼎的 UNIX,由?Dennis Ritchi 丹尼斯里奇
?和?Ken Thompson 肯湯姆森
?共同開發(fā),UNIX 是一個簡化版的 MULTICS ,核心概念差不多,但是 UNIX 卻更加靈活和成功。奠定了小型化機器流行的基礎(chǔ)。
在 UNIX 開發(fā)出來不久,Andrew Tanenbaum 也開發(fā)出來了一套操作系統(tǒng) MINIX,不過這個操作系統(tǒng)是用于教學(xué)目的,沒有開源,而 Tanenbaum 就是寫現(xiàn)代操作系統(tǒng)的那個大佬。
又過了幾年,Linus Torvalds 基于 UNIX 操作系統(tǒng)開發(fā)了 Linux,一直流傳至今。
我沒有查到中斷到底是何時引入的,但是從 Linux 問世以來就已經(jīng)有了,而且 Linux 是基于 UNIX 開發(fā)的,可以認(rèn)為 UNIX 就已經(jīng)引入中斷機制了,而且換個角度來說,UNIX 作為如此著名的操作系統(tǒng),應(yīng)該會引入中斷機制的。
當(dāng)然我知道大多數(shù)人對計算機歷史沒有太多興趣,所以我們現(xiàn)在還是切回主線了。
中斷的概念和相關(guān)原理
中斷是指計算機在運行過程中,由于某些原因(這個原因可以是系統(tǒng)外部、也可以是系統(tǒng)內(nèi)部或者程序出現(xiàn)緊急事件)不得不停下來當(dāng)前正在執(zhí)行的任務(wù),轉(zhuǎn)而處理其他任務(wù)的過程,在處理完其他事情后,計算機會返回繼續(xù)執(zhí)行當(dāng)前任務(wù),這個完整的過程就被稱為中斷(Interrupt)
。
還有一種處理方式是輪詢,現(xiàn)代計算機一般都包含輸入輸出設(shè)備,在輪詢機制中,CPU 會不斷的順序詢問每個設(shè)備是否需要提供服務(wù),如果需要提供服務(wù),CPU 就會轉(zhuǎn)而為設(shè)備驅(qū)動進(jìn)行服務(wù);可以看到,這種輪詢的方式性能較差,而且比較耗費 CPU 資源。
輪詢的方式可以看做是一種被動要求 CPU 為其服務(wù)的方式,而中斷可以看做是一種主動要求 CPU 為其服務(wù)的方式。從我們?nèi)粘I詈蛯W(xué)習(xí)過程中就能夠知道,主動要求的方式效率要比被動詢問的方式要高,因為你肯定也經(jīng)歷過上課老師問同學(xué)們會不會的時候,有人主動站起來問問題要比老師問每個學(xué)生沒有回復(fù)效率要高的多。
在中斷的過程中,設(shè)備會向 CPU 發(fā)出的請求,而這個請求被稱為中斷請求(IRQ - Interrupt Request),CPU 針對中斷請求做出響應(yīng)轉(zhuǎn)而執(zhí)行相關(guān)程序被稱為中斷服務(wù)程序(ISR - Interrupt Service Routine)或者叫中斷服務(wù)過程。
這里需要認(rèn)識一個新的概念:中斷控制器(PIC - Programmable Interrupt Controller),中斷控制器負(fù)責(zé)管理設(shè)備發(fā)出的這些中斷請求,簡單來說它就是這些中斷請求的管理者。這個玩意會和設(shè)備的引腳相連接以便接收設(shè)備發(fā)出來的中斷信號,當(dāng)設(shè)備激活 IRQ 時,中斷控制器會立刻檢測到并對其做出響應(yīng)。不過真實的情況是,計算機無時無刻都在發(fā)出 IRQ,所以中斷控制器經(jīng)常會收到很多 IRQ,甚至有可能 CPU 正在執(zhí)行中斷過程的同時 PIC 還收到了 IRQ,這時中斷控制器需要對這些 IRQ 排出一個響應(yīng)優(yōu)先級,來告知 CPU 應(yīng)該首先執(zhí)行哪個中斷處理程序。
PIC 更多是適用于單核 CPU ,對于多核 CPU 來說并不適用,適用于多核 CPU 的是 APIC,APIC 我們后面簡單提到一些,不過目前還是以 PIC 為主,因為 Linux 0.11 用的是 PIC。
中斷的具體過程是這樣的:PIC 會向 CPU 的引腳發(fā)出一個中斷信號,CPU 知道產(chǎn)生了中斷信號后會立刻停下當(dāng)前進(jìn)程,并詢問 PIC 需要執(zhí)行哪個中斷請求,PIC 通過數(shù)據(jù)總線告知 CPU 中斷號,CPU 根據(jù)中斷號去 IDT(中斷向量表)中取得中斷向量并執(zhí)行中斷處理程序,處理完成后,CPU 會返回當(dāng)前的任務(wù)繼續(xù)執(zhí)行。
上面聊到的這些中斷都是通過設(shè)備產(chǎn)生的中斷,這些中斷的本質(zhì)是外部設(shè)備產(chǎn)生的信號來告知操作系統(tǒng)其狀態(tài)的變化,這種中斷被稱為硬中斷
;還有一種中斷是軟中斷
,軟中斷通常是由軟件中引起中斷的指令產(chǎn)生的,比如 int 指令就會產(chǎn)生軟中斷,設(shè)備產(chǎn)生的硬中斷不會等待太長時間,響應(yīng)速度比較快,而指令產(chǎn)生的軟中斷是一種推后的機制,響應(yīng)速度不如硬中斷快。
80x86 的中斷系統(tǒng)
這部分主要介紹一下 x86 所使用的中斷控制芯片相關(guān)內(nèi)容,會涉及到一些嵌入式相關(guān)的知識。
80x86 組成的微機機系統(tǒng)中采用了 8259A 可編程中斷控制芯片。每個 8259A 芯片可以管理 8 個中斷源。通過多片級聯(lián)的方式,8259A 能構(gòu)成最多管理 64 個中斷向量的系統(tǒng)。在 PC/AT 系列兼容機中,使用了兩片 8259A 芯片,可以管理 15 級中斷向量,如下圖所示:
從圖中可以看到,圖上方是主芯片,圖下方是從芯片,從芯片的 INT 引腳連接到主芯片的 IR2 引腳上,這也就是說,從芯片的中斷信號可以作為主芯片的輸入信號。
8259A 是一塊可編程芯片,可以通過 IN 和 OUT 指令對 8259A 進(jìn)行編程,一旦完成了初始化編程,芯片就進(jìn)入了操作狀態(tài),此時芯片可以隨時響應(yīng)外部設(shè)備提出的中斷請求(IRQ0 - IRQ15)。通過中斷判優(yōu)選擇,芯片將當(dāng)前最高優(yōu)先級的中斷請求作為中斷服務(wù)對象,并通過 INT 請求通知 CPU 外中斷請求到來,然后根據(jù)中斷號執(zhí)行中斷處理程序。
中斷向量表
上面提到過中斷向量表是 CPU 根據(jù)中斷號執(zhí)行中斷處理程序前需要查詢的"一張表",獲取中斷向量值后就可以對應(yīng)中斷服務(wù)程序的入口值。
80x86 機器支持 256 個中斷,理論上每個中斷都需要安排一個中斷處理程序。在 80x86 實模式下,每個中斷向量由 4 個字節(jié)組成,這 4 個字節(jié)組成了一個中斷處理程序的段值和段內(nèi)偏移值,所以整個中斷向量表的大小是 1024 字節(jié)。在程序加電啟動時,程序進(jìn)入實模式,此時 ROM BIOS 會在物理地址 0x0000:0x0000 處完成中斷向量表的初始化。在中斷向量表中,中斷向量號順序排列,每個中斷向量號占用 4 字節(jié),因此每個中斷向量的內(nèi)存位置就是 [0x0000:N 乘 4,0x0000:N+1 乘 4 - 1) 。
中斷向量表在 32 位保護(hù)模式下也叫做中斷描述符表,也是我們常說的 IDT 表。
IDT 表和中斷向量表都是描述中斷服務(wù)程序地址的表項,基本上中斷向量表和 IDT 表換湯不換藥,只不過 IDT 表除了有中斷服務(wù)程序的地址外,還包含有特權(quán)級和描述符類別等信息。
對于 Linux 內(nèi)核來說,中斷信號分為兩類:硬件中斷和軟件中斷,每個中斷是由 0 - 255 之間的一個數(shù)字來標(biāo)識。對于中斷 int0 - int31 來說,每個中斷的功能都由 intel 制定或保留用,這些屬于軟件中斷,但是 intel 公司稱之為異常。叫做異常也是可以理解的,因為這些中斷都是在探測到異常情況下發(fā)出的。中斷 int32 - int255 可以由用戶自己設(shè)定。常見的硬件和軟件中斷描述見下表。
在 Linux 系統(tǒng)中,將 int32 - int47 對應(yīng)于 8259A 中斷控制芯片發(fā)出的硬件中斷請求信號 IRQ0 - IRQ15,并把程序編程發(fā)出的系統(tǒng)調(diào)用中斷設(shè)置為 int128 ,也就是 0x80。
下面是 8259A 芯片中斷請求發(fā)出的中斷號列表:
中斷請求號 | 中斷號 | 用途 |
---|---|---|
IRQ0 | 0x20(32) | 8253 發(fā)出的 100HZ 時鐘中斷 |
IRQ1 | 0x21(33) | 鍵盤中斷 |
IRQ2 | 0x22(34) | 接連從芯片 |
IRQ3 | 0x23(35) | 串行口 2 |
IRQ4 | 0x24(36) | 串行口 1 |
IRQ5 | 0x25(37) | 并行口 2 |
IRQ6 | 0x26(38) | 軟盤驅(qū)動器 |
IRQ7 | 0x27(39) | 并行口 1 |
IRQ8 | 0x28(40) | 實時鐘中斷 |
IRQ9 | 0x29(41) | 保留 |
IRQ10 | 0x2a(42) | 保留 |
IRQ11 | 0x2b(43) | 保留(網(wǎng)絡(luò)接口) |
IRQ12 | 0x2c(44) | PS/2 鼠標(biāo)口中斷 |
IRQ13 | 0x2d(45) | 數(shù)學(xué)協(xié)處理器中斷 |
IRQ14 | 0x2e(46) | 硬盤中斷 |
IRQ15 | 0x2f(47) | 保留 |
在系統(tǒng)剛剛初始化后,內(nèi)核在 head.s 程序中會對所有 256 個中斷向量進(jìn)行默認(rèn)設(shè)置。默認(rèn)設(shè)置就是給這些中斷向量隨便設(shè)置一個初值,設(shè)置這個值的目的是為了防止出現(xiàn)一般保護(hù)性錯誤。
一般保護(hù)性錯誤:是指在英特爾 x86 架構(gòu)和 AMDx86-64 架構(gòu)和其它架構(gòu)中的一種中斷情況,指正在運行的程序(內(nèi)核或用戶態(tài)程序)違反處理器架構(gòu)中保護(hù)措施的情況。
最常見的情況就是
Linux 中的這些中斷不會所有的都用到,有些中斷是保留中,另外對于系統(tǒng)中所使用的一些中斷,內(nèi)核會在其初始化過程中重新設(shè)置這些中斷描述符,讓他們指向?qū)嶋H的處理過程。
另外,在設(shè)置中斷描述符表 IDT 表時 Linux 內(nèi)核使用了中斷門和陷阱門兩種門描述符。它們之間的區(qū)別在于對標(biāo)志寄存器 EFLAGS 中的中斷允許標(biāo)志 IF 的影響。由中斷門描述符執(zhí)行的中斷會復(fù)位 IF 標(biāo)志,因此可以避免其他中斷干擾當(dāng)前中斷的處理。隨后中斷結(jié)束后指令 iret 會恢復(fù) IF 標(biāo)志的原值;而通過陷阱門執(zhí)行的中斷不會響應(yīng) IF 標(biāo)志。
這里需要說一下兩個指令 cli 和 sti,為了避免競爭條件對臨界代碼的干擾,在 Linux 0.11 內(nèi)核中很多地方都使用了 cli 和 sti 指令。cli 指令用于復(fù)位 CPU 標(biāo)志寄存器 EFLAGS 中的中斷標(biāo)志,使得系統(tǒng)在執(zhí)行 cli 指令后不會響應(yīng)外部中斷。sti 指令用于設(shè)置標(biāo)志寄存器中的中斷標(biāo)志,能夠讓 CPU 識別并響應(yīng)外部設(shè)備發(fā)出的中斷。這倆相當(dāng)于是個可逆的關(guān)系。
當(dāng)一段代碼進(jìn)入可能引起競爭條件的臨界代碼區(qū)時,內(nèi)核中就會使用 cli 指令來關(guān)閉對外部中斷的響應(yīng),而在執(zhí)行完競爭代碼區(qū)時內(nèi)核就會執(zhí)行 sti 指令以重新允許 CPU 響應(yīng)外部中斷。如果不設(shè)置 cli 和 sti 的話,就可能引起對臨界代碼的多重寫操作,導(dǎo)致數(shù)據(jù)不一致,產(chǎn)生崩潰現(xiàn)象。