作 者:道哥,10+年的嵌入式開發老兵。
公眾號:【IOT物聯網小鎮】,專注於:C/C++、Linux作業系統、應用程式設計、物聯網、微控制器和嵌入式開發等領域。 公眾號回覆【書籍】,獲取 Linux、嵌入式領域經典書籍。
轉 載:歡迎轉載文章,轉載需註明出處。
在軟體開發中,中斷是一個繞不開的重要話題,但是,不知道您是否遇到過這樣的困惑:
很多書籍、文章在介紹中斷相關的知識點時,說的都挺有道理。
這篇文章對中斷的講解很正確,那篇文章在描述中斷的時候也挺對的,但是,這兩篇文章中,怎麼有些內容是矛盾的啊?!
單獨看任何一篇文章感覺都有道理,看的越多,反而越迷糊?
好比在森林裡迷路了,如果只有一個指南針,肯定能走出來。
但是,如果你有 2
個指南針,所指的方向卻是相反的,這個時候應該相信誰呢?!
我們仔細梳理了一下就會發現:每一篇文章都是在一定的語境、一定的上下文環境中來講解的,不同文章的矛盾之處,恰恰是它們所描述的那個上下文大環境不同。
上下文環境,就是描述當前正在執行的程式相關的靜態資訊,比如:有哪些程式碼段,棧空間在哪裡,程式描述資訊在什麼位置,當前執行到哪一條指令等等。
如果我們沒有一個全域性的視角,在同一個上下文環境中來對比不同的文章,就會讓自己的理解和認識越來越蒙圈。
因此,對於這種概念比較龐雜,無法用某種確定的邏輯來貫穿的知識點,在腦袋中一定要有一幅全域性的地圖。
只有對這個全域性的地圖掌握了,在具體學習每一個區域性的知識點時,才能知道自己所處的位置在哪裡,才不至於走偏。
這篇文章,我們繼續去繁從簡,從 8086
這個最簡單的處理器入手,來聊一下關於中斷的一些知識。
有了這個儲備,理清了基本的脈絡之後,以後再去學習 Linux
系統中的中斷相關內容時,才會有原來如此的感覺!
中斷向量與中斷描述符
中斷向量這個詞很時髦,也很神祕!
按道理,不應該在第一部分就端上中斷向量這盤硬菜,應該從中斷源開始聊起。
但是,畢竟我們已經學習過那麼多關於中斷的知識了,腦袋中肯定是對中斷已經有了一些的基本認知。
所以,在這裡我們還是首先來明確一下中斷向量和中斷描述符這個問題。
在前面的文章中已經聊過關於真實模式和保護模式的問題,在 【Linux 從頭學】這個系列中,我們一直以來描述的都是真實模式下的事情。
本文是真實模式下的最後一篇文章,下一篇文章將會進入保護模式。
那麼,中斷向量就是工作在真實模式下的,處理器通過中斷號和中斷向量,來定位到相應的中斷處理程式。
而中斷描述符呢,就是工作在保護模式下,處理器通過中斷號和中斷描述符,來定位到相應的中斷處理程式。
也就是說:中斷向量和中斷描述符,它倆的根本作用是一樣的。
只是它們存在於不同的大環境中,而且從描述上也能感覺到,保護模式下的中斷描述符會更復雜一些,功能也更強大一些。
它倆就像一對兄弟一樣,從外表上看是差不多,功能也是類似。但是透入到內部去看,就會發現有很多的不同之處。
因此,這篇文章我們講解的就是在真實模式下的中斷,這一點請大家先明白。
中斷的分類
在 x86
系統中,中斷的分類如下:
內部中斷
所謂的內部中斷,是在 CPU
內部產生並進行處理的。比如:
CPU 遇到一條除以 0 的指令時,將產生 0 號中斷,並呼叫相應的中斷處理程式;
CPU 遇到一條不存在的非法指令時,將產生 6 號中斷,並呼叫相應的中斷處理程式;
對於內部中斷,有時候也稱之為異常。
軟中斷也屬於內部中斷,是非常有用的,它是由 int
指令觸發的。比如 int3
這條指令,gdb
就是利用它來實現對應用程式的除錯。
很久之前寫過這樣的一篇文章原來gdb的底層除錯原理這麼簡單,其中就描述了 gdb
是如何通過插入一個 int
指令,來替換被除錯程式的指令碼,從而實現斷點除錯功能的。
外部中斷
x86
CPU
上有 2
箇中斷引腳:INT
和 INTR
,分別對應:不可遮蔽中斷和可遮蔽中斷。
所謂不可遮蔽,就是說:中斷不可以被忽視,CPU
必須處理這個中斷。
如果不處理,程式就沒法繼續執行。
而對於可遮蔽中斷,CPU
可以忽略它不執行,因為這類中斷不會對系統的執行造成致命的影響。
對於外部的可遮蔽中斷,CPU
上只有一根 INTR
引腳,但是需要產生中斷訊號的裝置那麼多,如何對眾多的中斷訊號進行區分呢?
一般都是通過可程式設計中斷控制器(Programmable Interrupt Controller, PIC
),在計算機中使用最多的就是 8259a
晶片。
雖然現代計算機都已經是 APIC
(高階可程式設計中斷控制器) 了,但是由於 8259a
晶片是那麼的經典,大部分描述外部中斷的文章都會用它來舉例。
每一片 8259a
可以提供 8
箇中斷輸入引腳,兩片晶片級聯在一起,就可以提供 15
箇中斷訊號:
主片的輸出引腳 INT 連線到 CPU 的 INTR 引腳上;
從片的輸出引腳 INT 連線到主片的引腳 2 上;
這樣的話,兩片 8259a
晶片就可以向 CPU
提供 15
箇中斷訊號了,比如:滑鼠、鍵盤、串列埠、硬碟等等外設。
8259a 之所以稱作可程式設計,是因為它的內部有相關的暫存器。
可以通過指定的埠號,對這些暫存器進行設定,讓 8 根 IRQ 中斷線上的訊號,在送到 CPU 時,對應不同的中斷號。
另外,對於外部可遮蔽中斷,有 2
層的遮蔽機制:
在 8259 晶片中,有中斷遮蔽暫存器,可以對 IRQ0 ~ IRQ7 輸入引腳進行遮蔽;
在 CPU 內部,也有一個標誌暫存器,可以對某一類中斷訊號進行遮蔽;
中斷號
在 x86
處理器中,一共支援 256
箇中斷,每一箇中斷都分配了一箇中斷號,從 0
到 255
。
其中,0 ~ 31
號中斷向量被保留,用來處理異常和非遮蔽中斷(其中只有 2
號向量用於非遮蔽中斷,其餘全部是異常)。
當 BIOS
或者作業系統提供了異常處理程式之後,當一個異常產生時,就會通過中斷向量表找到響應的異常處理程式,查詢的過程馬上就會介紹到。
從中斷號 32
開始,全部分配給外部中斷。
比如:
系統定時器中斷 IRQ0,分配的就是 32 號中斷;
Linux 的系統呼叫,分配的就是 128 號中斷;
我們來分別看一下內部中斷和外部中斷相關的中斷號:
對於通過 8259a
可程式設計中斷控制器接入的中斷訊號分配如下圖所示:
剛才已經說過,8259a
是可程式設計的,假如我們通過配置暫存器,把 IRQ0
的中斷號設定為 32
, 那麼主片上 IRQ1 ~ IRQ7
所對應的中斷號依次加 1
,從片上 IRQ8~IRQ15
對應的中斷號也是依次遞增。
所以,有時候我們可以在程式碼中斷看到下面的巨集定義:
中斷向量和中斷處理程式
當一箇中斷髮生的時候,CPU
獲取到該中斷對應的中斷號,下一步就是要確定呼叫哪一個函式來處理這個中斷,這個函式就稱作中斷服務程式(Interrupt Service Routine,ISR),有時候也稱作中斷處理程式、中斷處理函式,本質都一樣。
中斷向,就是通過中斷號去查詢處理程式的重要的橋樑!
中斷向量的本質
在 8086
中,一箇中斷向量,就是一個 段地址:中斷處理函式偏移量 這樣的一對資料,通過這個資料,就可以定位到記憶體中指定位置的那個中斷處理函式。
非常類似於高階程式語言中的函式指標,就是用來指向一個函式的開始地址。
8086
規定:256
箇中斷向量,必須從記憶體的 0
地址處開始存放。
每一箇中斷向量佔用 4
個位元組(2
個位元組的段地址,2
個位元組的偏移地址),256
箇中斷一共佔用了 1024
個位元組的空間。
之前的文章中,已經介紹過相關的記憶體模型,如下圖所示:
如果把一箇中斷向量看作函式指標,那麼這個中斷向量表就相當於是函式指標陣列。
舉例:
假設 2
號中斷被觸發了,CPU
就會到中斷向量表中查詢 2
號中斷的中斷向量。
因為每一箇中斷向量佔據 4
個位元組,那麼 2
號中斷向量的開始地址就是 2 * 4 = 8
,第 8
個位元組。
然後在第 8
個位元組開始,取 4
個位元組的內容:0x1000:0x2000
。
意思是:2
號中斷的處理函式,在段地址為 0x1000,偏移量為 0x2000 的位置處。
那麼 CPU
就按照 8086
的實體地址計算方式,得到中斷處理函式的實體地址為 0x12000
(段地址左移 4
位 + 偏移地址),於是就跳轉到該函式地址處去執行。
由於 Linux 系統是執行在保護模式,在這個模式下,當發生中斷時,是通過中斷描述符來查詢中斷處理函式的。
每一箇中斷描述符,描述了一箇中斷處理函式所在段的選擇子和偏移量,本質上也是用來查詢一箇中斷處理函式。
中斷處理程式的安裝
既然通過中斷向量,找到了中斷處理程式,那麼這些中斷處理程式都是誰放在記憶體中的呢?
如果您看過一些比較底層的計算機書籍,就能看到一般都會舉例:如何手動的把一個普通函式設定為一箇中斷處理程式。
操作步驟是:
在程式碼中,寫一個普通函式;
把這個函式的指令碼,搬運到記憶體中的某一個位置;
把這個位置(段地址:偏移量),作為一箇中斷向量,設定到中斷向量表中;
此時,如果發生了該中斷,你所提供的函式就作為中斷處理函式被執行了。
當然了,在一個計算機系統中,BIOS
、作業系統和各種外設,會自動為我們提供很多基本的中斷處理函式的。
比如:BIOS
中就提供了軟中斷、內部中斷、硬體中斷等處理函式,這些函式是固化在 BIOS
的程式碼中的(對映到 BIOS
所在的 ROM
晶片上),BIOS
只需要把這些處理函式的地址,寫入到中斷向量表中的相應位置即可。
在之前的文章中提到過,記憶體中的某些位置是對映到外設的 ROM
,在這些外設的 ROM
中也存在一些外設自帶的程式。
BIOS
在啟動時,會掃描這些對映到外設的記憶體空間,通過某些關鍵字資訊,如果發現外設有自帶的程式,就會去執行。
這些外設程式一般是進行一些自身的初始化,並填寫相關的中斷向量表,使它們指向外設自帶的中斷處理程式。
對於作業系統來說就更不用說了,它會重新安排自己需要的中斷處理函式,這部分內容我們以後再一起學習、討論!
中斷現場的保護和恢復
當一箇中斷髮生的時候,肯定有一個正在執行的程式被打斷。
當中斷處理函式執行結束之後,這個被打斷的程式需要從剛才被打斷的地方繼續執行(暫時先不要考慮從中斷返回點,進行多工切換的事情)。
而一個程式執行的上下文環境,就是處理器中的各種暫存器內容:程式碼段暫存器 cs
,指令指標暫存器 sp
,標誌暫存器 FLAGS
。
但是,在中斷處理程式中,也需要使用這些暫存器。
處理器中的這些暫存器,就是每一個程式執行時上下文資訊的儲存容器,當然也包括終端處理程式!
因此,在進入中斷處理程式之前,CPU
會自動的把這些暫存器 push
到棧中儲存起來,然後再跳轉到中斷處理程式中去執行。
當中斷處理程式執行結束後,CPU
會從棧中彈出這些內容,恢復到相應的暫存器中,於是被打斷的程式就可以繼續執行了。
總結:中斷的本質
從功能的角度看,中斷有 2
個作用:
提供執行非同步序列的機制;
給應用程式提供進入系統層的入口;
關於第 2
點,以後在介紹到 Linux
中的 int 0x80
中斷就非常清楚了,也就是通過中斷,讓應用層的程式有機會進入到系統程式碼中去執行。
因為應用層與作業系統層的程式碼,是工作在不同的安全級別。
為了系統的安全,Linux
作業系統提供了這樣的一個機制,讓低安全級別的應用程式,進入到高安全級別的作業系統程式碼中去執行,畢竟所有的硬體等系統資源都是由作業系統來統一管理的。
我們再從中斷處理程式的安裝角度來看,中斷本質上就是增加了一層間接性:通過固定位置的中斷向量表,讓中斷處理函式的實際地址可以被動態的放在任意位置。
為什麼這麼說?
假如作業系統想為某一箇中斷提供處理函式,那麼這個處理函式的地址放在記憶體中的什麼位置比較合適?
需要考慮 CPU
, 記憶體大小和佈局等多種因素,非常複雜!
而通過使用中斷向量表,就在一個固定位置處存放了很多個“指標”。
當中斷處理函式放在記憶體中某個任意位置之後,讓“指標”指向這個函式的地址就可以了,從而達到解耦的目的。
這樣的話,無論是發生硬體中斷,還是應用層程式碼通過中斷門來呼叫作業系統提供的函式,只要觸發相應的中斷就可以了,簡化了 CPU
的設計。
關於中斷的相關內容,還有很多需要學習,任重而道遠!
特別是在 Linux
系統中,中斷處理又分為上半部分、下半部分,而下半部分又可以根據不同的功能需求採取不同的機制來處理。
我仍然是持有之前的觀點:磨刀不誤砍柴工。
把學習週期拉長,一點一滴的積累,Haste Makes Waste!
推薦閱讀
【1】C語言指標-從底層原理到花式技巧,用圖文和程式碼幫你講解透徹
【2】一步步分析-如何用C實現物件導向程式設計
【3】原來gdb的底層除錯原理這麼簡單
【4】內聯彙編很可怕嗎?看完這篇文章,終結它!
其他系列專輯:精選文章、C語言、Linux作業系統、應用程式設計、物聯網