為什麼中斷如此關鍵#
中斷是硬體告訴 CPU「有事發生」的唯一方式:網卡收到封包、磁碟寫完、計時器到了、鍵盤按鍵。沒有中斷,CPU 必須輪詢(polling)── 浪費 CPU、延遲糟。但中斷會打斷正在跑的任何東西,所以核心對中斷處理有嚴格的紀律與分層。
從硬體到 CPU 的路徑#
設備 ──────► PIC / GIC / APIC ──────► CPU
(interrupt controller)設備透過 IRQ line(或 MSI 訊息)通知中斷控制器,控制器決定送給哪顆 CPU。CPU 收到後:
- 暫停當前指令
- 切到 kernel mode(如果原來在 user mode)
- 把當前狀態存到 stack
- 跳到中斷向量表對應位置
- 執行核心的中斷入口程式碼,最終呼叫該 IRQ 註冊的 handler
近代 x86 用 APIC(Advanced PIC),ARM 用 GIC(Generic Interrupt Controller)。它們提供更多 IRQ 線、更靈活的優先權與目標 CPU 控制。
註冊中斷處理函式#
#include <linux/interrupt.h>
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
/* 處理中斷 */
return IRQ_HANDLED; // 或 IRQ_NONE(不是我的)
}
int err = request_irq(irq, my_irq_handler,
IRQF_SHARED, "my-device", dev);
if (err) ...
/* 解除註冊(卸載前必做) */
free_irq(irq, dev);關鍵 flag:
| flag | 含義 |
|---|---|
IRQF_SHARED | 允許多個 device 共用同一條 IRQ 線 |
IRQF_TRIGGER_RISING/FALLING/HIGH/LOW | 觸發方式(GPIO 中斷常用) |
IRQF_NOBALANCING | 不要被 IRQ balancer 搬到別的 CPU |
IRQF_PERCPU | 每 CPU 獨立的 IRQ |
IRQF_ONESHOT | threaded IRQ 用,handler 完成前不重觸發 |
dev_id 在 shared IRQ 時必須是唯一指標(通常給 device 結構),用於 free_irq 找對人。
中斷處理的鐵律#
- 不能睡眠:不能呼叫
mutex_lock、kmalloc(GFP_KERNEL)、copy_from_user、任何wait_event - 儘量短:handler 跑越久,其他中斷就被擋越久(中斷期間是禁中斷的)
- 不能被 preempt:reschedule 不會發生
- 可能在任意 CPU 上:與某個行程的 mm 無關,task context 也不確定
只允許用:
- spinlock(
_irqsave變體) kmalloc(GFP_ATOMIC)- atomic op、per-CPU 變數
printk(雖可用,但持續輸出會拖慢系統)- 喚醒 wait queue / scheduler 把工作交給後續處理
Top-half 與 bottom-half#
中斷處理慣例上分兩段:
┌──────────────────────────┐
│ 硬體中斷觸發 │
│ │ │
│ ▼ │
│ Top-half (hard IRQ) │ ← 短、禁中斷、做最緊急的事
│ - ack 硬體 │
│ - 抓必要資料 │
│ - 排程 bottom-half │
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ Bottom-half │ ← 較長、開中斷、可被搶占
│ (softirq/tasklet/wq) │
│ - 解析資料 │
│ - 喚醒等待者 │
│ - 執行協定處理 │
└──────────────────────────┘把「能延後就延後」的工作丟到 bottom-half,可以讓 top-half 極短,整體中斷延遲較低。
softirq#
最古老、最低延遲的 bottom-half 機制。系統內靜態定義幾個 softirq:
enum {
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ,
NR_SOFTIRQS
};特性:
- 不能睡眠(與硬中斷一樣)
- 每 CPU 獨立執行,但同一個 softirq 在不同 CPU 上可以併發 ── handler 必須 SMP-safe
- 由硬中斷返回前、或
ksoftirqd背景 thread 執行
新增 softirq 需要修改核心列舉,所以驅動不會自己加 ── 它們用 tasklet 或 workqueue。
tasklet#
建構在 TASKLET_SOFTIRQ 之上的便利機制:
static void my_tasklet_func(struct tasklet_struct *t)
{
/* 處理 */
}
DECLARE_TASKLET(my_tasklet, my_tasklet_func);
// 在 IRQ handler 裡:
tasklet_schedule(&my_tasklet);特性:
- 同一個 tasklet 不會在多 CPU 同時跑(softirq 才會)── handler 不需自我同步
- 不能睡眠
- 比 softirq 簡單:不用申請 softirq number
tasklet 在 5.x 後逐漸被 deprecation pressure 推著淘汰。新驅動建議用 threaded IRQ 或 workqueue。tasklet 仍存在但有歷史包袱(不能睡眠、API 不太一致)。
Workqueue#
可睡眠的 bottom-half ── 把工作交給 kernel worker thread 執行:
static void my_work_func(struct work_struct *work)
{
/* 可睡眠:可以做 mutex_lock、kmalloc(GFP_KERNEL)、I/O */
}
DECLARE_WORK(my_work, my_work_func);
// 排到系統 workqueue
schedule_work(&my_work);
// 或自己建一個 workqueue
struct workqueue_struct *wq = alloc_workqueue("my_wq", WQ_UNBOUND, 0);
queue_work(wq, &my_work);
destroy_workqueue(wq);alloc_workqueue flags:
| flag | 行為 |
|---|---|
WQ_UNBOUND | 不綁定特定 CPU,調度更彈性 |
WQ_FREEZABLE | 系統 suspend 時凍結 |
WQ_MEM_RECLAIM | 保證至少有一個 worker 可用,防止記憶體緊張時死鎖 |
WQ_HIGHPRI | 高優先 worker pool |
延遲版本:
DECLARE_DELAYED_WORK(my_dwork, my_work_func);
schedule_delayed_work(&my_dwork, msecs_to_jiffies(100));Threaded IRQ#
最簡潔的「可睡眠中斷處理」:
static irqreturn_t my_hard_handler(int irq, void *dev_id)
{
/* 短:ack、抓資料 */
return IRQ_WAKE_THREAD; // 喚醒 thread fn
}
static irqreturn_t my_thread_fn(int irq, void *dev_id)
{
/* 可睡眠:在 kernel thread 上跑 */
return IRQ_HANDLED;
}
request_threaded_irq(irq, my_hard_handler, my_thread_fn,
IRQF_ONESHOT, "my-dev", dev);或省略 hard handler,全交給 thread:
request_threaded_irq(irq, NULL, my_thread_fn,
IRQF_ONESHOT, "my-dev", dev);IRQF_ONESHOT 確保 thread 跑完前不會再次觸發 ── 否則可能 race。
對複雜驅動,threaded IRQ 大幅簡化設計:handler 可以呼叫 mutex、做 I2C/SPI 通訊、配置記憶體 ── 都不會出問題。
softirq vs tasklet vs workqueue 比較#
| 機制 | 可睡眠 | 同 CPU 併發 | 跨 CPU 併發 | 延遲 | 適用 |
|---|---|---|---|---|---|
| Softirq | ✗ | ✗ | ✓ | 最低 | net、block 核心子系統 |
| Tasklet | ✗ | ✗ | ✗ | 低 | 舊驅動(建議改用其他) |
| Workqueue | ✓ | ✓ (per cpu) | ✓ | 較高 | 大多數驅動 |
| Threaded IRQ | ✓ | - | - | 中 | 結構清晰的驅動 |
中斷負載平衡#
預設 IRQ 由特定 CPU 處理,可能不平衡 ── 一顆 CPU 被特定設備中斷打爆。Linux 用兩種機制處理:
irqbalance#
userspace daemon,定期讀 /proc/interrupts,把 IRQ affinity 改寫到較閒的 CPU。預設裝在多數發行版。
手動調整#
cat /proc/interrupts # 各 IRQ 在各 CPU 的計數
echo 4 > /proc/irq/<N>/smp_affinity # 把 IRQ N 綁到 CPU 2 (bit 2)
echo 0-3 > /proc/irq/<N>/smp_affinity_list # 同上,list 形式對網路熱衷的 server 常會:
- 把 NIC 中斷分散到不同 CPU
- 用 RSS(Receive Side Scaling)讓硬體本身就把封包散到多個 queue
- 開啟 RPS / RFS 在軟體層加把勁
NMI#
Non-Maskable Interrupt,連 cli / 關中斷都擋不住。用於:
- 看門狗(watchdog):核心卡死也能被中斷
- Perf 採樣:硬體 PMU 觸發 NMI 抓 stack
- 嚴重硬體錯誤通知
NMI handler 限制更嚴:不能用一般 spinlock(因為原來的 holder 可能就是被你打斷的人)。要用 printk_safe_* 等特殊 API。
觀測與除錯#
cat /proc/interrupts # 各 IRQ 計數
watch -n1 cat /proc/interrupts # 動態看哪個被觸發
cat /proc/softirqs # softirq 計數
mpstat -I CPU 1 # 各 CPU 的中斷率
sar -I XALL 1 # 各中斷詳細/proc/interrupts 範例:
CPU0 CPU1 CPU2 CPU3
0: 32 0 0 0 IO-APIC 2-edge timer
1: 9 0 0 0 IO-APIC 1-edge i8042
24: 0 0 0 0 PCI-MSI 524288-edge enp0s31f6
25: 1235801 2354 0 1010 PCI-MSI 524289-edge enp0s31f6-rx-0最後一欄是名稱,可看出哪個設備。中斷數量極大(每秒幾萬以上)時需要關注,可能該開啟 NAPI/中斷合併。
NAPI ── 網路中斷的通用處理#
高速網卡每秒可能收幾百萬封包,每個都中斷 ── CPU 全在跑 IRQ handler 沒空處理封包,這叫 livelock。NAPI(New API)的解法:
- 第一個封包觸發 IRQ
- handler 把該 NIC 加進 poll list、關閉該 NIC 的中斷
- softirq 在合理時間內 poll 走所有可用封包(最多 budget 個)
- queue 空了才重新開啟中斷
效果:高負載時退化為 polling(吞吐極好),低負載時保留 IRQ(延遲極低)。
計時器:低解析度 vs 高解析度#
// 低解析度(jiffies tick)
static struct timer_list my_timer;
timer_setup(&my_timer, my_timer_fn, 0);
mod_timer(&my_timer, jiffies + HZ); // 1 秒後
// 高解析度(hrtimer)
static struct hrtimer my_hrt;
hrtimer_init(&my_hrt, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
my_hrt.function = my_hrt_fn;
hrtimer_start(&my_hrt, ms_to_ktime(5), HRTIMER_MODE_REL);低解析度 timer 精度 = 1 / HZ(典型 1ms 或 4ms),實作便宜。hrtimer 用硬體計時器(HPET / TSC deadline / arm_arch_timer),可達 ns 級。
小結#
中斷的設計層次:
- 硬體:interrupt controller 路由
- Top-half:極短、禁中斷、ack 硬體
- Bottom-half:可延後執行的部分
- softirq:核心子系統用,最低延遲
- tasklet:舊驅動慣用,逐步淘汰中
- workqueue:可睡眠,現代驅動首選
- threaded IRQ:最簡潔的「可睡眠中斷」
設計驅動時的決策樹:
我的 handler 需要睡眠嗎?
├ 是 → workqueue 或 threaded IRQ
└ 否 → 中斷會極頻繁嗎?
├ 是 → 採用 NAPI / softirq 模式
└ 否 → tasklet(或直接 hard IRQ 做完)