このページの目次

割込みサブシステム

カバー範囲: irq_desc/irq_chip/irq_domain → 割込み処理フロー (トップハーフ/ボトムハーフ) → softirq/tasklet/workqueue → スレッド化された割込み (threaded IRQ) → MSI/MSI-X → APIC/IOMMU による割込みリマップ カーネルバージョン: 2.6 ~ 6.x

概要

割込みサブシステムは、カーネルにおいてハードウェアと最も密接に結合している部分です。ネットワークカードがパケットを受信したり、ディスクがDMAを完了したり、タイマーが期限切れになったりすると、ハードウェアは割込みコントローラ (APIC/GIC) を介してCPUに割込み信号を送信します。カーネルの割込みサブシステムは、以下の役割を担います:割込みを正しいCPUへルーティングする、対応するデバイスドライバのハンドラへ分散する、そして時間のかかる処理をボトムハーフ (下半部) へ延期する。

現代の割込みサブシステムの中心的な抽象化は3層で構成されます:

  • irq_chip: ハードウェア割込みコントローラ (IO-APIC, GIC など)
  • irq_domain: ハードウェア割込み番号からLinux IRQ番号へのマッピング
  • irq_desc: 各Linux IRQのディスクリプタ (ハンドラ、統計情報、アフィニティ)

ハードウェア割込み vs Linux IRQ

ハードウェア割込み番号 (hwirq):
  割込みコントローラ内部の割込みラインの番号
  異なるコントローラで同じhwirqが存在し得る

Linux IRQ番号 (virq):
  カーネルが統一する仮想割込み番号
  irq_domainを介してマッピングされる: virq = irq_domain->map(hwirq)
  /proc/interrupts で表示されるのはLinux IRQ番号

irq_desc

// include/linux/irqdesc.h
struct irq_desc {
    struct irq_common_data  irq_common_data;
    struct irq_data         irq_data;
    unsigned int __percpu   *kstat_irqs;     // per-CPU割込みカウント (/proc/interrupts用)
    irq_flow_handler_t      handle_irq;      // 上位レベルのハンドラ (handle_fasteoi など)
    struct irqaction        *action;         // ドライバが登録したハンドラのリスト
    unsigned int            depth;           // 無効化カウンター
    unsigned int            istate;          // 内部状態
};

irqaction: ドライバが登録したハンドラ

// include/linux/interrupt.h
struct irqaction {
    irq_handler_t           handler;         // ドライバが提供するハンドラ
    unsigned long           flags;           // IRQF_SHARED, IRQF_ONESHOT など
    const char              *name;           // /proc/interrupts で表示される名前
    void                    *dev_id;         // デバイス識別子 (共有割込みでは一意である必要がある)
    struct irqaction        *next;           // 共有割込みの次のアクション
};

割込み処理フロー

ハードウェアからカーネルへ

ハードウェア割込みからカーネルへ: デバイスがラインを引いてhandle_irq_desc()に入るまでの3ステップフロー ① デバイスが割込みラインを引く → 割込みコントローラ(APIC / GIC) ② 割込みコントローラが割込みメッセージを送信 → CPU(APICバス / MSI経由) ③ CPUが割込みに応答 ・ ハードウェアがRIP / CS / RFLAGSを保存(x86: スタックへpush) ・ カーネルスタックへ切り替え(ユーザー空間から入った場合) ・ IDT内のエントリ(entry_SYSCALL_64の対応するエントリ)へジャンプ common_interrupt() do_IRQ() handle_irq_desc() handle_irq_desc()への3ステップ: コンテキストの保存 → エントリの特定 → コアへの委譲。その後、割込みのトリガタイプに応じて分流する(下図参照)。

handle_irq_desc() パス

handle_irq_desc() パス: 割込みのトリガタイプに応じて対応するフローハンドラへ分流 handle_irq_desc(desc) generic_handle_irq_desc(desc) desc->handle_irq(desc) 上位フローハンドラ handle_fasteoi(desc) 最も一般的 ・ レベルトリガ mask_irq(desc) 本割込みをマスク(無効化) handle_irq_event(desc) actionリストを走査 → action->handler() IRQF_WAKE_THREAD → カーネルスレッドをウェイクアップ unmask_irq(desc) 復元 handle_edge_irq(desc) エッジトリガ ack_irq(desc) → handle_irq_event() → … handle_fasteoiは最も一般的なパス(レベルトリガ): まず割込みをマスク → 登録された全actionを走査してハンドラを呼び出し → 復元; もしあるactionがIRQF_WAKE_THREADを要求する場合、対応するカーネルスレッドをウェイクアップして下半部を実行します。

共有割込み

IRQF_SHARED: 複数のデバイスが同じ割込みラインを共有
  → 登録された全ハンドラが順に呼び出される
  → 各ハンドラは「自分のデバイスからの割込みかどうか」を確認する
     (デバイスレジスタを読み取り、一致しない場合は → IRQ_NONEを返す)
  → 全ハンドラがIRQ_NONEを返す → 誤割込み (spurious interrupt)
     + spuriousカウントを増加 → 100k回継続 → この割込みを無効化

