轉自 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)軟中斷和tasklet和Linux中斷管理 (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內核版本,中斷分爲兩種:
-
快中斷,申請的時候帶IRQF_DISABLED標記,在IRQ HANDLER裏面不允許新的中斷進來;
-
慢中斷,申請的時候不帶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