一個能思想的人,才真是一個力量無邊的人。
一、Cortex-A7系列中斷介紹
Cortex-A7內核只有8個異常中斷,這8個異常中斷的中斷向量表如下:
向量地址 | 終端類型 | 中斷模式 |
---|---|---|
0x00 | 復位中斷(Rest) | 特權模式(SVC) |
0x04 | 未定義指令中斷(Undefined Instruction) | 未定義指令中止模式(Undef) |
0x08 | 軟中斷(Software Interrupt,SWI) | 特權模式(SVC) |
0x0C | 指令預取中止中斷(Prefetch Abort) | 中止模式 |
0x10 | 數據訪問中止中斷(Data Abort) | 中止模式 |
0x14 | 未使用(Not Used) | 未使用 |
0x18 | IRQ 中斷(IRQ Interrupt) | 外部中斷模式(IRQ) |
0x1C | FIQ 中斷(FIQ Interrupt) | 快速中斷模式(FIQ) |
Cortex-A7的中斷控制器叫做GIC。【類比STM32的NVIC】GIC可以開關中斷,設置中斷優先級!Cortex-A7用到的版本是GIC V2,該版本最多支持8個核。GIC 將衆多的中斷源分爲分爲三類:SPI、PPI、SGI。我們重點關注SPI(Shared Peripheral Interrupt,共享中斷)。那些外部中斷都屬於 SPI 中斷(注意!不是 SPI 總線那個中斷) 。比如按鍵中斷、串口中斷等等,這些中斷所有的 Core 都可以處理,不限定特定 Core。
Cortex-A 內核 CPU 的所有外部中斷都屬於這個 IRQ 中斷,當任意一個外部中斷髮生的時候都會觸發 IRQ 中斷。在 IRQ 中斷服務函數裏面就可以讀取指定的寄存器來判斷髮生的具體是什麼中斷,所以我們重點關注IRQ中斷!
爲了區分不同的中斷,引入了終端號。SPI(共享中斷)分到的中斷號範圍是ID32~ID1019,一共998箇中斷號。像 GPIO 中斷、串口中斷等這些外部中斷都在這裏面 ,至於具體到某個 ID 對應哪個中斷,那就由半導體廠商根據實際情況去定義了。我們需要使用的時候,查芯片手冊即可。
二、Linux中斷處理
Linux 系統對中斷處理的演進,是使用內核線程來處理中斷。關於進程和線程,這裏也簡單介紹下進程和線程的知識。
- Linux中資源分配的單位是進程,調度的單位是線程。
- 在一個進程裏,可能有多個線程,這些線程共用打開的文件句柄、全局變量等等。
- 而這些線程,之間是互相獨立的,“同時運行”,也就是說:每一個線程,都有自己的棧。
Linux 系統中不僅含有硬件中斷,也有軟件中斷。我們一一道來!
2.1 硬件中斷
Linux中對於硬件產生的中斷,稱之爲“硬件中斷”(hard irq)。如按鍵中斷,串口中斷,並且每個硬件中斷都有對應的處理函數。
2.2 軟件中斷
- 軟件中斷何時生產?
- 由軟件決定,對於 X 號軟件中斷,只需要把它的 flag 設置爲 1 就表示發生了該中斷。
- 軟件中斷何時處理?
- 軟件中斷嘛,並不是那麼十萬火急,有空再處理它好了。什麼時候有空?不能讓它一直等吧?Linux 系統中,各種硬件中斷頻繁發生,至少定時器中斷每 10ms 發生一次,那取個巧?在處理完硬件中斷後,再去處理軟件中斷?就這麼辦!
- 有哪些軟件中斷?
- 查內核源碼 include/linux/interrupt.h
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
numbering. Sigh! */
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
- 怎麼觸發軟件中斷?
- 最核心的函數是 raise_softirq,簡單地理解就是設置 softirq_veq[nr]的標記位。
extern void raise_softirq(unsigned int nr);
- 怎麼設置軟件中斷的處理函數?
extern void open_softirq(int nr, void (*action)(struct softirq_action *));
後面講到的中斷下半部 tasklet 就是使用軟件中斷實現的。
2.3 中斷處理原則
2.3.1 中斷處理不能嵌套
中斷嵌套突然暴發,那麼棧將越來越大,棧終將耗盡。所以,爲了防止這種情況發生,也是爲了簡單化中斷的處理,在 Linux 系統上中斷無法嵌套:即當前中斷 A 沒處理完之前,不會響應另一箇中斷 B(即使它的優先級更高)。
2.3.1 中斷處理越快越好
在單芯片系統中,假設中斷處理很慢,那應用程序在這段時間內就無法執行:系統顯得很遲頓。在 SMP 系統中,假設中斷處理很慢,那麼正在處理這個中斷的 CPU 上的其他線程也無法執行。在中斷的處理過程中,該 CPU 是不能進行進程調度的,所以中斷的處理要越快越好,儘早讓其他中斷能被處理──進程調度靠定時器中斷來實現。
在 Linux 系統中使用中斷,只需爲某個中斷 irq 註冊中斷處理函數 handler。
- 使用request_irq 註冊Handler,函數原型如下:
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
但是,處理某個中斷要做的事情就是很多,沒辦法加快。 比如對於按鍵中斷,我們需要等待幾十毫秒消除機械抖動。難道要在 handler 中等待嗎?對於計算機來說,這可是一個段很長的時間。怎麼辦?
這時,“下半部”的處理思想便由此產生。
2.4 要處理的事情實在太多,拆分爲:上半部、下半部
當一箇中斷要耗費很多時間來處理時,它的壞處是:在這段時間內,其他中斷無法被處理。 換句話說, 在這段時間內,系統是關中斷的。如果某個中斷就是要做那麼多事,我們能不能把它拆分成兩部分:緊急的、不緊急的?在 handler 函數裏只做緊急的事,然後就重新開中斷,讓系統得以正常運行;那些不緊急的事,以後再處理,處理時是開中斷的。
中斷下半部的實現有很多種方法,先來了解兩種主要的: tasklet(小任務)、 work queue(工作隊列)。
2.4.1 下半部要做的事情耗時不是太長: tasklet
假設我們把中斷分爲上半部、下半部。 發生中斷時,上半部下半部的代碼何時、如何被
調用?
當下半部比較耗時但是能忍受,並且它的處理比較簡單時,可以用 tasklet 來處理下半部。 tasklet 是使用軟件中斷來實現。
貼代碼,一目瞭然:
使用流程圖簡化一下:
假設硬件中斷 A 的上半部函數爲 irq_top_half_A,下半部爲 irq_bottom_half_A。使用情景化的分析,才能理解上述代碼的精華。
-
a. 硬件中斷 A 處理過程中,沒有其他中斷髮生:
一開始, preempt_count = 0;
上述流程圖①~⑨依次執行,上半部、下半部的代碼各執行一次。 -
b. 硬件中斷 A 處理過程中,又再次發生了中斷 A:
一開始, preempt_count = 0;
執行到第⑥時,一開中斷後,中斷 A 又再次使得 CPU 跳到中斷向量表。
注意:這時 preempt_count 等於 1,並且中斷下半部的代碼並未執行。
CPU 又從①開始再次執行中斷 A 的上半部代碼:
在第①步 preempt_count 等於 2;
在第③步 preempt_count 等於 1;
在第④步發現 preempt_count 等於 1,所以直接結束當前第 2 次中斷的處理;
注意:重點來了,第 2 次中斷髮生後,打斷了第一次中斷的第⑦步處理。當第 2 次中斷 A處理完畢, CPU 會繼續去執行第⑦步。
可以看到,發生 2 次硬件中斷 A 時,它的上半部代碼執行了 2 次,但是下半部代碼只
執行了一次。所以,同一個中斷的上半部、下半部,在執行時是多對一的關係。
- c. 硬件中斷 A 處理過程中,又再次發生了中斷 B:
一開始, preempt_count = 0;
執行到第⑥時,一開中斷後,中斷 B 又再次使得 CPU 跳到中斷向量表。
注意:這時 preempt_count 等於 1,並且中斷 A 下半部的代碼並未執行。
CPU 又從①開始再次執行中斷 B 的上半部代碼:
在第①步 preempt_count 等於 2;
在第③步 preempt_count 等於 1;
在第④步發現 preempt_count 等於 1,所以直接結束當前第 2 次中斷的處理;
注意:重點來了,第 2 次中斷髮生後,打斷了第一次中斷 A 的第⑦步處理。當第 2 次中斷 B處理完畢, CPU 會繼續去執行第⑦步。
可以看到,在第⑦步裏,它會去執行中斷 A 的下半部,也會去執行中斷 B 的下半部。
所以,多箇中斷的下半部,是彙集在一起處理的。
總結:
- a. 中斷的處理可以分爲上半部,下半部
- b. 中斷上半部,用來處理緊急的事,它是在關中斷的狀態下執行的
- c. 中斷下半部,用來處理耗時的、不那麼緊急的事,它是在開中斷的狀態下執行的
- d. 中斷下半部執行時,有可能會被多次打斷,有可能會再次發生同一個中斷
- e. 中斷上半部執行完後,觸發中斷下半部的處理
- f. 中斷上半部、下半部的執行過程中,不能休眠:中斷休眠的話,以後誰來調度進程啊?
2.4.2 下半部要做的事情太多並且很複雜:工作隊列(work queue)
在中斷下半部的執行過程中,雖然是開中斷的,期間可以處理各類中斷。但是畢竟整個中斷的處理還沒走完,這期間 APP 是無法執行的。假設下半部要執行 1、 2 分鐘,在這 1、 2 分鐘裏 APP 都是無法響應的。
這誰受得了?
所以,如果中斷要做的事情實在太耗時,那就不能用軟件中斷來做,而應該用內核線程來做:在中斷上半部喚醒內核線程。內核線程和 APP 都一樣競爭執行, APP 有機會執行,系統不會卡頓。
這個內核線程是系統幫我們創建的,一般是 kworker 線程,內核中有很多這樣的線程:
kworker 線程要去“工作隊列”(work queue)上取出一個一個“工作”(work),來執行它裏面的函數。
那我們怎麼使用 work、 work queue 呢?
-
a. 創建 work:
- 你得先寫出一個函數,然後用這個函數填充一個 work 結構體。比如:
- 你得先寫出一個函數,然後用這個函數填充一個 work 結構體。比如:
-
b. 要執行這個函數時,把 work 提交給 work queue 就可以了:
- 下述函數會把 work 提供給系統默認的 work queue: system_wq,它是一個隊列。
- 下述函數會把 work 提供給系統默認的 work queue: system_wq,它是一個隊列。
-
c. 誰來執行 work 中的函數?
- 不用我們管, schedule_work 函數不僅僅會把 work 放入隊列,還會把 kworker 線程喚醒。此線程搶到時間運行時,它就會從隊列中取出 work,執行裏面的函數。
-
d. 誰把 work 提交給 work queue?
- 在中斷場景中,可以在中斷上半部調用 schedule_work 函數。
總結:
- a. 很耗時的中斷處理,應該放到線程裏去
- b. 可以使用 work、 work queue
- c. 在中斷上半部調用 schedule_work 函數,觸發 work 的處理
- d. 既然是在線程中運行,那對應的函數可以休眠。
2.5 新技術: threaded irq
使用線程來處理中斷,並不是什麼新鮮事。 使用 work 就可以實現,但是需要定義 work、調用 schedule_work,好麻煩啊。
太懶了太懶了,就這 2 步你們都不願意做。好,內核是爲懶人服務的,再殺出一個函數:
你可以只提供 thread_fn,系統會爲這個函數創建一個內核線程。發生中斷時,內核線程就會執行這個函數。
說懶是開玩笑,內核開發者也不會那麼在乎懶人。
以前用 work 來線程化地處理中斷,一個 worker 線程只能由一個 CPU 執行,多箇中斷的 work 都由同一個 worker 線程來處理,在單 CPU 系統中也只能忍着了。但是在 SMP 系統中,明明有那麼多 CPU 空着,你偏偏讓多箇中斷擠在這個 CPU 上?
新技術 threaded irq,爲每一箇中斷都創建一個內核線程;多箇中斷的內核線程可以分配到多個 CPU 上執行, 這樣便提高了執行效率。
三、Linux中斷系統中的重要數據
能弄清楚上面這個圖,對 Linux 中斷系統的掌握也基本到位了。最核心的結構體是 irq_desc,之前爲了易於理解,我們前面說在 Linux 內核中有一箇中斷數組,對於每一個硬件中斷,都有一個數組項, 這個數組就是 irq_desc 數組。
注意: 如果內核配置了 CONFIG_SPARSE_IRQ,那麼它就會用基數樹(radix tree)來代替irq_desc 數組。 SPARSE 的意思是“稀疏”,假設大小爲 1000 的數組中只用到 2 個數組項,那不是浪費嘛? 所以在中斷比較“稀疏”的情況下可以用基數樹來代替數組。
3.1 irq_desc 數組
irq_desc 結構體在 include/linux/irqdesc.h 中定義,主要內容如下圖:
每一個 irq_desc 數組項中都有一個函數: handle_irq, 還有一個 action 鏈表。 要理解它們,需要先看中斷結構圖:
關注上圖中的A號中斷和B號中斷位置!
外部設備 1、外部設備 n 共享一個 GPIO 中斷 B,多個 GPIO 中斷匯聚到 GIC(通用中斷控制器)的 A 號中斷, GIC 再去中斷 CPU。那麼軟件處理時就是反過來,先讀取 GIC 獲得中斷號 A,再細分出 GPIO 中斷 B,最後判斷是哪一個外部芯片發生了中斷。
所以, 中斷的處理函數來源有三:
- ① GIC 的處理函數:
假設 irq_desc[A].handle_irq 是 XXX_gpio_irq_handler(XXX 指廠家), 這個函數需要讀取芯片的 GPIO 控制器,細分發生的是哪一個 GPIO 中斷(假設是 B),再去調用 irq_desc[B].handle_irq。
注意: irq_desc[A].handle_irq 細分出中斷後 B,調用對應的irq_desc[B].handle_irq。顯然中斷 A 是 CPU 感受到的頂層的中斷, GIC 中斷 CPU 時, CPU 讀取 GIC 狀態得到中斷 A。
- ② 模塊的中斷處理函數:
比如對於 GPIO 模塊向 GIC 發出的中斷 B, 它的處理函數是irq_desc[B].handle_irq。BSP 開發人員會設置對應的處理函數,一般是 handle_level_irq 或 handle_edge_irq,從名字上看是用來處理電平觸發的中斷、邊沿觸發的中斷。
注意:導致 GPIO 中斷 B 發生的原因很多,可能是外部設備 1,可能是外部設備 n,可能只是某一個設備,也可能是多個設備。所以 irq_desc[B].handle_irq 會調用某個鏈表裏的函數,這些函數由外部設備提供。這些函數自行判斷該中斷是否自己產生, 若是則處理。
- ③ 外部設備提供的處理函數:
這裏說的“外部設備”可能是芯片,也可能總是簡單的按鍵。它們的處理函數由自己驅動程序提供,這是最熟悉這個設備的“人”: 它知道如何判斷設備是否發生了中斷,如何處理中斷。
對於共享中斷,比如 GPIO 中斷 B, 它的中斷來源可能有多個, 每個中斷源對應一箇中斷處理函數。所以 irq_desc[B]中應該有一個鏈表,存放着多箇中斷源的處理函數。一旦程序確定發生了 GPIO 中斷 B,那麼就會從鏈表裏把那些函數取出來,然後一一執行。這個鏈表就是 action 鏈表。鏈表可以在本部分開頭的第一張圖片看到。
3.2 irqaction 結構體
irqaction 結構體在 include/linux/interrupt.h 中定義,主要內容如下圖:
當調用 request_irq、 request_threaded_irq 註冊中斷處理函數時,內核就會構造一個irqaction 結構體。在裏面保存 name、 dev_id 等,最重要的是 handler、 thread_fn、 thread。
- handler 是中斷處理的上半部函數, 用來處理緊急的事情。
- thread_fn 對應一個內核線程 thread,當 handler 執行完畢, Linux 內核會喚醒對應的內核線程。在內核線程裏,會調用 thread_fn 函數。
- 可以提供 handler 而不提供 thread_fn,就退化爲一般的 request_irq 函數。(只有上半部)
- 可以不提供 handler 只提供 thread_fn, 完全由內核線程來處理中斷。(只有下半部)
- 也可以既提供 handler 也提供 thread_fn,這就是中斷上半部、下半部。
裏面還有一個名爲 sedondary 的 irqaction 結構體,它的作用以後再分析。在 reqeust_irq 時可以傳入 dev_id, 爲何需要 dev_id? 作用有 2:
- ① 中斷處理函數執行時,可以使用 dev_id
- ② 卸載中斷時要傳入 dev_id,這樣才能在 action 鏈表中根據 dev_id 找到對應項
所以在共享中斷中必須提供 dev_id, 非共享中斷可以不提供。
3.3 irq_data 結構體
irq_data 結構體在 include/linux/irq.h 中定義,主要內容如下圖:
它就是個中轉站,裏面有 irq_chip 指針、irq_domain 指針,都是指向別的結構體。
比較有意思的是 irq和hwirq,其中irq 是軟件中斷號, hwirq 是硬件中斷號。 比如上面我們舉的例子,在 GPIO 中斷 B 是軟件中斷號,可以找到 irq_desc[B]這個數組項; GPIO 裏的第 x 號中斷, 這就是 hwirq。
誰來建立 irq、 hwirq 之間的聯繫呢?由 irq_domain 來建立。 irq_domain 會把本地的hwirq 映射爲全局的 irq,什麼意思?比如 GPIO 控制器裏有第 1 號中斷, UART 模塊裏也有第 1 號中斷,這兩個“第 1 號中斷”是不一樣的,它們屬於不同的“域”──irq_domain。
3.4 irq_domain 結構體
irq_domain 結構體在 include/linux/irqdomain.h 中定義,主要內容如下圖:
當我們後面從設備樹講起,如何在設備樹中指定中斷,設備樹的中斷如何被轉換爲 irq時, irq_domain 將會起到極大的作爲。這裏基於入門的解度簡單講講,在設備樹中你會看到這樣的屬性:
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_RISING>;
它表示要使用 gpio1 裏的第 5 號中斷, hwirq 就是 5。但是我們在驅動中會使用 request_irq(irq, handler)這樣的函數來註冊中斷, irq 是什麼?它是軟件中斷號,它應該從“gpio1 的第 5 號中斷”轉換得來。
誰把 hwirq 轉換爲 irq?由 gpio1 的相關數據結構,就是 gpio1 對應的 irq_domain 結構體。
irq_domain 結構體中有一個 irq_domain_ops 結構體,裏面有各種操作函數,主要是:
-
① xlate
用來解析設備樹的中斷屬性, 提取出 hwirq、 type 等信息。 -
② map
把 hwirq 轉換爲 irq。
3.5 irq_chip 結構體
irq_chip 結構體在 include/linux/irq.h 中定義,主要內容如下圖:
這個結構體跟“chip”即芯片相關,裏面各成員的作用在頭文件中也列得很清楚, 摘錄部分如下:
* @irq_startup: start up the interrupt (defaults to ->enable if NULL)
* @irq_shutdown: shut down the interrupt (defaults to ->disable if NULL)
* @irq_enable: enable the interrupt (defaults to chip->unmask if NULL)
* @irq_disable: disable the interrupt
* @irq_ack: start of a new interrupt
* @irq_mask: mask an interrupt source
* @irq_mask_ack: ack and mask an interrupt source
* @irq_unmask: unmask an interrupt source
* @irq_eoi: end of interrupt
我們在 request_irq 後,並不需要手工去使能中斷,原因就是系統調用對應的 irq_chip 裏的函數幫我們使能了中斷。
我們提供的中斷處理函數中,也不需要執行主芯片相關的清中斷操作,也是系統幫我們調用 irq_chip 中的相關函數。
但是對於外部設備相關的清中斷操作,還是需要我們自己做的。就像上面圖裏的“外部設備 1“、“外部設備 n”, 外設備千變萬化,內核裏可沒有對應的清除中斷操作。