---
tags: NCKU Linux Kernel Internals, 作業系統
---
# Linux 核心設計: Interrupt
[Linux 核心設計: 中斷處理和現代架構考量](https://hackmd.io/@sysprog/linux-interrupt)
:::danger
老師的課程內容從很廣泛的角度講述了中斷相關的議題,一時無法完整的消化。此筆記僅補充一些基本的議題,整理的還不夠完整,詳細請參考課程錄影
:::
## What is interrupt?
Some reference to [CSE 438/598 Embedded Systems Programming](http://rts.lab.asu.edu/web_438_Fall_2014/CSE438_Fall2014_Main_page.htm) : [Linux Interrupt Processing and Kernel Thread](http://rts.lab.asu.edu/web_438/CSE438_598_slides_yhlee/438_7_Linux_ISR.pdf)
簡單概括的話,Interrupt 是一個通知 CPU 事件發生的機制,迫使 CPU 無論忙碌與否,都要對此事件做出回應。
當 interrupt 發生,類似於 context switch(需注意僅是類似,但本質上並不相同!),硬體會儲存當前 process 的狀態(通常需要儲存的訊息會相對 context switch 少一些),從 process context 切換到 interrupt context,判斷 interrupt 的類型後,使用對應的 interrupt handler 去對此進行處理。
### Preemptive Context Switching
![](https://i.imgur.com/j7tNtQQ.png)
Context Switching / multitasking 可以分成[協同式(Cooperative)](https://en.wikipedia.org/wiki/Cooperative_multitasking)與[搶佔式(Preemptive)](https://en.wikipedia.org/wiki/Preemption_(computing)),前者需由 thread 本身決定甚麼時候讓出 CPU 讓其他 thread 執行(例如透過 [schedule()](https://elixir.bootlin.com/linux/latest/source/kernel/sched/core.c#L4375)),後者則需藉由 interrupt,在每次離開 interupt context 時去做 context switch,把 CPU 移轉給當前優先權最高的 thread 去執行。
### Interrupt Handling
Interrupt 可以分成多種類型,例如:
* I/O interrupt
* Timer interrupt
* Interprocessor interrupt
![](https://i.imgur.com/o76BTom.png)
當外部的硬體裝置發出某種訊號,[PIC](https://en.wikipedia.org/wiki/Programmable_interrupt_controller) 會接收該硬體發出的 interrupt。PIC 接受的訊號會被轉換成一組 vector,用來查詢系統中的 [IDT](https://en.wikipedia.org/wiki/Interrupt_descriptor_table),找到對應的 [ISR / Interrupt handler](https://en.wikipedia.org/wiki/Interrupt_handler) 起始位址進行處理。每個 PIC 可以處理有限數量的 interrupts,如果讓其中一個 interupt 接受另一個 PIC 的 訊號,則可以擴充可處理的 interrupts 總量。
> 延伸閱讀: [PIC中斷控制器介紹](http://stenlyho.blogspot.com/2008/08/pic.html)
在現代的作業系統中,ISR 會被切成 top half 和 botton half 兩個部份,目的是為了減少任務的延遲。當 interrupt 發生,為了避免 nested interrupt 導致中斷的處理變得複雜(需考慮如 ISR 的 reentry、資源的互斥等),最簡單的作法是在 interrupt context 中關閉 interrupt,然而如果關閉的時間過長,可能會導致系統對 I/O 的回應變慢,導致錯過某個 interrupt 而產生延遲。
Top half 和 botton half 的區分使得系統可以把 interrupt 的處理推遲,在 top half 中,disable interrupt ,做最小而重要的任務後(例如 pending 發生的 interrupt 類型),enable interrupt,如果接著沒有 interrupt 進來,再對 bottom half 去做處理。藉此,降低處理 interrupt 產生的 latency。
在 linux 中,主要有三種延遲 intterupt 處理的機制:
* softirqs
* tasklets
* workqueues
### Softirq
softirq 在 kernel 的編譯時期就會被註冊,由 [open_softirq](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/kernel/softirq.c#L447) 初始化。
```c=
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
```
可以看到一個這裡去 index `softirq_vec` 並設定一個對應 softirq 處理的 function pointer。
```c=
struct softirq_action
{
void (*action)(struct softirq_action *);
};
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
const char * const softirq_to_name[NR_SOFTIRQS] = {
"HI", "TIMER", "NET_TX", "NET_RX", "BLOCK", "BLOCK_IOPOLL",
"TASKLET", "SCHED", "HRTIMER", "RCU"
};
```
`softirq_vec` 是型別為 `softirq_action` 的 array,結構中僅有一個指向 action funtion 的 pointer。在 `softirq_vec` 中,有 NR_SOFTIRQS(=10) 種的 softirq 被註冊:
* 兩個屬於 tasklet 的處理 (HI, TASKLET)
* 兩個屬於網路 (NET_TX, NET_RX)
* 兩個屬於 block device (BLOCK, BLOCK_IOPOLL)
* 兩個屬於 timer (TIMER, HRTIMER)
* 一個屬於 scheduler (SCHED)
* 一個屬於 read-copy-update (RCU)
透過 `cat /proc/softirqs` 也可以得到相關的資訊。
```c=
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
```
`raise_softirq` 會觸發 softirq 的處理。`local_irq_save` 首先將狀態存入一個 [Interrupt flag](https://en.wikipedia.org/wiki/Interrupt_flag) 並且關閉 interrupt,`local_irq_restore` 則反之會回存 flag,回復到 `local_irq_save` 之前的狀態(interrupt 可能是開或關,視乎保存前的狀況而定)。
關閉 interrupt 的理由為何呢? 這是由於 `raise_softirq_irqoff` 中將會對全域的變數做設置 bitflag 的操作(對某個位元做 or 1,詳見 `or_softirq_pending`),則倘若 interrupt 未關閉,將可能導致該全域變數的 race condition。因此避免另一個 softirq 的執行,才可以預防競爭導致的 dead lock。
```c=
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
if (!in_interrupt())
wakeup_softirqd();
}
```
`raise_softirq_irqoff` 會根據 `nr` 透過 `__raise_softirq_irqoff(nr)` 去 pending softirq 的 bitmask `__softirq_pending`, 標註要被延遲處理的 intterupt 類型。
在離開 `raise_softirq_irqoff` 之前,檢查 CPU 是在 interrupt context 或是 process context,如果是在 interrupt context 中,則 restore interrupt flag 再開啟 interrupt 即可,返回後會自然進行 softirq 的 bottom half 處理,但是如果是在 process context 的話,則需要透過 `wakeup_softirqd` 去喚醒 kernel thread deamon `ksoftirqd`。
```c=
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
...
restart:
while ((softirq_bit = ffs(pending))) {
...
h->action(h);
...
}
...
pending = local_softirq_pending();
if (pending) {
if (time_before(jiffies, end) && !need_resched() && --max_restart)
goto restart;
}
...
}
```
`ksoftirqd` 會透過 `run_ksoftirqd` 去檢查是否有被推遲處理的 interrupt ,使用 `__do_softirq` 去做對應的處理。根據 `__softirq_pending` 的 bitmask 內容,就可以知道有哪些 interrupt 的處理是被延遲的。
當系統在做推遲的處理時,有可能會不斷有新的 softirqs 發生,此時如果為了處理新的 softirq,可能會導致 userspace 的 thread 不能被排程,因此可以看到這裡會設定一個允許處理的時間。
對於有沒有被推遲的 softirq 檢查會被安插在 kernel 中以確保周期性的運作。主要的檢查點在 [`do_IRQ`](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/irq.c#L218) 中,也就是實際有 intterrupt 發生時的處理點。在 `do_IRQ` 的結束前,會呼叫 `exiting_irq()`,`exiting_irq()` 再呼叫 `irq_exit()`。
```c=
void irq_exit(void)
{
...
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
...
}
```
`irq_exit` 會檢查是否有 pending 的 softirq,呼叫的 `invoke_softirq` 也會呼叫 `__do_softirq`,對 bottom half 做相應的處理。
### Tasklet
Softirq 是面向性能的,相同的 softirq 可以同時在不同的 CPU 上平行進行,因此程式必須要可以 reentry,對於撰寫程式就增加了一定的難度。而其另一個缺點是在編譯時期就決定好對應的處理,無法動態的註冊和刪除,顯然對於 kernel module 的撰寫不大友善,而 tasklet 的設計可以解決此問題。
```c=
void __init softirq_init(void)
{
int cpu;
for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
}
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
```
在初始化階段時,程式會走遍所有 possible processors(支援熱插拔的 processor?),並初始化 [per_cpu](https://0xax.gitbooks.io/linux-insides/content/Concepts/linux-cpu-1.html) 的 `tasklet_vec` 和 `tasklet_hi_vec`
```c=
struct tasklet_head {
struct tasklet_struct *head;
struct tasklet_struct **tail;
};
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
```
每個 CPU 都會維護一個 tasklet 的 linked-list,其中 HI_SOFTIRQ 用於高優先級的 tasklet,TASKLET_SOFTIRQ 則用於普通的 tasklet。可以看到 `softirq_init` 的最後有呼叫我們在前面提到的 `open_softirq`,去註冊兩個 tasklet 相關的 softirq。
```c=
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->next = NULL;
t->state = 0;
atomic_set(&t->count, 0);
t->func = func;
t->data = data;
}
```
接著,我們可以透過 linux kernel 中提供的 API 來操作 tasklet。一個例子是 `tasklet_init`,可以用來動態的初始化 `tasklet_struct`
```c=
DECLARE_TASKLET(name, func, data);
DECLARE_TASKLET_DISABLED(name, func, data);
```
透過上面兩個 macro 也可以靜態的定義 tasklet。
```c=
void tasklet_schedule(struct tasklet_struct *t);
void tasklet_hi_schedule(struct tasklet_struct *t);
void tasklet_hi_schedule_first(struct tasklet_struct *t);
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
void __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;
local_irq_save(flags);
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_restore(flags);
}
```
上面的 API 則可以用來標示 tasklet 已經準備好要被執行(根據優先權的要求使用不同的 API)。以 tasklet_schedule 為例,會將 tasklet stuct 的狀態設成 `TASKLET_STATE_SCHED`,再去執行 `__tasklet_schedule`,`__tasklet_schedule` 的作用就類似前面提及的 `raise_softirq`,先保存 interrupt flag 並且關閉 interrupt,將 `tasklet_vec` 更新後,呼叫 `raise_softirq_irqoff` 去 pending softirq。如此一來,當 kernel 要去處理 bottom half 時,前面註冊的 softirq action `tasklet_action` 就會被呼叫。
```c=
static void tasklet_action(struct softirq_action *a)
{
local_irq_disable();
list = __this_cpu_read(tasklet_vec.head);
__this_cpu_write(tasklet_vec.head, NULL);
__this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head));
local_irq_enable();
while (list) {
if (tasklet_trylock(t)) {
t->func(t->data);
tasklet_unlock(t);
}
...
}
}
```
在 tasklet action 中,本地的 interrupt 會先被關閉,接著取出 local cpu 的 tasklet linked-list 到一個臨時變量中,再將該鍊linked-list 設為 NULL。然後開啟 interrupt,走遍整個 list。
```c=
static inline int tasklet_trylock(struct tasklet_struct *t)
{
return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);
}
```
`tasklet_trylock` 被呼叫來嘗試將 state 設為 `TASKLET_STATE_RUN`,如果成功,則執行在 `tasklet_init` 註冊的對應 function,結束後再透過 `tasklet_unlock` 回復 state。
注意到 softirq 和 tasklet 同樣運行在 interrupt context (software irq context) 之下,因此不允許 sleep / preempt / context switch,也不允許存取 userspace 的資料。此外,同一個 tasklet 不允許在多個 CPU 上平行處理,每個 tasklet 將僅在調度它的 CPU 上運行,以優化 cache 使用。因而這種設計可能不理想,因為其他潛在 idle 的 CPU 不能用於運行此 tasklet。
> * [why tasklet cant sleep](https://lists.kernelnewbies.org/pipermail/kernelnewbies/2011-November/003812.html)
> * [Why kernel code/thread executing in interrupt context cannot sleep?](https://stackoverflow.com/questions/1053572/why-kernel-code-thread-executing-in-interrupt-context-cannot-sleep/1056710#1056710)
### Work queue
workqueue 是另一種處理 bottom half 的方式,其最大的特點在於 workqueue 是執行在 kernel context,而非 interrupt context。
```c=
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
```
整個 workqueue 的核心概念是對 interrupt 的處理建立 per-CPU 的 kernel threads,而整個 workqueue 的基本單元根據一個 [`work_struct`](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/include/linux/workqueue.h#L100) 來描述。其中 `func` 是排程任務的執行內容,`data` 則是任務要處理的數據。
```c=
#define DECLARE_WORK(n, f) \
struct work_struct n = __WORK_INITIALIZER(n, f)
```
DECLARE_WORK 可以用來靜態建立 workqueue。
```c=
#define INIT_WORK(_work, _func) \
__INIT_WORK((_work), (_func), 0)
#define __INIT_WORK(_work, _func, _onstack) \
do { \
__init_work((_work), _onstack); \
(_work)->data = (atomic_long_t) WORK_DATA_INIT(); \
INIT_LIST_HEAD(&(_work)->entry); \
(_work)->func = (_func); \
} while (0)
```
或者可以通過 `INIT_WORK` 動態建立。
```c=
static inline bool queue_work(struct workqueue_struct *wq,
struct work_struct *work)
{
return queue_work_on(WORK_CPU_UNBOUND, wq, work);
}
```
一旦 `work_struct` 被建立,可以透過 `queue_work` 將其加入到 workqueue 中。`queue_work_on` 被呼叫,其中 `WORK_CPU_UNBOUND` 表示該 kernel thread 不限定在哪個 CPU 中被執行。
### Reference
* [Introduction to deferred interrupts (Softirq, Tasklets and Workqueues)](https://0xax.gitbooks.io/linux-insides/content/Interrupts/linux-interrupts-9.html)
* [linux kernel的中断子系统之(八):softirq](http://www.wowotech.net/irq_subsystem/soft-irq.html)
* [linux kernel的中断子系统之(九):tasklet](http://www.wowotech.net/irq_subsystem/tasklet.html)
* [softirq, tasklet和workqueue的区别](https://blog.csdn.net/jusang486/article/details/51155277)
## TODO
- [ ] 自行閱讀 softirq、tasklet、work queue 的程式碼,並透過實驗補充二手文章中可能忽略的更多的細節
- [ ] 研究 interrupt 在多核心上的額外考量
- [ ] 研究虛擬化技術的實作框架(如何運作?),以及其對作業系統在中斷處理上的影響