ボトムハーフ (下半部)

割込みハンドラは最も緊急の作業のみを行う (割込みの確認、デバイスのマスク、データのコピー)。時間のかかる処理 (プロトコルスタックの処理、キャッシュへの書き戻し) はボトムハーフに委ねる。

softirq

// kernel/softirq.c
// 事前に定義されたsoftirqの種類 (コンパイル時に決定):
enum {
    HI_SOFTIRQ    = 0,   // 高優先度tasklet
    TIMER_SOFTIRQ = 1,   // タイマー期限切れ
    NET_TX_SOFTIRQ = 2,  // ネットワーク送信
    NET_RX_SOFTIRQ = 3,  // ネットワーク受信 (NAPI)
    BLOCK_SOFTIRQ = 4,   // ブロック層
    TASKLET_SOFTIRQ = 6, // 通常のtasklet
    RCU_SOFTIRQ   = 9,   // RRCUコールバック
    NR_SOFTIRQS
};

// softirqは割込みコンテキストで実行される (ただし割込み有効)
// irq_exit() によってチェックがトリガされる:
//   if (in_interrupt()) return;  // ネストしたハードウェア割込み中でない場合
//   do_softirq() → __do_softirq() → softirqビットマップを走査

// ksoftirqd: per-CPUカーネルスレッド
//   softirqの処理が多すぎる場合 (MAX_SOFTIRQ_RESTART) → ksoftirqdへ委譲
//   → CPUをユーザープロセスへ譲渡

tasklet (廃止済み、threaded IRQに置き換え)

// kernel/softirq.c
// taskletはsoftirq (HI_SOFTIRQ または TASKLET_SOFTIRQ) をベースにする
// 同種のtaskletは同時に実行されない (同期を簡素化)
// ⚠️ 2022以降のカーネルでは廃止: 新コードではthreaded IRQを使用

struct tasklet_struct {
    void (*func)(unsigned long);
    unsigned long data;
};

Threaded IRQ: 現在推奨される方法

// kernel/irq/manage.c
// 各割込みごとに独立したカーネルスレッドを持つことができる
// ハンドラが IRQ_WAKE_THREAD を返すと、カーネルは threaded_fn の実行をスケジューリングする

request_threaded_irq(irq, handler, thread_fn, flags, name, dev);
//   handler: ハードウェア割込みコンテキストで実行 (最小限の作業のみ)
//   thread_fn: 独立したカーネルスレッド内で実行 (スリープ可能、mutex取得可能)
//
// 利点:
//   - 割込み無効時間の削減
//   - 下半部でスリープ可能 (mutex取得、IO実行)
//   - リアルタイムスケジューリングポリシーで管理可能

Workqueue: より重い下半部

// kernel/workqueue.c
// workqueueはプロセスコンテキスト (カーネルスレッド kworker) で実行される
// 適している用途: スリープが必要、長時間実行される作業

// tasklet/softirqとの主な違い:
//   workqueue: プロセスコンテキスト、スリープ可能
//   tasklet/softirq: 割込みコンテキスト、スリープ不可

schedule_work(&work);          // システムworkqueue
queue_work(my_wq, &work);     // 専用workqueue

MSI / MSI-X

// kernel/irq/msi.c
// メッセージシグナルド割込み (Message Signaled Interrupts)
// デバイスが直接メモリアドレスへ書き込み → 割込みコントローラ (割込みラインを経由しない)
// 利点:
//   - 共有不要 (各MSIに一意のアドレスがある)
//   - より高性能 (PCIeのposted write vs INTxの低速なレベル信号)
//   - MSI-Xではデバイスが複数の割込みベクタを持てる (例: NVMeはキューごとに1つ)

// PCIデバイスでMSIを有効化:
pci_alloc_irq_vectors(pdev, 1, 16, PCI_IRQ_MSI);
  → デバイスが16個のMSIベクタを割り当て
  → Linux IRQ番号を返す (各ベクタごとに1つ)

割込みアフィニティと負荷分散

# 割込みの分布を確認
cat /proc/interrupts

# 割込みアフィニティの設定 (どのCPUがどの割込みを処理するか)
echo 2 > /proc/irq/<N>/smp_affinity  # CPU 1にバインド

# irqbalanceデーモンが自動でバランスを取る
#   割込みカウントとCPU負荷に基づいて判断
#   特定の割込み (例: NVMe) はNUMAローカルCPUに直接バインドされることがある

参考と拡張

  • カーネルドキュメント⁠: Documentation/core-api/irq/, Documentation/PCI/msi-howto.rst
  • LWN: "The design of the interrupt subsystem", "Threaded interrupt handlers"
  • ソースコード⁠:
    • kernel/irq/ — 割込みコア (handle, manage, chip, domain, msi)
    • kernel/softirq.c — softirqとtasklet
    • kernel/workqueue.c — workqueue
    • arch/x86/kernel/irq.c — x86割込みエントリ
    • arch/x86/kernel/apic/ — APICドライバ

キーワード: irq_desc, irq_chip, irq_domain, softirq, tasklet, threaded IRQ, workqueue, MSI/MSI-X, interrupt affinity