Linux 中斷子系統:中斷處理知識點大全
Linux 中斷相關節點
/proc/interrupts
cat 這個節點,會打印系統中所有的中斷信息,如果是多核CPU,每個核都會打印出來。
包括每個中斷的名字、中斷號 IRQ number、每個中斷的觸發次數、在哪個CPU核處理的、是邊沿觸發還是電平觸發,屬于哪個中斷控制器,都會打印出來。
/proc/irq/…
進入這個目錄。會看到以中斷號命名的文件夾,每個中斷號文件夾下面都有幾個節點,存儲了這個中斷的信息,比如 smp_affinity、affinity_hint、spurious等。smp_affinity 代表中斷號核CPU之間的親緣綁定關系,也就是如果某個中斷號綁定了一個CPU核,那么這個中斷就會一直在這個CPU上處理。
如何讓某個中斷在某個特定的 CPU 處理?
kernel 2.4 以后的版本才支持把不同的硬件中斷請求(IRQs)分配到特定的 CPU 上,這個綁定技術被稱為 SMP IRQ Affinity. 更多介紹請參看 Linux 內核源代碼自帶的文檔:linux-4.14/Documentation/IRQ-affinity.txt
/proc/irq/{IRQ}/smp_affinity
/proc/irq/{IRQ}/smp_affinity_list
/proc/irq/{IRQ}/smp_affinity 指定給定的 irq 中斷號源允許哪些CPU執行,它是一個掩碼位,比如是ff,代表11111111,表示這個中奪冠可以在 8 個 CPU 執行,具體在哪一個 CPU 執行,靠分配器分配。
如果這個 /proc/irq/{IRQ}/smp_affinity 指定為 00000001,代表這個IRQ只能在最后一個CPU核進行處理,其他CPU不允許處理,大家可以測試一下,博主測試是 OK 的(GIC支持,其他中斷控制器不一定)。
串口手動賦值的重啟以后會消失,可以在代碼中調用 irq_set_affinity 函數,指定中斷的掩碼,來達到某個中斷被固定CPU處理的需求。
中斷分發機制
對于 GIC-V2 而言,SPI 的分發是根據 Distributor 中的 Interrupt Processor Targets Registers 來決定的。對于任何一個 SPI,其都有在某個 GICD_ITARGETSRn 寄存器中有 8 個bit標識送達的 processor,如果只有一個 bit 被 set,那么就很簡單了,如果該中斷是當前優先級最高的中斷,那么 Distributor 就會送到對應的 CPU interface,該中斷最終會送達指定的 CPU。
如果該中斷對應的 Interrupt Processor Targets Registers 中的那 8 個 bit 有多個 bit 被 set 的話,Distributor 如何處理呢?“依次輪著把產生的中斷給各個 CPU,還是說看哪個CPU有空就給哪個CPU來著”,讓硬件處理這么復雜的邏輯有些不合適,實際上,GIC 的硬件是不會進行任何判斷的,也不會集成任何的算法,它就是根據Interrupt Processor Targets Registers的bit設定情況,忠實的把中斷送往指定的一個processor或者多個processors。
大家可以去看看 gic_set_affinity 這個函數,這個函數確保一個中斷的 Interrupt Processor Targets Registers 中的那8個bit只有一個bit被設定。
/kernel5.15/drivers/irqchip/irq-gic-v3.c
在 1244 和 1246 行,1246 行就是在 online 的 CPU 中選中一個,1263 行寫入到寄存器中,GIC 會讀取這個寄存器,是哪個 CPU,然后將中斷發給這個CPU。中間的函數很簡單,大家可以自己看。
中斷狀態機
對于 GIC-V2 而言,中斷的狀態機由 Distributor 維護,每個中斷都有一個狀態機。
Inactive :中斷未激活(未發生)。
Pending:中斷到達 GIC ,等待 CPU 的處理。
Active:中斷得到 CPU 的應答,中斷被CPU處理。
Active and pending :某個中斷正在被 CPU 處理,這時候該中斷又來了。
來看一個例子:
(a)N 和 M 用來標識兩個外設中斷,N 的優先級大于 M
(b)兩個中斷都是 SPI 類型,level trigger,active-high
(c)兩個中斷被配置為去同一個 CPU
(d)都被配置成 group 0,通過 FIQ 觸發中斷
時刻 | 事件 |
T0時刻 | Distributor檢測到M這個interrupt source的有效觸發電平 |
T2時刻 | Distributor將M這個interrupt source的狀態設定為pending |
T17時刻 | 大約15個clock之后,CPU interface拉低nFIQCPU信號線,向CPU報告M外設的中斷請求。這時候,CPU interface的ack寄存器(GICC_IAR)的內容會修改成M interrupt source對應的ID |
T42時刻 | Distributor檢測到N這個優先級更高的interrupt source的觸發事件 |
T43時刻 | Distributor將N這個interrupt source的狀態設定為pending。同時,由于N的優先級更高,因此Distributor會標記當前優先級最高的中斷 |
T58時刻 | 大約15個clock之后,CPU interface拉低nFIQCPU信號線,向CPU報告N外設的中斷請求。當然,由于T17時刻已經assert CPU了,因此實際的電平信號仍然保持asserted。這時候,CPU interface的ack寄存器(GICC_IAR)的內容會被更新成N interrupt source的ID |
T61時刻 | 軟件通過讀取 ack 寄存器的內容,獲取了當前優先級最高的,并且狀態是pending的interrupt ID(也就是N interrupt source對應的ID),通過讀該寄存器,CPU也就ack了該interrupt source N。這時候,Distributor將N這個interrupt source的狀態設定為pending and active(因為是電平觸發,只要外部仍然有asserted的電平信號,那么一定就是pending的,而該中斷是正在被CPU處理的中斷,因此狀態是pending and active)注意:T61標識CPU開始服務該中斷 |
T64時刻 | 3個clock之后,由于CPU已經ack了中斷,因此GIC中CPU interface模塊 deassert nFIQCPU信號線,解除發向該CPU的中斷請求 |
T126時刻 | 由于中斷服務程序操作了N外設的控制寄存器(ack外設的中斷),因此N外設deassert了其interrupt request signal |
T128時刻 | Distributor解除N外設的pending狀態,因此N這個interrupt source的狀態設定為active |
T131時刻 | 軟件操作End of Interrupt寄存器(向GICC_EOIR寄存器寫入N對應的interrupt ID),標識中斷處理結束。Distributor將N這個interrupt source的狀態修改為idle,注意:T61~T131是CPU服務N外設中斷的的時間區域,這個期間,如果有高優先級的中斷pending,會發生中斷的搶占(硬件意義的),這時候CPU interface會向CPU assert 新的中斷。 |
T146時刻 | 大約15個clock之后,Distributor向CPU interface報告當前pending且優先級最高的interrupt source,也就是M了。漫長的pending之后,M終于迎來了春天。CPU interface拉低nFIQCPU信號線,向CPU報告M外設的中斷請求。這時候,CPU interface的ack寄存器(GICC_IAR)的內容會修改成M interrupt source對應的ID |
T211時刻 | CPU ack M中斷(通過讀GICC_IAR寄存器),開始處理低優先級的中斷 |
Linux 搶占機制
GIC 中斷控制器支持中斷優先級搶占,一個高優先級中斷可以搶占一個低優先級且處于active狀態的中斷,即GIC仲裁單元會記錄和比較當前優先級最高的pending狀態,然后去搶占當前中斷,并且發送這個最高優先級的中斷請求給CPU。
從GIC角度看,GIC 會發送高優先級中斷請求給CPU。但是CPU不一定響應!!!因為在中斷處理過程中,CPU處于關中斷狀態(關閉本CPU),需要等低優先級中斷處理完畢,直到發送 EOI 給GIC,然后CPU才會響應pending狀態中優先級最高的中斷進行處理。所以 Linux 下:
1、高優先級中斷無法搶占正在執行的低優先級中斷。
2、同處于 pending 狀態的中斷,優先響應高優先級中斷進行處理。
3、同優先級同是 pending 狀態的中斷,選擇硬件中斷號 ID 最小的一個發給CPU。
這樣是可以理解的,如果萬一中斷大量爆發,中斷如果允許嵌套的話,棧會越來越大,會爆掉,所以為了防止這種情況發生,Linux中中斷不允許嵌套,單CPU中,在一個中斷處理完之前,不會相應另外一個中斷,哪怕優先級比它高。
FreeRTOS 中是允許高優先級中斷搶占正在執行的低優先級中斷,不同系統設定不一樣。
中斷與進程
進程調度是一個復雜的機制, 根據需求的不同,在不同時刻會切換調度機制,CPU會根據進程優先級、時間片等信息,對不同進程進行調度。
中斷可以打斷進程的運行,任意一個中斷的優先級都比所有的進程高。
在中斷處理過程中,主要是 GIC 和 CPU 的交互,即便 GIC 支持高優先級中斷搶占正在執行的低優先級中斷,發信號給 CPU core,但是 CPU core 可以不處理,因為 Linux 中當 CPU core 執行中斷處理時,是關中斷和關搶占的狀態,不再相應中斷信號。
也就意味著,在中斷優先級這個概念中,只有當 GIC 同時存在多個 pending 的中斷,這時候會選擇優先級最高的去執行,高優先級會搶占低優先級中斷(哪怕低優先級先來)。如果低優先級中斷處于 active 狀態,是不可以被搶占的,這是前后關系。搶占只存在于同時是pending 狀態的時候。
Linux 為什么中斷不允許休眠?
所謂的睡眠,就是調用 schedule 讓出 CPU,調度器選擇另外個進程繼續執行,這個過程涉及進程棧空間的切換。
1、假如中斷上下文中調用 schedule ,此時獲取的 struct thread info 數據結構是發生中斷時該進程棧信息,而不是中斷上下文調用 schedule 時任何信息。這就導致再也無法返回中斷上下文中調用 schedule 的地方。
2、中斷上下文處于關中斷中,需要發送個 EOI 通知 GIC 中斷處理結束,GIC 和CPUinterface 才會進入下一次中斷處理。如果中途 schedule,那么整個系統的中斷都會被屏蔽掉。
一般進入中斷后,需要關中斷,也會關搶占,同時注意不可以調用schedule。
unhandled interrupt 和 spurious interrupt
未處理中斷和虛假中斷
在中斷處理的最后,總會有一段代碼如下:
- irqreturn_t
- handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action)
- {
- ……
- if (!noirqdebug)
- note_interrupt(irq, desc, retval);
- return retval;
- }
note_interrupt就是進行unhandled interrupt和spurious interrupt處理的。對于這類中斷,linux kernel有一套復雜的機制來處理,你可以通過command line參數(noirqdebug)來控制開關該功能。
當發生了一個中斷,但是沒有被處理(有兩種可能,一種是根本沒有注冊的 specific handler,第二種是有 handler,但是 handler 否認是自己對應的設備觸發的中斷),怎么辦?毫無疑問這是一個異常狀況,那么 kernel 是否要立刻采取措施將該 IRQ disable 呢?也不太合適,畢竟 interrupt request 信號線是允許共享的,直接 disable 該 IRQ 有可能會下手太狠,kernel 采取了這樣的策略:如果該 IRQ 觸發了 100,000 次,但是 99,900 次沒有處理,在這種條件下,我們就是 disable 這個 interrupt request line。
中斷線和中斷號是一個意思。
相關的控制數據在中斷描述符中,如下:
- struct irq_desc {
- ……
- unsigned int irq_count;--------記錄發生的中斷的次數,每100,000則回滾
- unsigned long last_unhandled;-----上一次沒有處理的IRQ的時間點
- unsigned int irqs_unhandled;------沒有處理的次數
- ……
- }
中斷的生命周期