為什麼中斷如此關鍵#

中斷是硬體告訴 CPU「有事發生」的唯一方式:網卡收到封包、磁碟寫完、計時器到了、鍵盤按鍵。沒有中斷,CPU 必須輪詢(polling)── 浪費 CPU、延遲糟。但中斷會打斷正在跑的任何東西,所以核心對中斷處理有嚴格的紀律與分層。

從硬體到 CPU 的路徑#

設備    ──────► PIC / GIC / APIC ──────► CPU
                  (interrupt controller)

設備透過 IRQ line(或 MSI 訊息)通知中斷控制器,控制器決定送給哪顆 CPU。CPU 收到後:

  1. 暫停當前指令
  2. 切到 kernel mode(如果原來在 user mode)
  3. 把當前狀態存到 stack
  4. 跳到中斷向量表對應位置
  5. 執行核心的中斷入口程式碼,最終呼叫該 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_ONESHOTthreaded IRQ 用,handler 完成前不重觸發

dev_id 在 shared IRQ 時必須是唯一指標(通常給 device 結構),用於 free_irq 找對人。

中斷處理的鐵律#

  1. 不能睡眠:不能呼叫 mutex_lockkmalloc(GFP_KERNEL)copy_from_user、任何 wait_event
  2. 儘量短:handler 跑越久,其他中斷就被擋越久(中斷期間是禁中斷的)
  3. 不能被 preempt:reschedule 不會發生
  4. 可能在任意 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 IRQworkqueue。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)的解法:

  1. 第一個封包觸發 IRQ
  2. handler 把該 NIC 加進 poll list、關閉該 NIC 的中斷
  3. softirq 在合理時間內 poll 走所有可用封包(最多 budget 個)
  4. 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 級。

小結#

中斷的設計層次:

  1. 硬體:interrupt controller 路由
  2. Top-half:極短、禁中斷、ack 硬體
  3. Bottom-half:可延後執行的部分
    • softirq:核心子系統用,最低延遲
    • tasklet:舊驅動慣用,逐步淘汰中
    • workqueue:可睡眠,現代驅動首選
    • threaded IRQ:最簡潔的「可睡眠中斷」

設計驅動時的決策樹:

我的 handler 需要睡眠嗎?
  ├ 是 → workqueue 或 threaded IRQ
  └ 否 → 中斷會極頻繁嗎?
         ├ 是 → 採用 NAPI / softirq 模式
         └ 否 → tasklet(或直接 hard IRQ 做完)