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