2016-11-02html
中斷這個特性相比你們都不會陌生,稍微懂點操做系統知識的人均可以說到一二。可是要真正把中斷描述清楚,以及LInux中和windows中的實現方式,這可能仍是有點難度的。今天筆者就想徹頭徹尾的把中斷給詳細分析下。linux
說到中斷還不得不從現代操做系統的特性提及,不管是桌面PC操做系統仍是嵌入式都是多任務的操做系統,而很遺憾,處理器每每是單個的,即便在硬件成本逐漸降低,從而硬件配置直線上升的今天,PC機的核心可能已經達到4核心,8核心,而手機移動設備更難以想象的達到16核心,32核心,處理器的數量依然不可能作到每一個任務一個CPU,因此CPU必須做爲一種全局的資源讓全部任務共享。說到共享,如何共享呢?何時給任務A用,何時給任務B用......這就是進程調度,具體的安排就由調度算法決定了。進程如何去調度?現代操做系統通常都是採用基於時間片的優先級調度算法,把CPU的時間劃分爲很細粒度的時間片,一個任務每次只能時間這麼多的時間,時間到了就必須交出使用權,即換其餘的任務使用。這種要看操做系統的定時器機制了。那麼時間片到以後,系統作了什麼呢?這就要用到咱們的中斷了,時間片到了由定時器觸發一個軟中斷,而後進入相應的處理歷程。固然這一點不足以代表中斷的重要,計算機操做系統天然離不開外部設備:鼠標、鍵盤、網卡、磁盤等等。就拿網卡來說,我計算機並不知道時候數據包會來到,我能保證的就是數據來了我能正常接收就好了。可是我又不可能一直等着接收數據包,要是這樣其餘任務就死完了。因此合理的辦法是,你數據包來到以後,通知我,而後我再對你處理,怎麼通知呢??答:中斷!鍵盤、鼠標亦是如此!算法
好了閒話說了這麼多,進入正題吧!編程
如上面所述,中斷信號由外部設備發起,準確來講是由外部設備的控制器發起,由於外部設備自己並不能發起信號。必須網卡設備,的那個網絡數據包到達網卡,網卡的控制器就向IO APIC發送中斷信號,IO APIC把信號發送給本地APIC,本地APIC把信號傳送給CPU,若是根據當時狀況,要處理這個中斷,就保存當時的運行上下文,切換到中斷上下文中,根據IDT查找對應的處理函數進行處理。處理完成後,須要恢復中斷以前的狀態。windows
大體過程就如上面所述,可是具體這個過程是怎麼執行的,關鍵幾點以下:數組
一、設備控制器如何發送中斷信號安全
二、APIC如何接受中斷信號,以及作了什麼處理網絡
三、處理器收到中斷信號又作了什麼操做架構
在此以前,咱們須要介紹下中斷控制器8259A和APIC併發
8259A中斷控制器由兩片8259A芯片級聯組成,每一個芯片有8箇中斷輸入引腳,其中IRQ2被用來鏈接從芯片,因此一共能夠支持15箇中斷號,這也就是早期採用8259A中斷控制器只能使用15個外部中斷的緣由,使用8259A中斷控制器的工做架構以下:
每一個外部設備鏈接一條中斷線,當設備須要中斷CPU時,經過這些中斷線,發送中斷請求。中斷控制器感知到這些中斷請求,會設置中斷控制器中的中斷請求寄存器的相應位爲1,鑑於多箇中斷可能併發到達,中斷控制器具有中斷判優功能,當其選定一箇中斷做爲當前響應中斷時,會清除中斷請求寄存器中的對應位,而後設置中斷服務寄存器的某些位爲1,代表CPU正在服務於某個中斷請求。
另外8259A還有一個8位的中斷屏蔽寄存器,每一位對應於一箇中斷線,當對應的位被設置後,代表要屏蔽這些中斷。爲了處理不一樣優先級的中斷,中斷控制器還有同一個優先權判決器,當一箇中斷到達時,判斷到達的中斷優先級和ISR中正在服務的中斷優先級的大小,若高於正在服務的中斷的優先級,須要打斷當前中斷的處理,轉而處理新到達的中斷請求,不然,不予理會。
中斷觸發方式:
中斷請求輸入端IR0~IR7可採用的中斷觸發方式有電平觸發和邊沿觸發兩種,由初始化命令字ICW1中的LTIM位來設定。
當外部設備請求服務時,設置本身對應寄存器的位爲1,即成了高電平,那麼中斷控制器端就能夠接受到中斷信號,進入中斷的處理。
因爲8259A中斷控制器只能應用與單處理器,且其中斷源的限制,後來Intel開發了高級可編程中斷控制器APIC
APIC由兩部分:本地APIC和IO APIC。本地APIC和邏輯CPU綁定,它控制傳遞給邏輯處理器中斷信號和產生IPI中斷(這是處理器間中斷,只用於多處理器狀況)、
本地APIC能夠接受一下中斷源:
以上中斷源稱爲本地中斷源,當本地APIC接收到一箇中斷信號,會經過某個發送協議把信號發送給處理器核心,具體能夠經過一組被稱之爲local vector table 的APIC寄存器設置某個中斷源的中斷號。
而當接收外部中斷時,則須要經過IO APIC,那麼local vector table是個什麼東西呢?
local vector table
LVT容許用戶經過編程指定特定中斷的處理動做,每一箇中斷對應其中的一個表項,具體由一下幾個32位寄存器組成:
LVT CMCI Register(FEE0 02F0h)
LVT Timer Register(FEE0 0320h)
LVT Thermal Monitor Register(FEE0 0330h)
LVT Performance Counter Register(FEE0 0340h)
LVT LINT0 Register(FEE0 3350h)
LVT LINT1 Register(FEE0 0360h)
LVT Error Register(FEE0 0370h)
本地APIC和IO APIC 關係以下:
由上圖能夠看到,IO APIC實際上是做爲一個PCI設備掛載在PCI總線上,和傳統的PIC相比,IO APIC最大的做用在於中斷的分發,外部設備不直接鏈接在本地APIC,而是鏈接在IO APIC,由IO APIC處理中斷消息後發送給本地APCI。IO APIC通常由24箇中斷管腳,每一個管腳對應一個RTE,而且其各個管腳沒有優先級之分,具體中斷的優先級由其對應的向量決定,即前面所說的local vector table。每當IO APIC接收到一箇中斷消息,就根據其內部的PRT表格式化出一條中斷消息,發送給本地APIC。PRT表格式以下:
關於硬件先暫且介紹到這裏吧,描述硬件實在感受力不從心,感興趣的可參考具體的硬件手冊。
處理器收到中斷信號又作了什麼操做
在此以前咱們須要明白幾個概念:硬件中斷、軟件中斷、異常
雖然前面描述的不夠詳細,可是相信仍是能夠看出,中斷源能夠分爲兩部分:本地中斷源和外部中斷源。本地中斷源有些場合又稱爲軟件中斷,由於沒有具體的硬件與之對應。而那些由具體硬件觸發的中斷則稱爲硬件中斷。而異常則是程序指令流執行過程當中的同步過程,好比程序執行過程當中遇到除零錯,很顯然此時程序沒法繼續運行,只能處理完了這個異常,才能夠繼續運行。異常的同步特性和中斷的異步又是一個明顯的區別。另外在linux中爲了讓內核延期執行某個任務,也提出了一個軟中斷(software interrupt)的概念,這點在windows中與之對應的機制爲DPC,即延遲過程調用。這兩點我們後面在說。
暫且不說中斷異常的區別,系統使用一套機制來處理中斷和異常,即在內核中維護了一張IDT(Interrupt Descriptor Table)中斷描述符表,寄存器IDTR保存有表的基址。每一個表項爲8個字節。記錄對應中斷的處理函數的地址以及其餘的一些控制位。因此每一箇中斷對應一個表項。0-31號中斷號位系統爲預約義的中斷和異常保留的,用戶不得使用,因此硬件中斷號從32開始分發。
每當CPU接收到一箇中斷或者異常信號,CPU首先要作的決定是否響應這個中斷(具體由中斷控制器根據中斷優先級決定是否給CPU發送中斷信號),若是決定響應,就終止當前運行進程的運行,根據IDTR寄存器獲取中斷描述符表基地址,而後根據中斷號定位具體的中斷描述符。這裏中斷描述符可分爲兩種狀況:
一、 當中斷描述符對應的是中斷門或者陷阱門時,處理歷程運行在當前進程的上下文中,即不須要發生進程上下文的切換,只是若是處理歷程和當前進程的運行級別不一樣,則須要發生棧的切換,具體以下:
若是當前進程運行在level 3即用戶態,則當中斷髮生時:
若是中斷髮生時當前進程運行在內核態,則就不須要發生棧的切換,僅僅須要執行上述的後兩步。
具體動做參考下圖:
二、當中斷描述符對應一個任務門時,意味着這次中斷的處理由一個單獨的程序執行,和當前進程無關。使用新的任務處理中斷的優缺點也很明顯:
固然缺點也很明顯,每次中斷都會進行任務的切換,進程上下文的切換所帶來的開銷要比上面兩種方式大的多,而且每次中斷都要進行兩次進程切換:中斷進入和中斷返回。形成中斷響應延遲過大
因爲x86架構下的任務是非重入的,即一箇中斷處理程序執行期間會關中斷,那麼此時其餘的進程就得不到調度,假如說這個處理程序很繁瑣,那麼會出現CPU處理時間分配不均的狀況,且其餘的中斷得不到響應,這是不能容許的。因此操做系統在以前的基礎上把中斷處理歷程分紅兩部分:上半部和下半部。上半部主要處理哪些中斷來了必需要處理的事情,這個過程會關閉中斷,因此此過程儘量的短,在上半部處理結束,就開啓中斷。下半部主要處理不那麼急迫的事情,這個過程開啓中斷,這樣就增長了中斷響應的效率。Linux和windows都採用了這種機制。LInux中使用軟中斷,而windows總則使用DPC延遲過程調用。
下面咱們主要分析Linux下的softirq機制:
軟中斷可使內核延期執行某個任務,他們的運做方式和具體的硬件相似,甚至能夠說這裏就是模擬的硬件中斷,因此稱之爲軟件中斷也不爲過。既然提到軟中斷,那麼天然就設計到幾個點:
在3.11.1的內核版本中定義了10個軟中斷,而且系統不建議用戶本身添加軟中斷,因此對於軟中斷基本用於已定義好的功用,而若是用戶須要,可使用其中的一個類型即TASKLET_SOFTIRQ
具體的軟中斷類型以下:
1 enum 2 { 3 HI_SOFTIRQ=0, 4 TIMER_SOFTIRQ, 5 NET_TX_SOFTIRQ, 6 NET_RX_SOFTIRQ, 7 BLOCK_SOFTIRQ, 8 BLOCK_IOPOLL_SOFTIRQ, 9 TASKLET_SOFTIRQ, 10 SCHED_SOFTIRQ, 11 HRTIMER_SOFTIRQ, 12 RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ 13 14 NR_SOFTIRQS 15 };
每一個CPU維護一個軟中斷位圖__softirq_pending,實際上是一個32位的字段,每一位對應一個軟中斷。處理軟中斷時會獲取當前CPU的軟中斷位圖,根據各個位的設置,進行處理。
#define local_softirq_pending() __get_cpu_var(irq_stat).__softirq_pending
一、軟中斷的註冊
軟中斷的核心機制是一張表,相似於IDT,包含32個softirq_vec結構,該結構很簡單:就是一個函數地址,每一個軟中斷對應其中的一個,因此如今也僅僅使用前10項。
1 struct softirq_action 2 { 3 void (*action)(struct softirq_action *); 4 };
系統經過open_softirq函數註冊一個軟中斷,具體就是在softirq_vec數組中根據中斷號設置其對應的處理例程。
1 void open_softirq(int nr, void (*action)(struct softirq_action *)) 2 { 3 softirq_vec[nr].action = action; 4 }
nr是上面的一個枚舉值,action即是對應軟中斷的處理函數。
二、軟中斷的觸發
Linux系統經過raise_softirq函數引起一個軟中斷,每一個CPU有個軟中斷位圖,有32位,最多可對應32個軟中斷,當置位圖對應位爲1時,代表觸發了對應的軟中斷。在下次系統檢查是否有軟中斷時就會被檢測獲得,從而進行處理。
1 void raise_softirq(unsigned int nr) 2 { 3 unsigned long flags; 4 5 local_irq_save(flags); 6 raise_softirq_irqoff(nr); 7 local_irq_restore(flags); 8 }
核心函數在
1 inline void raise_softirq_irqoff(unsigned int nr) 2 { 3 __raise_softirq_irqoff(nr); 4 5 /* 6 * If we're in an interrupt or softirq, we're done 7 * (this also catches softirq-disabled code). We will 8 * actually run the softirq once we return from 9 * the irq or softirq. 10 * 11 * Otherwise we wake up ksoftirqd to make sure we 12 * schedule the softirq soon. 13 */ 14 /*若是咱們沒有在中斷上下文中(硬中斷或者軟中斷),就喚醒軟中斷守護進程,不然之能等到從中斷返回的過程當中*/ 15 if (!in_interrupt()) 16 wakeup_softirqd(); 17 }
1 void __raise_softirq_irqoff(unsigned int nr) 2 { 3 trace_softirq_raise(nr); 4 or_softirq_pending(1UL << nr); 5 }
1 #define or_softirq_pending(x) this_cpu_or(irq_stat.__softirq_pending, (x))
在raise_softirq_irqoff函數中看下,在設置了對應的位以後調用了in_interrupt函數判斷是否處於硬中斷上下文或者軟中斷上下文,若是不在就調用wakeup_softirqd喚醒守護進程處理軟中斷。不然的話等到中斷退出的時候處理。
三、軟中斷的處理
處理時機:
軟中斷大概在三個地方會被檢測是否存在,若是存在會進行處理:
中斷上下文:CPU處於處理中斷上半部或者下半部,內核用in_interrupt來判斷是否處於中斷上下文。這是一個宏:
#define in_interrupt() (irq_count())
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK))
能夠看到這裏中斷上下文包括硬件中斷、軟件中斷、NMI中斷。說到這裏,出現了一個preempt_count(),LInux爲每一個進程的thread_info結構中維護了一個preempt_count字段,該字段是int型,所以有32位,用於支持內核搶佔。當該字段爲0的時候,表示當前容許內核搶佔,不然不能夠。具體請參考另外一篇博文:Linux中的進程調度
處理過程:
軟中斷的處理核心都在do_softirq函數。
1 asmlinkage void do_softirq(void) 2 { 3 __u32 pending; 4 unsigned long flags; 5 6 if (in_interrupt()) 7 return; 8 /*關閉全部中斷 會保存eflags寄存器的內容*/ 9 local_irq_save(flags); 10 11 pending = local_softirq_pending(); 12 13 if (pending) 14 __do_softirq(); 15 /*開啓全部中斷,恢復eflagS寄存器的內容*/ 16 local_irq_restore(flags); 17 }
首先就會判斷當前是否處於中斷上下文,若是處於就直接返回,一個軟中斷既不能打斷硬件中斷也不能打斷軟件中斷。若是不在中斷上下文,就調用local_softirq_pending函數判斷是否存在被觸發的軟中斷,若是存在就進入if,調用__do_softirq函數, 不然開啓中斷,不作處理。
1 asmlinkage void __do_softirq(void) 2 { 3 struct softirq_action *h; 4 __u32 pending; 5 unsigned long end = jiffies + MAX_SOFTIRQ_TIME; 6 int cpu; 7 unsigned long old_flags = current->flags; 8 int max_restart = MAX_SOFTIRQ_RESTART; 9 10 /* 11 * Mask out PF_MEMALLOC s current task context is borrowed for the 12 * softirq. A softirq handled such as network RX might set PF_MEMALLOC 13 * again if the socket is related to swap 14 */ 15 current->flags &= ~PF_MEMALLOC; 16 17 pending = local_softirq_pending(); 18 account_irq_enter_time(current); 19 20 __local_bh_disable(_RET_IP_, SOFTIRQ_OFFSET); 21 lockdep_softirq_enter(); 22 23 cpu = smp_processor_id(); 24 restart: 25 /* Reset the pending bitmask before enabling irqs */ 26 set_softirq_pending(0); 27 28 local_irq_enable(); 29 30 h = softirq_vec; 31 32 do { 33 if (pending & 1) { 34 unsigned int vec_nr = h - softirq_vec; 35 int prev_count = preempt_count(); 36 37 kstat_incr_softirqs_this_cpu(vec_nr); 38 39 trace_softirq_entry(vec_nr); 40 h->action(h); 41 trace_softirq_exit(vec_nr); 42 if (unlikely(prev_count != preempt_count())) { 43 printk(KERN_ERR "huh, entered softirq %u %s %p" 44 "with preempt_count %08x," 45 " exited with %08x?\n", vec_nr, 46 softirq_to_name[vec_nr], h->action, 47 prev_count, preempt_count()); 48 preempt_count() = prev_count; 49 } 50 51 rcu_bh_qs(cpu); 52 } 53 h++; 54 pending >>= 1; 55 } while (pending); 56 57 local_irq_disable(); 58 59 pending = local_softirq_pending(); 60 if (pending) { 61 if (time_before(jiffies, end) && !need_resched() && 62 --max_restart) 63 goto restart; 64 65 wakeup_softirqd(); 66 } 67 68 lockdep_softirq_exit(); 69 70 account_irq_exit_time(current); 71 __local_bh_enable(SOFTIRQ_OFFSET); 72 tsk_restore_flags(current, old_flags, PF_MEMALLOC); 73 }
有了上面的鋪墊,這裏並不難理解。首先調用local_softirq_pending函數獲取當前CPU軟中斷位圖,而後調用__local_bh_disable函數禁止本地軟中斷,接着調用lockdep_softirq_enter函數標記進入softirq context。下面的restart段就開始處理位圖中的軟中斷了。
進入該節的首要操做對位圖清零,由於隨時可能有同種類型的軟中斷被觸發,接着就調用local_irq_enable函數開啓中斷。下面h = softirq_vec;是獲取軟中斷描述符表的起始地址,進入do循環,從pending的第一位開始處理,每次pending右移1位,同時h++,因此h定位具體的軟中斷類型,pending判斷是否被觸發。若是被觸發,那麼進入if內部,內部就是調用了h->action(h)函數處理軟中斷;
在循環結束後,就再次關中斷,而後從新讀取pending,若是又有新的軟中斷被觸發&&本次處理軟中斷未超時&&當前進進程的調度位TIF_NEED_RESCHED沒有被設置&&重啓次數沒到最大限制,就再次執行restart節進行處理。不然只能喚醒守護進程下次再處理軟中斷。
以後就標記退出softirq context,開啓軟中斷。
每一個CPU都會有一個軟中斷守護進程ksoftirqd,同時也有一個軟中斷位圖,咱們觸發的時候會指定CPU的id,各個CPU處理的軟中斷就不會影響,即便兩個CPU處理同一類型的軟中斷。這樣也避免了不少須要同步的操做,固然兩個CPU都在處理同一類型的軟中斷,那麼仍是須要必定的同步來保障臨界區的安全。若是在do_softirq的末尾有未處理的軟中斷,就不得不喚醒守護進程進行處理;一樣在raise_softirq_irqoff中在觸發指定軟中斷後,判斷是否在中斷上下文,若是不在中斷上下文就喚醒守護進程,不然下次檢查調度的時候處理這些軟中斷。
基本的處理過程就如上所述,可是仍是存在很多問題,前面代碼片斷中出現了不少開關中斷的操做,爲什麼須要有這些操做以及這些操做的原理如何?下面咱們分析一下。
開關中斷涉及到的函數主要有下面幾個:
其中1和3是針對hard irq,而2是針對soft irq。並且以上函數都是成對出現的。
local_irq_save和local_irq_restore是保存和恢復EFLAGS寄存器的狀態,首先執行local_irq_save會保存EFLAGS寄存器的狀態到一個變量,而後禁止本地中斷(可屏蔽的外部中斷),local_irq_restore會恢復EFLAGS寄存器到以前保存的狀態。
local_irq_disable會直接禁止本地中斷(可屏蔽的外部中斷),而local_irq_enable會打開本地中斷。這些都是針對可屏蔽外部中斷,對於NMI和異常沒有做用。
local_bh_disable會設置當前進程的搶佔計數器,即增長對應的位,這樣,當前進程就標識爲不可搶佔,也就關閉了軟件中斷。爲何說這裏關閉了軟件中斷呢?由於前面咱們設置了搶佔計數器,而在每次檢查準備調度時候,都會判斷當前是否處於中斷上下文,若是處於就不發生調度,從而不搶佔當前進程。
總結:說實話,內核真是複雜的很,在寫本篇博客的時候天然會參考一些書籍以及大牛們的博客,發現本身的確要學的東西太多,別人寫書或者寫博客,都能很天然的結合其餘的模塊,旁徵博引,而本身雖然已經盡最大可能描述清楚,但仍是以爲不夠豐滿,只能之後慢慢學習了,同時其中可能難免有錯誤的地方,還請老師們指點!!
參考資料:
一、LInux 3.11.1內核源碼
二、http://www.wowotech.net/linux_kenrel/soft-irq.html
三、linux 內核源代碼情景分析