1. 程式人生 > >Linux中斷(interrupt)子系統之三:中斷流控處理層

Linux中斷(interrupt)子系統之三:中斷流控處理層

1.  中斷流控層簡介

早期的核心版本中,幾乎所有的中斷都是由__do_IRQ函式進行處理,但是,因為各種中斷請求的電氣特性會有所不同,又或者中斷控制器的特性也不同,這會導致以下這些處理也會有所不同:

 

  • 何時對中斷控制器發出ack迴應;
  • mask_irq和unmask_irq的處理;
  • 中斷控制器是否需要eoi迴應?
  • 何時開啟cpu的本地irq中斷?以便允許irq的巢狀;
  • 中斷資料結構的同步和保護;

/*****************************************************************************************************/
宣告:本博內容均由http://blog.csdn.net/droidphone原創,轉載請註明出處,謝謝!
/*****************************************************************************************************/
為此,通用中斷子系統把幾種常用的流控型別進行了抽象,併為它們實現了相應的標準函式,我們只要選擇相應的函式,賦值給irq所對應的irq_desc結構的handle_irq欄位中即可。這些標準的回撥函式都是irq_flow_handler_t型別:

 

 

 
  1. typedef void (*irq_flow_handler_t)(unsigned int irq,

  2. struct irq_desc *desc);

目前的通用中斷子系統實現了以下這些標準流控回撥函式,這些函式都定義在:kernel/irq/chip.c中,

 

 

  • handle_simple_irq  用於簡易流控處理;
  • handle_level_irq  用於電平觸發中斷的流控處理;
  • handle_edge_irq  用於邊沿觸發中斷的流控處理;
  •  handle_fasteoi_irq  用於需要響應eoi的中斷控制器;
  • handle_percpu_irq  用於只在單一cpu響應的中斷;
  • handle_nested_irq  用於處理使用執行緒的巢狀中斷;

驅動程式和板級程式碼可以通過以下幾個API設定irq的流控函式:

 

  • irq_set_handler();
  • irq_set_chip_and_handler();
  • irq_set_chip_and_handler_name();

 

以下這個序列圖展示了整個通用中斷子系統的中斷響應過程,flow_handle一欄就是中斷流控層的生命週期:

                                                                                           圖1.1  通用中斷子系統的中斷響應過程

2.  handle_simple_irq

該函式沒有實現任何實質性的流控操作,在把irq_desc結構鎖住後,直接呼叫handle_irq_event處理irq_desc中的action連結串列,它通常用於多路複用(類似於中斷控制器級聯)中的子中斷,由父中斷的流控回撥中呼叫。或者用於無需進行硬體控制的中斷中。以下是它的經過簡化的程式碼:

 

 
  1. void

  2. handle_simple_irq(unsigned int irq, struct irq_desc *desc)

  3. {

  4. raw_spin_lock(&desc->lock);

  5. ......

  6. handle_irq_event(desc);

  7.  
  8. out_unlock:

  9. raw_spin_unlock(&desc->lock);

  10. }

 

3.  handle_level_irq

該函式用於處理電平中斷的流控操作。電平中斷的特點是,只要裝置的中斷請求引腳(中斷線)保持在預設的觸發電平,中斷就會一直被請求,所以,為了避免同一中斷被重複響應,必須在處理中斷前先把mask irq,然後ack irq,以便復位裝置的中斷請求引腳,響應完成後再unmask irq。實際的情況稍稍複雜一點,在mask和ack之後,還要判斷IRQ_INPROGRESS標誌位,如果該標誌已經置位,則直接退出,不再做實質性的處理,IRQ_INPROGRESS標誌在handle_irq_event的開始設定,在handle_irq_event結束時清除,如果監測到IRQ_INPROGRESS被置位,表明該irq正在被另一個CPU處理中,所以直接退出,對電平中斷來說是正確的處理方法。但是我覺得在ARM系統中,這種情況根本就不會發生,因為在沒有進入handle_level_irq之前,中斷控制器沒有收到ack通知,它不會向第二個CPU再次發出中斷請求,而當程式進入handle_level_irq之後,第一個動作就是mask irq,然後ack irq(通常是聯合起來的:mask_ack_irq),這時候就算裝置再次發出中斷請求,也是在handle_irq_event結束,unmask irq之後,這時IRQ_INPROGRESS標誌已經被清除。我不知道其他像X86之類的體系是否有不同的行為,有知道的朋友請告知我一下。以下是handle_level_irq經過簡化之後的程式碼:

 
  1. void

  2. handle_level_irq(unsigned int irq, struct irq_desc *desc)

  3. {

  4. raw_spin_lock(&desc->lock);

  5. mask_ack_irq(desc);

  6.  
  7. if (unlikely(irqd_irq_inprogress(&desc->irq_data)))

  8. goto out_unlock;

  9. ......

  10.  
  11. if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data)))

  12. goto out_unlock;

  13.  
  14. handle_irq_event(desc);

  15.  
  16. if (!irqd_irq_disabled(&desc->irq_data) && !(desc->istate & IRQS_ONESHOT))

  17. unmask_irq(desc);

  18. out_unlock:

  19. raw_spin_unlock(&desc->lock);

  20. }

雖然handle_level_irq對電平中斷的流控進行了必要的處理,因為電平中斷的特性:只要沒有ack irq,中斷線會一直有效,所以我們不會錯過某次中斷請求,但是驅動程式的開發人員如果對該過程理解不透徹,特別容易發生某次中斷被多次處理的情況。特別是使用了中斷執行緒(action->thread_fn)來響應中斷的時候:通常mask_ack_irq只會清除中斷控制器的pending狀態,很多慢速裝置(例如通過i2c或spi控制的裝置)需要在中斷執行緒中清除中斷線的pending狀態,但是未等到中斷執行緒被排程執行的時候,handle_level_irq早就返回了,這時已經執行過unmask_irq,裝置的中斷線pending處於有效狀態,中斷控制器會再次發出中斷請求,結果是裝置的一次中斷請求,產生了兩次中斷響應。要避免這種情況,最好的辦法就是不要單獨使用中斷執行緒處理中斷,而是要實現request_threaded_irq()的第二個引數irq_handler_t:handler,在handle回撥中使用disable_irq()關閉該irq,然後在退出中斷執行緒回撥前再enable_irq()。假設action->handler沒有遮蔽irq,以下這幅圖展示了電平中斷期間IRQ_PROGRESS標誌、本地中斷狀態和觸發其他CPU的狀態:

                                          圖3.1  電平觸發中斷狀態

上圖中顏色分別代表不同的狀態:

狀態 紅色 綠色
IRQ_PROGRESS            TRUE        FALSE
是否允許本地cpu中斷             禁止                 允許  
是否允許該裝置再次觸發中斷(可能由其它cpu響應)             禁止           允許

4.  handle_edge_irq

該函式用於處理邊沿觸發中斷的流控操作。邊沿觸發中斷的特點是,只有裝置的中斷請求引腳(中斷線)的電平發生跳變時(由高變低或者有低變高),才會發出中斷請求,因為跳變是一瞬間,而且不會像電平中斷能保持住電平,所以處理不當就特別容易漏掉一次中斷請求,為了避免這種情況,遮蔽中斷的時間必須越短越好。核心的開發者們顯然意識到這一點,在正是處理中斷前,判斷IRQ_PROGRESS標誌沒有被設定的情況下,只是ack irq,並沒有mask irq,以便復位裝置的中斷請求引腳,在這之後的中斷處理期間,另外的cpu可以再次響應同一個irq請求,如果IRQ_PROGRESS已經置位,表明另一個CPU正在處理該irq的上一次請求,這種情況下,他只是簡單地設定IRQS_PENDING標誌,然後mask_ack_irq後退出,中斷請求交由原來的CPU繼續處理。因為是mask_ack_irq,所以系統實際上只允許掛起一次中斷。

 
  1. if (unlikely(irqd_irq_disabled(&desc->irq_data) ||

  2. irqd_irq_inprogress(&desc->irq_data) || !desc->action)) {

  3. if (!irq_check_poll(desc)) {

  4. desc->istate |= IRQS_PENDING;

  5. mask_ack_irq(desc);

  6. goto out_unlock;

  7. }

  8. }

  9.  
  10. desc->irq_data.chip->irq_ack(&desc->irq_data);

從上面的分析可以知道,處理中斷期間,另一次請求可能由另一個cpu響應後掛起,所以在處理完本次請求後還要判斷IRQS_PENDING標誌,如果被置位,當前cpu要接著處理被另一個cpu“委託”的請求。核心在這裡設定了一個迴圈來處理這種情況,直到IRQS_PENDING標誌無效為止,而且因為另一個cpu在響應並掛起irq時,會mask irq,所以在迴圈中要再次unmask irq,以便另一個cpu可以再次響應並掛起irq:

 
  1. do {

  2. ......

  3. if (unlikely(desc->istate & IRQS_PENDING)) {

  4. if (!irqd_irq_disabled(&desc->irq_data) &&

  5. irqd_irq_masked(&desc->irq_data))

  6. unmask_irq(desc);

  7. }

  8.  
  9. handle_irq_event(desc);

  10.  
  11. } while ((desc->istate & IRQS_PENDING) &&

  12. !irqd_irq_disabled(&desc->irq_data));

IRQS_PENDING標誌會在handle_irq_event中清除。

                                 圖4.1   邊沿觸發中斷狀態

上圖中顏色分別代表不同的狀態:

狀態         紅色         綠色
IRQ_PROGRESS         TRUE         FALSE
是否允許本地cpu中斷         禁止         允許
是否允許該裝置再次觸發中斷(可能由其它cpu響應)         禁止         允許
是否處於中斷上下文     處於中斷上下文     處於程序上下文


由圖4.1也可以看出,在處理軟體中斷(softirq)期間,此時仍然處於中斷上下文中,但是cpu的本地中斷是處於開啟狀態的,這表明此時巢狀中斷允許發生,不過這不要緊,因為重要的處理已經完成,被巢狀的也只是軟體中斷部分而已。這個也就是核心區分top和bottom兩個部分的初衷吧。

5.  handle_fasteoi_irq

現代的中斷控制器通常會在硬體上實現了中斷流控功能,例如ARM體系中的GIC通用中斷控制器。對於這種中斷控制器,CPU只需要在每次處理完中斷後發出一個end of interrupt(eoi),我們無需關注何時mask,何時unmask。不過雖然想著很完美,事情總有特殊的時候,所以核心還是給了我們插手的機會,它利用irq_desc結構中的preflow_handler欄位,在正式處理中斷前會通過preflow_handler函式呼叫該回調。

 
  1. void

  2. handle_fasteoi_irq(unsigned int irq, struct irq_desc *desc)

  3. {

  4. raw_spin_lock(&desc->lock);

  5.  
  6. if (unlikely(irqd_irq_inprogress(&desc->irq_data)))

  7. if (!irq_check_poll(desc))

  8. goto out;

  9. ......

  10. if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {

  11. desc->istate |= IRQS_PENDING;

  12. mask_irq(desc);

  13. goto out;

  14. }

  15.  
  16. if (desc->istate & IRQS_ONESHOT)

  17. mask_irq(desc);

  18.  
  19. preflow_handler(desc);

  20. handle_irq_event(desc);

  21.  
  22. out_eoi:

  23. desc->irq_data.chip->irq_eoi(&desc->irq_data);

  24. out_unlock:

  25. raw_spin_unlock(&desc->lock);

  26. return;

  27. ......

  28. }

此外,核心還提供了另外一個eoi版的函式:handle_edge_eoi_irq,它的處理類似於handle_edge_irq,只是無需實現mask和unmask的邏輯。

6.  handle_percpu_irq

該函式用於smp系統,當某個irq只在一個cpu上處理時,我們可以無需用自旋鎖對資料進行保護,也無需處理cpu之間的中斷巢狀重入,所以函式很簡單:

 
  1. void

  2. handle_percpu_irq(unsigned int irq, struct irq_desc *desc)

  3. {

  4. struct irq_chip *chip = irq_desc_get_chip(desc);

  5.  
  6. kstat_incr_irqs_this_cpu(irq, desc);

  7.  
  8. if (chip->irq_ack)

  9. chip->irq_ack(&desc->irq_data);

  10.  
  11. handle_irq_event_percpu(desc, desc->action);

  12.  
  13. if (chip->irq_eoi)

  14. chip->irq_eoi(&desc->irq_data);

  15. }

 

7.  handle_nested_irq

該函式用於實現其中一種中斷共享機制,當多箇中斷共享某一根中斷線時,我們可以把這個中斷線作為父中斷,共享該中斷的各個裝置作為子中斷,在父中斷的中斷執行緒中決定和分發響應哪個裝置的請求,在得出真正發出請求的子裝置後,呼叫handle_nested_irq來響應中斷。所以,該函式是在程序上下文執行的,我們也無需掃描和執行irq_desc結構中的action連結串列。父中斷在初始化時必須通過irq_set_nested_thread函式明確告知中斷子系統:這些子中斷屬於執行緒巢狀中斷型別,這樣驅動程式在申請這些子中斷時,核心不會為它們建立自己的中斷執行緒,所有的子中斷共享父中斷的中斷執行緒。

 

 
  1. void handle_nested_irq(unsigned int irq)

  2. {

  3. ......

  4. might_sleep();

  5.  
  6. raw_spin_lock_irq(&desc->lock);

  7. ......

  8. action = desc->action;

  9. if (unlikely(!action || irqd_irq_disabled(&desc->irq_data)))

  10. goto out_unlock;

  11.  
  12. irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);

  13. raw_spin_unlock_irq(&desc->lock);

  14.  
  15. action_ret = action->thread_fn(action->irq, action->dev_id);

  16.  
  17. raw_spin_lock_irq(&desc->lock);

  18. irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);

  19.  
  20. out_unlock:

  21. raw_spin_unlock_irq(&desc->lock);

  22. }