while(true){
doNothing();
}
其他所有事情都是由操作系統(tǒng)提前注冊的中斷機制和其對應的中斷處理函數(shù)完成,我們點擊一下鼠標,敲擊一下鍵盤,執(zhí)行一個程序,都是用中斷的方式來通知操作系統(tǒng)幫我們處理這些事件,當沒有任何需要操作系統(tǒng)處理的事件時,它就乖乖停在死循環(huán)里不出來。所以,中斷,非常重要,它也是理解整個操作系統(tǒng)的根基,掌握它,不虧!那我們開始吧。
五花八門的中斷分類
關于中斷的分類,教科書上和網(wǎng)上有很多"標準"答案了,如果你用搜索引擎去尋找答案,可能會找出很多不一樣的分類結果。所以我打算直接在 Intel 手冊上找個最官方的標準答案。在 Intel 手冊 Volume 1 Chapter 6.4 Interrupts and Exception 給出。 翻譯過來就是,中斷可以分為中斷和異常,異常又可以分為故障、陷阱、中止。第一句話有點奇怪,啥叫中斷分為中斷和異常呢?你看好多文章的時候也是這么寫的,不知道你有沒有曾疑惑過。但其實原文的意思準確說是,CPU 提供了兩種中斷程序執(zhí)行的機制,中斷和異常。第一個中斷是個動詞,第二個中斷才是真正的機制種類。好吧,我感覺原文也挺奇怪的,但人家就這么叫,沒轍。接下來我只需要翻譯一下就好了,再夾雜點自己的解讀。An interrupt is an asynchronous event that is typically triggered by an I/O device.先說第一個機制中斷(interrupt),中斷是一個異步事件,通常由 IO 設備觸發(fā)。比如點擊一下鼠標、敲擊一下鍵盤等。An exception is a synchronous event that is generated when the processor detects one or more predefined conditions while executing an instruction.再說第二個機制異常(exception),異常是一個同步事件,是 CPU 在執(zhí)行指令時檢測到的反常條件。比如除法異常、錯誤指令異常,缺頁異常等。這兩個機制,殊途同歸,都是讓 CPU 收到一個中斷號,至于 CPU 收到這個中斷號之后干嘛,我們暫且不管。 我們先看看收到中斷號之前,具體就是中斷和異常到底是怎么做到給 CPU 一個中斷號的。先說中斷,別眨眼。有一個設備叫做可編程中斷控制器,它有很多的 IRQ 引腳線,接入了一堆能發(fā)出中斷請求的硬件設備,當這些硬件設備給 IRQ 引腳線發(fā)一個信號時,由于可編程中斷控制器提前被設置好了 IRQ 與中斷號的對應關系,所以就轉化成了對應的中斷號,把這個中斷號存儲在自己的一個端口上,然后給 CPU 的 INTR 引腳發(fā)送一個信號,CPU 收到 INTR 引腳信號后去剛剛的那個端口讀取到這個中斷號的值。估計你被繞暈了,但讀我的文章有個好處,太復雜就上動圖,來吧。 你看,最終的目標,就是讓 CPU 知道,有中斷了,并且也知道中斷號是多少。比如上圖中按下了鍵盤,最終到 CPU 那里的反應就是,得到了一個中斷號 0x21。那異常的機制就更簡單了,是 CPU 自己執(zhí)行指令時檢測到的一些反常情況,然后自己給自己一個中斷號即可,無需外界給。比如 CPU 執(zhí)行到了一個無效的指令,則自己給自己一個中斷號 0x06,這個中斷號是 Intel 的 CPU 提前就規(guī)定好寫死了的硬布線邏輯。好了,到目前為止,我們知道了無論是中斷還是異常,最終都是通過各種方式,讓 CPU 得到一個中斷號。只不過中斷是通過外部設備給 CPU 的 INTR 引腳發(fā)信號,異常是 CPU 自己執(zhí)行指令的時候發(fā)現(xiàn)特殊情況觸發(fā)的,自己給自己一個中斷號。還有一種方式可以給到 CPU 一個中斷號,但 Intel 手冊寫在了后面,Chapter 6.4.4 INT n,就是大名鼎鼎的 INT 指令。 INT 指令后面跟一個數(shù)字,就相當于直接用指令的形式,告訴 CPU 一個中斷號。比如 INT 0x80,就是告訴 CPU 中斷號是 0x80。Linux 內(nèi)核提供的系統(tǒng)調(diào)用,就是用了 INT 0x80 這種指令。那我們上面的圖又豐富了起來。 有的地方喜歡把他們做一些區(qū)分,把 INT n 這種方式叫做軟件中斷,因為他是由軟件程序主動觸發(fā)的。相應的把上面的中斷和異常叫做硬件中斷,因為他們都是硬件自動觸發(fā)的。但我覺得大可不必,一共就這么幾個分類,干嘛還要增加一層理解的成本呢,記三個方式不好么?好了,總結一下,給 CPU 一個中斷號有三種方式,而這也是中斷分類的依據(jù)。
1.通過中斷控制器給 CPU 的 INTR 引腳發(fā)送信號,并且允許 CPU 從中斷控制器的一個端口上讀取中斷號,比如按下鍵盤的一個按鍵,最終會給到 CPU 一個 0x21 中斷號。
2.CPU 執(zhí)行某條指令發(fā)現(xiàn)了異常,會自己觸發(fā)并給自己一個中斷號,比如執(zhí)行到了無效指令,CPU 會給自己一個 0x06 的中斷號。
3.執(zhí)行 INT n 指令,會直接給 CPU 一個中斷號 n,比如觸發(fā)了 Linux 的系統(tǒng)調(diào)用,實際上就是執(zhí)行了 INT 0x80 指令,那么 CPU 收到的就是一個 0x80 中斷號。
再往后,CPU 以各種不同的方式收到的這些 0x21 0x06 0x80,都會一視同仁,做同樣的后續(xù)處理流程,所以從現(xiàn)在開始,前面的事情就不用再管了,這也體現(xiàn)了分層的好處。收到中斷號之后 CPU 干嘛?
那 CPU 收到中斷號后,如何處理呢?先用一句不太準確的話總結,CPU 收到一個中斷號 n 后,會去中斷向量表中尋找第 n 個中斷描述符,從中斷描述符中找到中斷處理程序的地址,然后跳過去執(zhí)行。為什么說不準確呢?因為從中斷描述符中找到的,并不直接是程序的地址,而是段選擇子和段內(nèi)偏移地址。然后段選擇子又會去全局描述符表中尋找段描述符,從中取出段基址。之后段基址 + 段內(nèi)偏移地址,才是最終處理程序的入口地址。
當然這個入口地址,還不是最終的物理地址,如果開啟了分頁,又要經(jīng)歷分頁機制的轉換,就像下面這樣。
不過不要擔心,這不是中斷的主流程,因為分段機制和分頁機制是所有地址轉換過程的必經(jīng)之路,并不是中斷這個流程所特有的。所以我們簡單的把中斷描述符表中存儲的地址,直接當做 CPU 可以跳過去執(zhí)行的中斷處理程序的入口地址,就好了,不影響理解他們。你看,這是不是簡單很多。那接下來的問題就很簡單了,這里出現(xiàn)了兩個名詞,那就分別對他們進行發(fā)問。1. 中斷描述符表是啥?
2.中斷描述符是啥?
3. 去哪里找他們?
分別回答即可中斷描述符表是啥?
就是一個在內(nèi)存中的數(shù)組而已,操作系統(tǒng)初始化過程中,有很多結構都稱之為 XXX 表,其實就是個數(shù)組罷了。以 linux-2.6.0 源碼為例,就很直觀了。
structdesc_structidt_table[256]={{0,0},};
你看,是一個大小為 256 的數(shù)組。idt_table 這個名字就是 Interrupt Descriptor Table,逐字翻譯過來確實就是中斷描述符表。
中斷描述符是啥?
就是中斷描述符表這個數(shù)組里的存儲的數(shù)據(jù)結構,通過剛剛的源碼也可以看出來,是一個叫 desc_struct 的結構。
structdesc_struct{
unsignedlonga,b;
};
好家伙,Linux 源碼里就這么簡單粗暴表示,一個中斷描述符的大小為 64 位,也就是 8 個字節(jié),具體里面存的啥通過這個源碼看不出來。翻一下 Intel 手冊,在 Volumn 3 Chapter 5.11 IDT Descriptors 中找到了一張圖。
可以看到,中斷描述符具體還分成好幾個種類,有:Task Gate:任務門描述符
Interrupt Gate:中斷門描述符
Trap Gate:陷阱門描述符
不要慌,其中任務門描述符 Linux 中幾乎沒有用到。中斷門描述符和陷阱門描述符的區(qū)別僅僅是是否允許中斷嵌套,實現(xiàn)方式非常簡單粗暴,就是 CPU 如果收到的中斷號對應的是一個中斷門描述符,就修改 IF 標志位(就是一個寄存器中一位的值),修改了這個值后就屏蔽了中斷,也就防止了中斷的嵌套。而陷阱門沒有改這個標志位,也就允許了中斷的嵌套。所以簡單理解的話,你把他們當做同樣一個描述符就好了,先別管這些細節(jié),他們的結構幾乎完全一樣,只是差了一個類型標識罷了。那這個中斷描述符的結構長什么樣呢?我們可以清晰地看到,里面有段選擇子和段內(nèi)偏移地址。 回顧下剛剛說的中斷處理流程。沒騙你吧。
但以上這些如果你都搞不明白,還是那句話,記這個最簡單的流程就好了,不影響理解。好了,現(xiàn)在我們直觀地看到了中斷描述符表這個 256 大小的數(shù)組,以及它里面存的中斷描述符長什么樣子,最終的目的,還是幫助 CPU 找到一個程序的入口地址,然后跳轉過去。OK,下一個問題,就是 CPU 怎么尋找到這個中斷描述符表的位置呢?它是在內(nèi)存中一個固定的位置么?
CPU 怎么找到中斷描述符表
答案是否定的,中斷描述符表在哪里,全憑各個操作系統(tǒng)的喜好,想放在哪里就放在哪里,但需要通過某種方式告訴 CPU,即可。怎么告訴呢?CPU 提前預留了一個寄存器叫 IDTR 寄存器,這里面存放的就是中斷描述符表的起始地址,以及中斷描述符表的大小。在 Volumn 3 Chapter 5.10 Interrupt Descriptor Table 中告訴了我們 IDTR 寄存器的結構。 操作系統(tǒng)的代碼可以通過 LIDT 指令,將中斷描述符表的地址放在這個寄存器里。還記得剛剛看的源碼么?中斷描述符表就是這個。
structdesc_structidt_table[256]={{0,0},};
然后操作系統(tǒng)把這個的地址用 LIDT 指令放在 IDTR 寄存器就行了。IDTR 寄存器里的值一共 48 位,前 16 位是中斷描述符表大小(字節(jié)數(shù)),后 32 位是中斷描述符表的起始內(nèi)存地址,就是這個 idt_table 的位置。Linux-2.6.0 源碼中是這樣構造這個結構的,簡單粗暴。
idt_descr:
.word256*8-1
.longidt_table
緊接著,一個 LIDT 指令把這個結構放到 IDTR 寄存器中。
lidtidt_descr
整個過程一氣呵成,呵得我連代碼格式都懶得調(diào)了,是不是很清晰明了。
這樣,CPU 收到一個中斷號后,中斷描述符表的起始位置從 IDTR 寄存器中可以知道,而且里面的每個中斷描述符都是 64 位大小,也就是 8 個字節(jié),那自然就可以找到這個中斷號對應的中斷描述符。接下來的問題就是,這個中斷描述符表是誰來提前寫好的?又是怎么寫的?誰把中斷描述符表這個結構寫在內(nèi)存的
很簡單,操作系統(tǒng)唄。在 Linux-2.6.0 內(nèi)核源碼的 traps.c 文件中,有這樣一段代碼。
void__inittrap_init(void){
set_trap_gate(0,÷_error);
...
set_trap_gate(6,&invalid_op);
...
set_intr_gate(14,&page_fault);
...
set_system_gate(0x80,&system_call);
}
你看,我們剛剛提到的除法異常、非法指令異常、缺頁異常,以及之后可能通過 INT 0x80 觸發(fā)系統(tǒng)調(diào)用的中斷處理函數(shù) system_call,就是這樣被寫到了中斷描述符表里。
經(jīng)過這樣一番操作后,我們的中斷描述符表里的值就豐富了起來。好了,現(xiàn)在只剩下最后一個問題了,CPU 在找到一個中斷描述符后,如何跳過去執(zhí)行?
找到中斷描述符后,干嘛
現(xiàn)在這個問題可以再問得大一些了,就是 CPU 在收到一個中斷號并且找到了中斷描述符之后,究竟做了哪些事?當然,最簡單的辦法就是,直接把中斷描述符里的中斷程序地址取出來,放在自己的 CS:IP 寄存器中,因為這里存的值就是下一跳指令的地址,只要放進去了,到下一個 CPU 指令周期時,就會去那里繼續(xù)執(zhí)行了。但 CPU 并沒有這樣簡單粗暴,而是幫助我們程序員做了好多額外的事情,這增加了我們的學習和理解成本,但方便了寫操作系統(tǒng)的程序員,拿到一些中斷的信息,以及中斷程序結束后的返回工作。但其實,就是做了一些壓棧操作。
1. 如果發(fā)生了特權級轉移,壓入之前的堆棧段寄存器 SS 及棧頂指針 ESP 保存到棧中,并將堆棧切換為 TSS 中的堆棧。
2. 壓入標志寄存器 EFLAGS。
3. 壓入之前的代碼段寄存器 CS 和指令寄存器 EIP,相當于壓入返回地址。
4. 如果此中斷有錯誤碼的,壓入錯誤碼 ERROR_CODE
5. 結束(之后就跳轉到中斷程序了)
壓棧操作結束后,棧就變成了這個樣子。
特權級的轉移需要切換棧,所以提前將之前的棧指針壓入。錯誤碼可以方便中斷處理程序做一些工作,如果需要,從棧頂拿到就好了。拋開這兩者不說,剩下的就只有標志寄存器和中斷發(fā)生前的代碼地址,被壓入了棧,這很好理解,就是方便中斷程序結束后,返回原來的代碼嘛~具體的壓棧工作,以及如何利用這些棧的信息達到結束中斷并返回原程序的效果,Intel 手冊中也寫得很清楚。看下面的話,通過配合 IRET 或 IRETD 指令返回。由于后續(xù)版本的 Linux 自己的玩法比較多,已經(jīng)不用 Intel 提供的現(xiàn)成指令了,所以這回我們從 Linux-0.11 版源碼中尋找答案。比如除法異常的中斷處理函數(shù),在 asm.s 中。
_divide_error:
push dword ptr _do_divide_error ;
no_error_code: ;
xchg [esp],eax ;
push ebx
push ecx
push edx
push edi
push esi
push ebp
push ds ;
push es
push fs
push 0 ;
lea edx,[esp+44] ;
push edx
mov edx,10h ;
mov ds,dx
mov es,dx
mov fs,dx
call eax ;
add esp,8 ;
pop fs
pop es
pop ds
pop ebp
pop esi
pop edi
pop edx
pop ecx
pop ebx
pop eax ;// 彈出原來eax 中的內(nèi)容。
iretd
只看最后一行,確實用了 iretd 指令。這個指令會依次彈出棧頂?shù)娜齻€元素,把它們分別賦值給 EIP,CS 和 EFLAGS,而棧頂?shù)娜齻€元素,又恰好是 EIP,CS 和 EFLAGS 這樣的順序,你說這巧不巧?當然不巧,人家 CPU 執(zhí)行中斷函數(shù)前做了壓棧操作,然后又提供了 iret 指令做彈棧操作,當然是給你配套使用的!你看,中斷是如何切到中斷處理程序的?就是靠中斷描述符表中記錄的地址。那中斷又如何回到原來的代碼繼續(xù)執(zhí)行呢?是通過 CPU 幫我們把中斷發(fā)生前的地址壓入了棧中,然后我們程序自己利用他們?nèi)シ祷兀斎灰部梢圆环祷亍?/span>這就是 CPU 和操作系統(tǒng)配合的結果,把中斷這個事給解決了。總結
所以總結起來就是,理解中斷,只要回答了這幾個問題就好。如何給 CPU 一個中斷號?
外部設備通過 INTR 引腳,或者 CPU 執(zhí)行指令的過程中自己觸發(fā),或者由軟件通過 INT n 指令強行觸發(fā)。
同樣中斷也是這樣進行分類的。
CPU 收到中斷號后如何尋找到中斷程序的入口地址?通過 IDTR 寄存器找到中斷描述符表,通過中斷描述符表和中斷號定位到中斷描述符,取出中斷描述符表中存儲的程序入口地址。
中斷描述符表是誰寫的?操作系統(tǒng)代碼寫上去的。
找到程序入口地址之后,CPU 做了什么?簡單說,實際上做的事情就是壓棧,并跳轉到入口地址處執(zhí)行代碼。而壓棧的目的,就是保護現(xiàn)場(原來的程序地址、原來的程序堆棧、原來的標志位)和傳遞信息(錯誤碼)
好了,中斷講完了,如果再往后擴大一點點概念,以上說的中斷,統(tǒng)統(tǒng)都是硬中斷。注意,不叫硬件中斷哦。為什么叫硬中斷呢?因為這是 Intel CPU 這個硬件實現(xiàn)的中斷機制,注意這里是實現(xiàn)機制,并不是觸發(fā)機制,因為觸發(fā)可以通過外部硬件,也可以通過軟件的 INT 指令。那與硬中斷對應的還有軟中斷,這個概念網(wǎng)上好多地方都講錯了,把軟中斷和 INT 指令這種軟件中斷混淆了,所以我覺得軟件中斷最好稱其為,由軟件觸發(fā)的中斷,而軟中斷稱其為軟件實現(xiàn)的中斷。軟中斷是純粹由軟件實現(xiàn)的一種類似中斷的機制,實際上它就是模仿硬件,在內(nèi)存中有一個地方存儲著軟中斷的標志位,然后由內(nèi)核的一個線程不斷輪詢這些標志位,如果有哪個標志位有效,則再去另一個地方尋找這個軟中斷對應的中斷處理程序。軟中斷是 Linux 實現(xiàn)中斷的下半部的一種非常常見的方式,之后我講 Linux 內(nèi)核如何接受網(wǎng)絡包這個事情的時候也可以看到,軟中斷是研究整個過程的一個突破口。-
操作系統(tǒng)
+關注
關注
37文章
6825瀏覽量
123333 -
網(wǎng)絡
+關注
關注
14文章
7565瀏覽量
88792 -
中斷
+關注
關注
5文章
898瀏覽量
41502
原文標題:好家伙!原來硬中斷就是這樣的
文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論