### 中斷(Interrupt)
中斷處理主要是指當硬體(或軟體)發生事件時,CPU 會「打斷」目前的程式流程,轉去處理更緊急事件的機制,會跳去執行一段「中斷服務程式(ISR)」的機制,在此筆記會去詳細介紹中斷處理機制是如何進行的。
#### 中斷的流程
```
硬體事件(例如:裝置完成傳輸)
↓
CPU 停下目前工作
↓
呼叫中斷服務常式(ISR / top half)
↓
(可選)安排延遲執行(bottom half,如 tasklet 或 workqueue)
↓
CPU 回到原本的工作
```
#### 中斷的功能與特性
| 功能 | 說明 |
| ------ | ------------------- |
| 即時響應 | 快速反應外部事件(如資料傳送完成) |
| 提高效率 | CPU 不用忙等(busy wait) |
| 硬體驅動溝通 | 硬體透過中斷告訴系統「我準備好了」 |
| 減少延遲 | 取代輪詢(polling)方式 |
#### 驅動中的中斷範例
```javascript=
irqreturn_t my_isr(int irq, void *dev_id)
{
printk(KERN_INFO "Interrupt received!\n");
tasklet_schedule(&my_tasklet); // 延遲處理
return IRQ_HANDLED;
}
```
### 延遲執行(Delayed Execution)
延遲執行的意思是:
「把某些不需要立即做的工作,安排在稍後(或背景)再執行。」
它的目的是:
* 減少中斷處理時間(讓 ISR 越快越好)
* 分離「即時事件」與「後續處理」
* 提升系統整體效能與回應速度
常見用途:
在驅動程式中,延遲一段時間後再檢查硬體狀態。
延遲重新嘗試某個動作(例如:重試 I/O)。
延遲某些背景工作(例如清除 buffer、更新狀態)。
#### 延遲執行的幾種常見機制
| 機制 | 執行上下文 | 可睡眠 | 適用情境 |
| --------------------------------- | --------------- | --- | ------------ |
| **Tasklet** | 軟中斷(Softirq) | ❌ | 中斷下半部,快速處理 |
| **Workqueue** | Process context | ✅ | 可以睡眠或呼叫阻塞函式 |
| **Timer** | 軟中斷(Softirq) | ❌ | 定時任務、timeout |
| **Delayed work** | Process context | ✅ | 延遲工作,可睡眠 |
| **msleep() / schedule_timeout()** | Process context | ✅ | 主動延遲執行 |
#### 兩者的關係:中斷 + 延遲執行
* 這兩者常常「搭配使用」!
* 常見模式:中斷上半部 + 延遲執行下半部
| 部分 | 名稱 | 功能 | 特性 |
| --------------------- | ------------------- | ------------- | ---------- |
| **上半部 (Top Half)** | ISR(中斷服務常式) | 立即響應硬體事件、讀取狀態 | 快速、不能睡眠 |
| **下半部 (Bottom Half)** | tasklet / workqueue | 延遲處理大量或耗時工作 | 可以延遲、部分可睡眠 |
#### 驅動中的延遲執行範例
1. 中斷 + tasklet
```javascript=
#include <linux/module.h>
#include <linux/interrupt.h>
static struct tasklet_struct my_tasklet;
void my_tasklet_func(unsigned long data)
{
printk(KERN_INFO "Tasklet: process deferred work\n");
}
irqreturn_t my_isr(int irq, void *dev_id)
{
printk(KERN_INFO "ISR: interrupt occurred\n");
tasklet_schedule(&my_tasklet); // 延遲執行
return IRQ_HANDLED;
}
static int __init my_init(void)
{
tasklet_init(&my_tasklet, my_tasklet_func, 0);
request_irq(10, my_isr, IRQF_SHARED, "my_irq", &my_tasklet);
return 0;
}
static void __exit my_exit(void)
{
tasklet_kill(&my_tasklet);
free_irq(10, &my_tasklet);
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
```
🧠 運作流程:
* 硬體觸發中斷 IRQ 10
* my_isr() 執行(上半部),排程 tasklet
* 核心稍後執行 my_tasklet_func()(下半部)
2. Busy Wait
```javascript=
for( n1=0; n1<10; n1++ )
{
printk(KERN_ALERT "n1=%d \r\n", n1 ); // 1Sec
j = jiffies + 1 * HZ;
while (jiffzies < j) ; // BusyWait
}
```
* jiffies 是一個不斷增長的全域變數,代表系統開機以來的「時脈 tick 數」。
* Busy Wait會讓CPU一直持續在等待,使用msleep()或者schedule_timeout()比較好
3. schedule()
```javascript=
for( n1=0; n1<10; n1++ )
{
printk(KERN_ALERT "n1=%d \r\n", n1 );
j = jiffies + 1 * HZ;
while (jiffies < j)
schedule(); // OS Ready/Running/Wait&Block
}
```
* schedule()執行會讓出 CPU 的使用權,讓其他可執行的 process 有機會被排程執行。
4. schedule_timeout
```javascript=
for( n1=0; n1<10; n1++ )
{
printk(KERN_ALERT "n1=%d \r\n", n1 );
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout( 1*HZ );
}
```
* schedule_timeout() 會自動計算超時,時間到即喚醒,不需要其他事件。
* 1*HZ: 一秒後自動喚醒
4. 延遲工作隊列(Workqueue)
```javascript=
DECLARE_WAIT_QUEUE_HEAD(joey_wait_queue_head);
for( n1=0; n1<10; n1++ )
{
printk(KERN_ALERT "n1=%d \r\n", n1 );
wait_event_timeout( joey_wait_queue_head , 0, 1 * HZ );
}
```
* 使用queue進行Delay,用來管理「哪些 process 正在等待」
* 1 秒後自動被 scheduler 喚醒,程式繼續執行。
CPU 釋放:在這 1 秒睡眠期間,CPU 可以去執行其他 process,不會 busy wait。
4. mdelay(短暫延遲)
```javascript=
for( n1=0; n1<10; n1++ )
{
printk(KERN_ALERT "n1=%d \r\n", n1 );
mdelay( 1000 );
}// 1 秒後執行
```
* mdelay() 會完全占用 CPU,在延遲期間不會切換到其他 process,不能在長時間延遲使用。
* 適用於短暫延遲(通常在微秒到幾毫秒範圍)
---
### Tasklet
Tasklet 是 Linux Kernel 的 底半部 (Bottom Half) 機制之一,用來延後執行中斷處理的工作,減少中斷上下文的執行時間。
特點:
* 運行在 軟體中斷 (soft interrupt) 上下文。
* 可以被中斷觸發,但比硬體中斷 (Top Half) 執行慢。
* 等硬體中斷結束後就會被呼叫
* 不能睡眠(sleep)。
* 適合做「簡單、快速」的延遲處理,例如:
* 累計資料
* 發送事件給使用者空間
* 設定旗標、喚醒等待隊列
#### Tasklet 使用範例
假設我們要在中斷服務例程 (ISR) 中延後處理:
```javascript=
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
static struct tasklet_struct my_tasklet;
void my_tasklet_func(unsigned long data)
{
printk(KERN_ALERT "Tasklet running! data=%ld\n", data);
}
irqreturn_t my_ISR(int irq, void *dev_id)
{
printk(KERN_ALERT "ISR triggered\n");
tasklet_schedule(&my_tasklet); // 延後處理
return IRQ_HANDLED;
}
static int __init my_module_init(void)
{
printk(KERN_ALERT "Init module\n");
tasklet_init(&my_tasklet, my_tasklet_func, 123);
// 這裡通常會 request_irq()
return 0;
}
static void __exit my_module_exit(void)
{
tasklet_kill(&my_tasklet);
printk(KERN_ALERT "Exit module\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
```
執行順序:
1. 中斷發生 → my_ISR 立即執行。
2. my_ISR 呼叫 tasklet_schedule() → Tasklet 加入隊列。
3. 中斷完成 → 軟中斷上下文執行 Tasklet。
4. Tasklet 執行 my_tasklet_func()。
### 中斷處理
接下來會詳細介紹中斷處理如何進行的,中斷是一種 **異步事件通知機制**。
當外部裝置(例如鍵盤、網卡、UART、GPIO)發生事件時,會透過 IRQ 線
(Interrupt ReQuest) 向 CPU 發送訊號。
CPU 收到後會:
* 暫停目前正在執行的工作;
* 保存現場 (context);
* 轉去執行一段特定的程式:中斷服務例程 (ISR, Interrupt Service Routine);
* 處理完畢後回到原本的程式繼續執行。
中斷處理的一個主要問題就是如何處理長時間的任務,中斷處理需要很快速地完成,且不能讓中斷阻塞太久。
Linux 為了解決這個問題把中斷處理分為兩個階段:
| 階段 | 名稱 | 執行位置 | 特性 | 範例 |
| ------- | ------------------- | --------- | -------- | ------------- |
| **上半部** | ISR (Top Half) | 中斷發生時立即執行 | 速度快、不能睡眠 | 讀取硬體暫存器、清中斷旗標 |
| **下半部** | Tasklet / Workqueue | 稍後再執行 | 可處理較長任務 | 資料處理、通知使用者空間 |
如果 ISR 處理花太多時間,系統即會發生:
* CPU 佔用太久;
* 其他中斷被延遲;
* 整體延遲上升。
👉 因此:
上半部只做必要的工作(如:清除硬體旗標);
下半部才做耗時的動作(如:拷貝資料、通知應用程式)。
* Tasklet / Workqueue為先前所介紹過的延遲執行功能
#### ISR (上半部) 的特性
```javascript
irqreturn_t my_isr(int irq, void *dev_id);
```
特性:
* 不可睡眠(不能呼叫可能阻塞的函式,例如 copy_to_user() 或 msleep())
* 執行時間要短
* 通常會排程一個下半部
#### 註冊與釋放中斷
註冊中斷
```javascript
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags, const char *name, void *dev);
```
| 參數 | 說明 |
| --------- | ------------------------------- |
| `irq` | 中斷號碼 (由硬體定義,如 GPIO IRQ、PCI IRQ) |
| `handler` | 安裝的處理 IRQ 函數指標 |
| `flags` | 例如 `IRQF_SHARED` (可共用 IRQ) |
| `name` | 出現在 `/proc/interrupts` |
| `dev` | 用來識別中斷來源的指標 (通常是裝置結構體) |
* 當中斷發生時會將 (void*)dev 指標傳給 IRQ 處理函數,通常是一個結構。
* 報告的中斷顯示在 `/proc/interrupts`
釋放中斷
```javascript
void free_irq(unsigned int irq, void *dev);
```
常見中斷旗標 (flags)
| 旗標 | 意義 |
| ---------------------- | ---------------------- |
| `IRQF_SHARED` | 可共用同一 IRQ (常見於 PCI 裝置) |
| `IRQF_TRIGGER_RISING` | 上升沿觸發 |
| `IRQF_TRIGGER_FALLING` | 下降沿觸發 |
| `IRQF_TRIGGER_HIGH` | 高電位觸發 |
| `IRQF_TRIGGER_LOW` | 低電位觸發 |
* 若是 GPIO 中斷,需搭配 gpio_to_irq(pin) 轉換。
#### 實際 IRQ 流程圖
```
硬體中斷發生
│
▼
CPU 暫停現有程序
│
▼
進入 ISR (Top Half)
│
├─ 清除硬體旗標
├─ 排程 tasklet/workqueue
▼
結束 ISR → 回到原程式
│
▼
tasklet/workqueue 執行 (Bottom Half)
```
#### 以 GPIO 為例所建立的範例
```javascript =
#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/slab.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Joey");
MODULE_DESCRIPTION("GPIO IRQ Example");
#define GPIO_NUM 17 // 假設使用 GPIO17 (依實際板子修改)
static int irq_num;
static struct tasklet_struct my_tasklet;
// Tasklet 底半部
static void my_tasklet_func(unsigned long data)
{
printk(KERN_INFO "Tasklet: 處理 GPIO IRQ 底半部, message=%s\n", (char *)data);
}
// ISR 上半部
static irqreturn_t my_gpio_isr(int irq, void *dev_id)
{
printk(KERN_INFO "ISR: GPIO 中斷觸發! IRQ=%d\n", irq);
tasklet_schedule(&my_tasklet); // 排程底半部
return IRQ_HANDLED;
}
static int __init my_gpio_irq_init(void)
{
int ret;
char *msg = kmalloc(32, GFP_KERNEL);
strcpy(msg, "Hello from GPIO IRQ");
// 1. 申請 GPIO
ret = gpio_request(GPIO_NUM, "my_gpio_irq");
if (ret) {
printk(KERN_ERR "gpio_request 失敗\n");
return ret;
}
// 2. 設為輸入模式
gpio_direction_input(GPIO_NUM);
// 3. 轉換成 IRQ 號碼
irq_num = gpio_to_irq(GPIO_NUM);
if (irq_num < 0) {
printk(KERN_ERR "gpio_to_irq 失敗\n");
gpio_free(GPIO_NUM);
return irq_num;
}
// 4. 初始化 Tasklet
tasklet_init(&my_tasklet, my_tasklet_func, (unsigned long)msg);
// 5. 註冊中斷 (上升沿觸發)
ret = request_irq(irq_num, my_gpio_isr, IRQF_TRIGGER_RISING, "gpio_irq_handler", NULL);
if (ret) {
printk(KERN_ERR "request_irq 失敗\n");
gpio_free(GPIO_NUM);
kfree(msg);
return ret;
}
printk(KERN_INFO "GPIO IRQ 模組載入成功: GPIO=%d, IRQ=%d\n", GPIO_NUM, irq_num);
return 0;
}
static void __exit my_gpio_irq_exit(void)
{
free_irq(irq_num, NULL);
tasklet_kill(&my_tasklet);
gpio_free(GPIO_NUM);
printk(KERN_INFO "GPIO IRQ 模組卸載完成\n");
}
module_init(my_gpio_irq_init);
module_exit(my_gpio_irq_exit);
```