詳解操作系統中斷


2016-11-02

中斷這個特性相比大家都不會陌生,稍微懂點操作系統知識的人都可以說到一二。但是要真正把中斷描述清楚,以及LInux中和windows中的實現方式,這可能還是有點難度的。今天筆者就想徹頭徹尾的把中斷給詳細分析下。


 

說到中斷還不得不從現代操作系統的特性說起,無論是桌面PC操作系統還是嵌入式都是多任務的操作系統,而很遺憾,處理器往往是單個的,即使在硬件成本逐漸下降,從而硬件配置直線上升的今天,PC機的核心可能已經達到4核心,8核心,而手機移動設備更不可思議的達到16核心,32核心,處理器的數量依然不可能做到每個任務一個CPU,所以CPU必須作為一種全局的資源讓所有任務共享。說到共享,如何共享呢?什么時候給任務A用,什么時候給任務B用......這就是進程調度,具體的安排就由調度算法決定了。進程如何去調度?現代操作系統一般都是采用基於時間片的優先級調度算法,把CPU的時間划分為很細粒度的時間片,一個任務每次只能時間這么多的時間,時間到了就必須交出使用權,即換其他的任務使用。這種要看操作系統的定時器機制了。那么時間片到之后,系統做了什么呢?這就要用到我們的中斷了,時間片到了由定時器觸發一個軟中斷,然后進入相應的處理歷程。當然這一點不足以表明中斷的重要,計算機操作系統自然離不開外部設備:鼠標、鍵盤、網卡、磁盤等等。就拿網卡來講,我計算機並不知道時候數據包會來到,我能保證的就是數據來了我能正常接收就行了。但是我又不可能一直等着接收數據包,要是這樣其他任務就死完了。所以合理的辦法是,你數據包來到之后,通知我,然后我再對你處理,怎么通知呢??答:中斷!鍵盤、鼠標亦是如此!

 

好了閑話說了這么多,進入正題吧!

如上面所述,中斷信號由外部設備發起,准確來說是由外部設備的控制器發起,因為外部設備本身並不能發起信號。必須網卡設備,的那個網絡數據包到達網卡,網卡的控制器就向IO APIC發送中斷信號,IO APIC把信號發送給本地APIC,本地APIC把信號傳送給CPU,如果根據當時情況,要處理這個中斷,就保存當時的運行上下文,切換到中斷上下文中,根據IDT查找對應的處理函數進行處理。處理完成后,需要恢復中斷之前的狀態。

大致過程就如上面所述,但是具體這個過程是怎么執行的,關鍵幾點如下:

1、設備控制器如何發送中斷信號

2、APIC如何接受中斷信號,以及做了什么處理

3、處理器收到中斷信號又做了什么操作

 在此之前,我們需要介紹下中斷控制器8259A和APIC


 8259A中斷控制器由兩片8259A芯片級聯組成,每個芯片有8個中斷輸入引腳,其中IRQ2被用來連接從芯片,所以一共可以支持15個中斷號,這也就是早期采用8259A中斷控制器只能使用15個外部中斷的原因,使用8259A中斷控制器的工作架構如下:

每個外部設備連接一條中斷線,當設備需要中斷CPU時,通過這些中斷線,發送中斷請求。中斷控制器感知到這些中斷請求,會設置中斷控制器中的中斷請求寄存器的相應位為1,鑒於多個中斷可能並發到達,中斷控制器具備中斷判優功能,當其選定一個中斷作為當前響應中斷時,會清除中斷請求寄存器中的對應位,然后設置中斷服務寄存器的某些位為1,表明CPU正在服務於某個中斷請求。

 另外8259A還有一個8位的中斷屏蔽寄存器,每一位對應於一個中斷線,當對應的位被設置后,表明要屏蔽這些中斷。為了處理不同優先級的中斷,中斷控制器還有同一個優先權判決器,當一個中斷到達時,判斷到達的中斷優先級和ISR中正在服務的中斷優先級的大小,若高於正在服務的中斷的優先級,需要打斷當前中斷的處理,轉而處理新到達的中斷請求,否則,不予理會。

 中斷觸發方式:

中斷請求輸入端IR0~IR7可采用的中斷觸發方式有電平觸發和邊沿觸發兩種,由初始化命令字ICW1中的LTIM位來設定。

  • 當LTIM設置為1時,為電平觸發方式。i8259A檢測到IRi(i=0~7)端有高電平時產生中斷。如采用這種觸發方式,中斷請求信號在被響應后應及時撤除,否則可能引起不該有的第二次中斷。
  •  當LTIM設置為0時,為邊沿觸發方式。8259A檢測到IRi端有由低到高的跳變信號時產生中斷

