Linux中斷管理機制

轉自  https://www.cnblogs.com/arnoldlu/p/8659981.html

新的linux kernel 及 arm不支持 中斷嵌套。 

 

 

關鍵詞:GIC、IAR、EOI、SGI/PPI/SPI、中斷映射、中斷異常向量、中斷上下文、內核中斷線程、中斷註冊。

 

由於篇幅較大,簡單梳理一下內容。

本章主要可以分爲三大部分:

講解硬件背景的1. ARM中斷控制器

系統初始化的靜態過程:GIC初始化和各中斷的中斷號映射2. 硬件中斷號和Linux中斷號的映射;每個中斷的註冊5. 註冊中斷

一箇中斷從產生到執行完畢的動態過程:ARM底層通用部分如何處理3. ARM底層中斷處理;GIC部分的處理流程以及上層通用處理部分4. 高層中斷處理

這裏的高層處理,沒有包括下半部。下半部在Linux中斷管理 (2)軟中斷和taskletLinux中斷管理 (3)workqueue工作隊列中進行介紹。

1. ARM中斷控制器

 

1.1 ARM支持中斷類型

ARM GIC-v2支持三種類型的中斷:

SGI:軟件觸發中斷(Software Generated Interrupt),通常用於多核間通訊,最多支持16個SGI中斷,硬件中斷號從ID0~ID15。SGI通常在Linux內核中被用作IPI中斷(inter-processor interrupts),並會送達到系統指定的CPU上。

PPI:私有外設中斷(Private Peripheral Interrupt),是每個CPU私有的中斷。最多支持16個PPI中斷,硬件中斷號從ID16~ID31。PPI通常會送達到指定的CPU上,應用場景有CPU本地時鐘。

SPI:公用外設中斷(Shared Peripheral Interrupt),最多可以支持988個外設中斷,硬件中斷號從ID32~ID1019。

 

1.2 GIC檢測中斷流程

GIC主要由兩部分組成,分別是仲裁單元(Distributor)和CPU接口模塊。

GIC仲裁單元爲每一箇中斷維護一個狀態機,分別是:inactive、pending、active and pending、active。

下面是來自IHI0048B GIC-V2規格書3.2.4 Interrupt handling state machine截圖:

GIC檢測中斷流程如下:

(1) 當GIC檢測到一箇中斷髮生時,會將該中斷標記爲pending狀態(A1)。

(2) 對處於pending狀態的中斷,仲裁單元回確定目標CPU,將中斷請求發送到這個CPU上。

(3) 對於每個CPU,仲裁單元會從衆多pending狀態的中斷中選擇一個優先級最高的中斷,發送到目標CPU的CPU Interface模塊上。

(4) CPU Interface會決定這個中斷是否可以發送給CPU。如果該終端優先級滿足要求,GIC會發生一箇中斷信號給該CPU。

(5) 當一個CPU進入中斷異常後,會去讀取GICC_IAR寄存器來響應該中斷(一般是Linux內核的中斷處理程序來讀寄存器)。寄存器會返回硬件中斷號(hardware interrupt ID),對於SGI中斷來說是返回源CPU的ID。

     當GIC感知到軟件讀取了該寄存器後,又分爲如下情況:

     * 如果該中斷源是pending狀態,那麼轉改將變成active。(C)    

     * 如果該中斷又重新產生,那麼pending狀態變成active and pending。(D)

     * 如果該中斷是active狀態,現在變成active and pending。(A2)

(6) 當處理器完成中斷服務,必鬚髮送一個完成信號EOI(End Of Interrupt)給GIC控制器。軟件寫GICC_EOIR寄存器,狀態變成inactive。(E1)

補充:

(7) 對於level triggered類型中斷來說,當觸發電平消失,狀態從active and pending變成active。(B2)

常用路徑是A1->D->B2->E1。

1.2.1 GIC中斷搶佔

GIC中斷控制器支持中斷優先級搶佔,一個高優先級中斷可以搶佔一個低優先級且處於active狀態的中斷,即GIC仲裁單元會記錄和比較當前優先級最高的pending狀態,然後去搶佔當前中斷,並且發送這個最高優先級的中斷請求給CPU,CPU應答了高優先級中斷,暫停低優先級中斷服務,進而去處理高優先級中斷。

GIC會將pending狀態優先級最高的中斷請求發送給CPU。

1.2.2 Linux對中斷搶佔處理

從GIC角度看,GIC會發送高優先級中斷請求給CPU。

但是目前CPU處於關中斷狀態,需要等低優先級中斷處理完畢,直到發送EOI給GIC。

然後CPU纔會響應pending狀態中優先級最高的中斷進行處理。

所以Linux下:

1. 高優先級中斷無法搶佔正在執行的低優先級中斷。

2.同處於pending狀態的中斷,優先響應高優先級中斷進行處理。

1.3 GIC中斷時序

 

藉助GIC-400 Figure B-2 Signaling physical interrupts理解GIC內部工作原理。

M和N都是SPI類型的外設中斷,且通過FIQ來處理,高電平觸發,N的優先級比M高,他們的目標CPU相同。

(1) T1時刻:GIC的總裁單元檢測到中斷M的電平變化。

(2) T2時刻:仲裁單元設置中斷M的狀態爲pending。

(3) T17時刻:CPU Interface模塊會拉低nFIQCPU[n]信號。在中斷M的狀態變成pending後,大概需要15個時鐘週期後會拉低nFIQCPU[n]信號來向CPU報告中斷請求(assertion)。仲裁單元需要這些時間來計算哪個是pending狀態下優先級最高的中斷。

(4) T42時刻:仲裁單元檢測到另外一個優先級更高的中斷N。

(5) T43時刻:仲裁單元用中斷N替換中斷M爲當前pending狀態下優先級最高的中斷,並設置中斷N爲pending狀態。

(6) T58時刻:經過tph個時鐘後,CPU Interface拉低你FIOCPU[n]信號來通知CPU。因爲此信號在T17時刻已經被拉低,CPU Interface模塊會更新GICC_IAR寄存器的Interrupt ID域,該域的值變成中斷N的硬件中斷號。

(7) T61~T131時刻:Linux對中斷N的服務程序--------------------------------------------------------------中斷服務程序處理段,從GICC_IAR開始到GICC_EOIR結束。

  T61時刻:CPU(Linux中斷服務例程)讀取GICC_IAR寄存器,即軟件響應了中斷N。這時仲裁單元把中斷N的狀態從pending變成active and pending。讀取GICC_IAR

  T64時刻:在中斷N被Linux相應3個時鐘內,CPU Interface模塊完成對nFIQCPU[n]信號的deasserts,即拉高nFIQCPU[n]信號。

  T126時刻:外設也deassert了該中斷N。

  T128時刻:仲裁單元移出了中斷N的pending狀態。

  T131時刻:Linux服務程序把中斷N的硬件ID號寫入GICC_EOIR寄存器來完成中斷N的全部處理過程。寫GICC_EOIR

(8) T146時刻:在向GICC_EOIR寄存器寫入中斷N中斷號後的tph個時鐘後,仲裁單元會選擇下一個最高優先級中斷,即中斷M,發送中斷請求給CPU Interface模塊。CPU Interface會拉低nFIQCPU[n]信號來向CPU報告外設M的中斷請求。

(9) T211時刻:Linux中斷服務程序讀取GICC_IAR寄存器來響應中斷,仲裁單元設置中斷M的狀態爲active and pending。

(10) T214時刻:在CPU響應中斷後的3個時鐘內,CPU Interface模塊拉高nFIOCPU[n]信號來完成deassert動作。

 

 

 

 

 

 

6. 一箇中斷的生命

經過上面的分析可以看出一箇中斷從產生、執行,到最終結束的流程。這裏我們用樹形代碼路徑來簡要分析一下一個中斷的生命週期。

vector_irq()->vector_irq()->__irq_svc()

  ->svc_entry()--------------------------------------------------------------------------保護中斷現場

  ->irq_handler()->gic_handle_irq()------------------------------------------------具體到GIC中斷控制器對應的就是gic_handle_irq(),此處從架構相關進入了GIC相關處理。

    ->GIC_CPU_INTACK--------------------------------------------------------------讀取IAR寄存器,響應中斷。

    ->handle_domain_irq()

      ->irq_enter()------------------------------------------------------------------------進入硬中斷上下文

      ->generic_handle_irq()

        ->generic_handle_irq_desc()->handle_fasteoi_irq()--------------------根據中斷號分辨不同類型的中斷,對應不同處理函數,這裏中斷號取大於等於32。

          ->handle_irq_event()->handle_irq_event_percpu()

            ->action->handler()-----------------------------------------------------------對應到特定中斷的處理函數,即上半部

              ->__irq_wake_thread()-----------------------------------------------------如果中斷函數處理返回IRQ_WAKE_THREAD,則喚醒中斷線程進行處理,但不是立即執行中斷線程。

      ->irq_exit()---------------------------------------------------------------------------退出硬中斷上下文。視情況處理軟中斷。

        ->invoke_softirq()-----------------------------------------------------------------處理軟中斷,超出一定條件任務就會交給軟中斷線程處理。

    ->GIC_CPU_EOI--------------------------------------------------------------------寫EOI寄存器,表示結束中斷。至此GIC纔會接收新的硬件中斷,此前一直是屏蔽硬件中斷的。

  ->svc_exit-------------------------------------------------------------------------------恢復中斷現場

 從上面的分析可以看出:

  • 中斷上半部的處理是關硬件中斷的,這裏的關硬件中斷是GIC就不接收中斷處理。直到寫EOI之後,GIC仲裁單元纔會重新選擇中斷進行處理。
  • 軟中斷運行於軟中斷上下文中,但是仍然是關硬件中斷的,這裏需要特別注意,軟中斷需要快速處理並且不能睡眠。
  • 不是所有軟中斷都運行於軟中斷上下文中,部分軟中斷任務可能會交給ksoftirqd線程處理。
  • 包括IRQ_WAKE_THREAD、ksoftirqd、woker等喚醒線程的情況,都不會在中斷上下文中進行處理。中斷上下文中所做的處理只是喚醒,執行時機交給系統調度。
  • 如果要提高Linux實時性,有兩個要點:一是將上半部線程化;另一個是將軟中斷都交給ksoftirqd線程處理

 

 

 

 

=================================================================================================

中斷棧

內核棧


#define MIN_THREAD_SHIFT    (14 + KASAN_THREAD_SHIFT)
#define THREAD_SIZE     (UL(1) << THREAD_SHIFT)

union thread_union {
#ifndef CONFIG_THREAD_INFO_IN_TASK
    struct thread_info thread_info;
#endif
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

注意這裏有一個宏來區分stack的實現方式 CONFIG_THREAD_INFO_IN_TASK ,如果定義了該宏,實際上會通過動態申請物理頁來作爲stack,並且把stack放到 task_struct 結構體中,用 task_struct->stack 來表示。
中斷棧

    對於x86平臺:

x86平臺上,中斷棧是獨立於內核棧的存在,兩者並不共享,如果是多處理器架構,那麼每個CPU都對應有一箇中斷棧,本文不對x86做介紹。

    對於 ARM平臺:

中斷棧和內核棧則是共享的,中斷棧和內核棧共享有一個負面因素,如果中斷髮生嵌套,可能會造成棧溢出,從而可能會破壞到內核棧的一些重要數據。

    對於ARM64平臺:

中斷棧的實現是獨立的,並且區分兩種情況,分別是vmap申請內存,還是直接靜態定義,並且根據CPU個數,每個CPU對應單獨的一個stack。


arch/arm64/kernel/irq.c:

#ifdef CONFIG_VMAP_STACK

static void init_irq_stacks(void)

{

    int cpu;

    unsigned long *p;

 

    for_each_possible_cpu(cpu) {

        /*

        * To ensure that VMAP'd stack overflow detection works

        * correctly, the IRQ stacks need to have the same

        * alignment as other stacks.

        */

        p = __vmalloc_node_range(IRQ_STACK_SIZE, THREAD_ALIGN,

                     VMALLOC_START, VMALLOC_END,

                     THREADINFO_GFP, PAGE_KERNEL,

                     0, cpu_to_node(cpu),

                     __builtin_return_address(0));

 

        per_cpu(irq_stack_ptr, cpu) = p;

    }

}

#else

/* irq stack only needs to be 16 byte aligned - not IRQ_STACK_SIZE aligned. */

DEFINE_PER_CPU_ALIGNED(unsigned long [IRQ_STACK_SIZE/sizeof(long)], irq_stack);

 

static void init_irq_stacks(void)

{

    int cpu;

 

    for_each_possible_cpu(cpu)

        per_cpu(irq_stack_ptr, cpu) = per_cpu(irq_stack, cpu);

}

#endif

 

 

 

IRQF_ONESHOT one shot本身的意思的只有一次的,結合到中斷這個場景,則表示中斷是一次性觸發的,不能嵌套。對於primary handler,當然是不會嵌套,但是對於threaded interrupt handler,我們有兩種選擇,一種是mask該interrupt source,另外一種是unmask該interrupt source。一旦mask住該interrupt source,那麼該interrupt source的中斷在整個threaded interrupt handler處理過程中都是不會再次觸發的,也就是one shot了。這種handler不需要考慮重入問題。
具體是否要設定one shot的flag是和硬件系統有關的,我們舉一個例子,比如電池驅動,電池裏面有一個電量計,是使用HDQ協議進行通信的,電池驅動會註冊一個threaded interrupt handler,在這個handler中,會通過HDQ協議和電量計進行通信。對於這個handler,通過HDQ進行通信是需要一個完整的HDQ交互過程,如果中間被打斷,整個通信過程會出問題,因此,這個handler就必須是one shot的。
IRQF_NO_SUSPEND 這個flag比較好理解,就是說在系統suspend的時候,不用disable這個中斷,如果disable,可能會導致系統不能正常的resume。

 

 

內核裏中斷嵌套:

https://blog.csdn.net/juS3Ve/article/details/81437432

問:Linux的中斷可以嵌套嗎?

答:以前是可以嵌套的,現在不可以!    

個人補充: 以前可以嵌套也僅限於fiq 可以打斷irq。

歷史

早前的Linux內核版本,中斷分爲兩種:

  1. 快中斷,申請的時候帶IRQF_DISABLED標記,在IRQ HANDLER裏面不允許新的中斷進來;

  2. 慢中斷,申請的時候不帶IRQF_DISABLED標記,在IRQ HANDLER裏面允許新的其他中斷嵌套進來。

老的Linux內核中,如果一箇中斷服務程序不想被別的中斷打斷,我們能看到這樣的代碼:

request_irq(FLOPPY_IRQ, floppy_interrupt,\
-					    IRQF_DISABLED, "floppy", NULL)

現在

在2010年如下的commit中,IRQF_DISABLED被作廢了:

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=e58aa3d2d0cc

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章