當外部設備請求服務時,設置自己對應寄存器的位為1,即成了高電平,那么中斷控制器端就可以接受到中斷信號,進入中斷的處理。

由於8259A中斷控制器只能應用與單處理器,且其中斷源的限制,后來Intel開發了高級可編程中斷控制器APIC

APIC由兩部分:本地APIC和IO APIC。本地APIC和邏輯CPU綁定,它控制傳遞給邏輯處理器中斷信號和產生IPI中斷(這是處理器間中斷,只用於多處理器情況)、

本地APIC可以接受一下中斷源:

  • 本地連接的IO設備,指直接鏈接在處理器中斷管腳LINT0和LINT1上。
  • 外部鏈接的IO設備,這些中斷源由外部IO設備產生比如鍵盤、鼠標等。外部IO設備連接IO APIC,IO APIC 把中斷信號發送給一個或者多個CPU(本地APIC)。
  • 處理器間中斷IPI,一個處理器可以通過發送IPI,中斷其他的處理器或者處理器組。
  • APIC定時器,用於定時向其綁定的處理器發送中斷信號
  • APIC內部錯誤,當本地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寄存器獲取中斷描述符表基地址,然后根據中斷號定位具體的中斷描述符。這里中斷描述符可分為兩種情況:

  • 中斷門和陷阱門
  • 任務門

1、 當中斷描述符對應的是中斷門或者陷阱門時,處理歷程運行在當前進程的上下文中,即不需要發生進程上下文的切換,只是如果處理歷程和當前進程的運行級別不同,則需要發生棧的切換,具體如下:

如果當前進程運行在level 3即用戶態,則當中斷發生時:

  1. CPU從TSS中得到新棧的段描述符和棧指針
  2. 把段描述符和棧指針壓棧
  3. 然后依次把EFLAGS、CS、EIP壓棧
  4. 如果是一個異常引起一個錯誤碼,則把錯誤碼在EIP之后壓棧

如果中斷發生時當前進程運行在內核態,則就不需要發生棧的切換,僅僅需要執行上述的后兩步。

具體動作參考下圖:

 

2、當中斷描述符對應一個任務門時,意味着此次中斷的處理由一個單獨的程序執行,和當前進程無關。使用新的任務處理中斷的優缺點也很明顯:

  • 被中斷進程的上下文會自動保存。
  • 新的任務會使用一個新的內核棧,這就避免了如果中斷是由棧錯誤引起的,使用中斷門或者陷阱門帶來的system crash。
  • 處理程序運行在自己的地址空間中,和其他的程序隔離比較好。

當然缺點也很明顯,每次中斷都會進行任務的切換,進程上下文的切換所帶來的開銷要比上面兩種方式大的多,並且每次中斷都要進行兩次進程切換:中斷進入和中斷返回。造成中斷響應延遲過大

由於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

1、軟中斷的注冊

軟中斷的核心機制是一張表,類似於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便是對應軟中斷的處理函數。

2、軟中斷的觸發

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喚醒守護進程處理軟中斷。否則的話等到中斷退出的時候處理。

3、軟中斷的處理                                           

處理時機:

軟中斷大概在三個地方會被檢測是否存在,如果存在會進行處理:

  • 從一個硬件中斷返回時
  • 在ksoftirqd內核線程中
  • 在那些顯式檢查和執行待處理的軟中斷的代碼中

中斷上下文: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. local_irq_savelocal_irq_restore
  2. local_bh_disablelocal_bh_enable
  3. local_irq_enablelocal_irq_disable

其中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會設置當前進程的搶占計數器,即增加對應的位,這樣,當前進程就標識為不可搶占,也就關閉了軟件中斷。為什么說這里關閉了軟件中斷呢?因為前面我們設置了搶占計數器,而在每次檢查准備調度時候,都會判斷當前是否處於中斷上下文,如果處於就不發生調度,從而不搶占當前進程。

 

總結:說實話,內核真是復雜的很,在寫本篇博客的時候自然會參考一些書籍以及大牛們的博客,發現自己的確要學的東西太多,別人寫書或者寫博客,都能很自然的結合其他的模塊,旁征博引,而自己雖然已經盡最大可能描述清楚,但還是覺得不夠豐滿,只能以后慢慢學習了,同時其中可能不免有錯誤的地方,還請老師們指點!! 

 

參考資料:

1、LInux 3.11.1內核源碼

2、http://www.wowotech.net/linux_kenrel/soft-irq.html

3、linux 內核源代碼情景分析


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM