Skilore

割り込みとDMA — CPU・デバイス間通信の全体像

**割り込み (Interrupt)** はCPUと外部デバイスが非同期に通信するための根幹メカニズムであり、**DMA (Direct Memory Access)** はCPUの介在なしに高速なデータ転送を実現する技術である。この2つを正しく理解することは、OSカーネル開発、デバイスドライバ設計、組み込みシステム構築、さらにはパフォーマンスチューニングの全てにおいて不可欠である。

108 分で読めます53,955 文字

割り込みとDMA — CPU・デバイス間通信の全体像

割り込み (Interrupt) はCPUと外部デバイスが非同期に通信するための根幹メカニズムであり、DMA (Direct Memory Access) はCPUの介在なしに高速なデータ転送を実現する技術である。この2つを正しく理解することは、OSカーネル開発、デバイスドライバ設計、組み込みシステム構築、さらにはパフォーマンスチューニングの全てにおいて不可欠である。


この章で学ぶこと

  • 割り込みの分類(ハードウェア割り込み・ソフトウェア割り込み・例外)を体系的に理解する
  • 割り込みベクタテーブル (IDT) の構造とルックアップ手順を説明できる
  • Linux カーネルにおけるトップハーフ/ボトムハーフの分離設計を理解する
  • DMA転送の初期化・実行・完了通知の一連の流れを追跡できる
  • スキャッタ/ギャザーDMAとIOMMUの役割を説明できる
  • MSI/MSI-X、RDMA、NVMe など現代のI/O技術の位置づけを把握する
  • 割り込みアフィニティやIRQバランシングによる性能最適化を実践できる
  • 代表的なアンチパターンを認識し、回避策を設計できる

前提知識

このガイドを読む前に、以下の知識があると理解が深まります:

  • 基本的なプログラミングの知識
  • 関連する基礎概念の理解
  • デバイスドライバ の内容を理解していること

全体アーキテクチャ俯瞰図

User Space
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
App (A)App (B)App (C)App (D)
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
syscallsyscallread()write()
▼ ▼ ▼ ▼
┌──────────────────────────────────────────────────────────┐
VFS (Virtual File System)
└──────────────────────┬───────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
Block / Character Device Layer
┌────────────┐ ┌─────────────┐ ┌──────────────┐
I/OInterruptDMA
SchedulerHandlerEngine
└─────┬──────┘ └──────┬──────┘ └──────┬───────┘
└────────┼────────────────┼────────────────┼───────────────┘
▼ ▼ ▼
┌──────────────────────────────────────────────────────────┐
Hardware Abstraction Layer
┌──────┐ ┌───────┐ ┌────────┐ ┌──────────┐
PIC/APICIOMMUDMA
8259Controller
└──┬───┘ └───┬───┘ └───┬────┘ └────┬─────┘
└──────┼──────────┼──────────┼────────────┼───────────────┘
▼ ▼ ▼ ▼
Kernel Space
Hardware Bus (PCIe / AHB / AXI)
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
KeyboardNICNVMeGPUTimer
SSD
└────────┘ └────────┘ └────────┘ └────────┘ └────────┘

この図が示す通り、ユーザ空間のアプリケーションがI/Oリクエストを発行すると、VFSを経由してデバイス層に到達し、最終的にハードウェアとの通信は割り込みとDMAによって制御される。本章ではこの一連の仕組みを深く掘り下げる。


1. 割り込みの基礎概念

1.1 なぜ割り込みが必要なのか

CPUとI/Oデバイスの間には、桁違いの速度差が存在する。

動作 所要サイクル(概算) 所要時間(概算)
CPUレジスタアクセス 1 サイクル 0.3 ns
L1キャッシュヒット 4 サイクル 1.2 ns
L3キャッシュヒット 40 サイクル 12 ns
メインメモリアクセス 200 サイクル 60 ns
SSD (NVMe) 読み取り 10-100 us
HDD シーク 3-10 ms
ネットワーク往復 (LAN) 0.1-1 ms
ネットワーク往復 (WAN) 10-100 ms

もし割り込みがなければ、CPUはデバイスの準備完了を繰り返しチェックする ポーリング (Polling) を行うしかない。ポーリングでは、デバイスが応答するまでCPUサイクルが無駄に消費される。

ポーリング vs 割り込み:

  [ポーリング方式]
  CPU: チェック → 未完了 → チェック → 未完了 → ... → 完了 → 処理
       ^^^^^^^^   ^^^^^^^^   ^^^^^^^^
       CPUサイクル浪費(ビジーウェイト)

  [割り込み方式]
  CPU: I/O要求発行 → 他タスク実行 → ... → 割り込み受信 → 処理
                     ^^^^^^^^^^^^^^^^^^^^^
                     CPUを有効活用

ただし、割り込みが万能というわけではない。高頻度I/O(10Gbps NIC でのパケット受信など)では、割り込みのオーバーヘッド自体がボトルネックになる場合がある。この問題に対しては、後述するポーリングモード(NAPI)やハイブリッド方式で対処する。

1.2 割り込みの3大分類

割り込みは発生源と性質に基づいて3種類に分類される。

割り込みの分類体系
┌─────────────────────────────────────────────┐
1. ハードウェア割り込み(外部割り込み)
発生源: 外部デバイス
┌────────────────┬───────────────────┐
マスカブルノンマスカブル
(INTR)(NMI)
CLI命令で無視不可
禁止可能メモリパリティ
キーボードエラー、ウォッチ
ディスク完了ドッグタイマー
NIC受信ハードウェア障害
└────────────────┴───────────────────┘
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
2. ソフトウェア割り込み(トラップ)
発生源: プログラム命令
- INT n 命令 (x86: int 0x80)
- SYSCALL / SYSENTER 命令
- SVC 命令 (ARM)
- デバッグブレークポイント (INT 3)
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
3. 例外 (Exception)
発生源: CPU内部
┌──────────┬──────────┬──────────┐
FaultTrapAbort
再実行可次命令復帰不可
ページオーバーダブル
フォルトフローフォルト
GPFデバッグマシン
トラップチェック
└──────────┴──────────┴──────────┘
└─────────────────────────────────────────────┘

例外のサブ分類の詳細

分類 戻り先 復帰可能性 代表例 x86ベクタ番号
Fault 例外発生命令 再実行可能 ページフォルト (#PF) 14
Fault 例外発生命令 再実行可能 一般保護例外 (#GP) 13
Fault 例外発生命令 再実行可能 ゼロ除算 (#DE) 0
Trap 次の命令 続行可能 ブレークポイント (#BP) 3
Trap 次の命令 続行可能 オーバーフロー (#OF) 4
Abort なし 復帰不可 ダブルフォルト (#DF) 8
Abort なし 復帰不可 マシンチェック (#MC) 18

1.3 x86/x86-64 割り込みベクタテーブル

x86アーキテクチャでは、割り込みは IDT (Interrupt Descriptor Table) を通じてハンドラにディスパッチされる。IDTは最大256エントリを持ち、各エントリがハンドラのアドレスとその属性を定義する。

x86-64 IDTエントリ構造 (16バイト / Gate Descriptor):

  ビット位置    フィールド
127:96 Reserved (上位DWORD)
95:64 Offset[63:32]
63:48 Offset[31:16]
47 P (Present bit)
46:45 DPL (特権レベル 0-3)
44 0 (固定)
43:40 Gate Type
0xE = Interrupt Gate
0xF = Trap Gate
39:35 Reserved
34:32 IST (Interrupt Stack Table)
31:16 Segment Selector
15:0 Offset[15:0]
IDTRレジスタ:
  ┌──────────────────────────────┐
  │ Base Address (64bit) │ Limit │  ← LIDT命令でロード
  └──────────────────────────────┘

Interrupt Gate と Trap Gate の違い:

  • Interrupt Gate: ハンドラ実行時にIFフラグ(割り込み許可フラグ)を自動的にクリア。以降の割り込みが禁止される
  • Trap Gate: IFフラグを変更しない。ハンドラ実行中も割り込みが受け付けられる

この違いにより、ハードウェア割り込みハンドラには通常 Interrupt Gate を使い、ソフトウェア割り込み(システムコール)には Trap Gate を使うのが一般的である。


2. 割り込み処理の詳細フロー

2.1 ハードウェア割り込みの発生から復帰まで

Device          PIC/APIC           CPU                   Memory
   │                │                │                      │
   │─IRQ信号──────→│                │                      │
   │                │─INTR信号────→│                      │
   │                │                │                      │
   │                │                │◆ 現在の命令を完了    │
   │                │                │                      │
   │                │                │◆ RFLAGS, CS, RIP    │
   │                │                │  をスタックにPUSH ──→│
   │                │                │                      │
   │                │←INTA(確認応答)─│                      │
   │                │                │                      │
   │                │─ベクタ番号───→│                      │
   │                │                │                      │
   │                │                │◆ IDT[ベクタ番号]     │
   │                │                │  からハンドラ取得 ←──│
   │                │                │                      │
   │                │                │◆ 特権レベル確認      │
   │                │                │  Ring3→Ring0ならTSS  │
   │                │                │  からRSP0をロード    │
   │                │                │                      │
   │                │                │◆ ハンドラ実行開始    │
   │                │                │  (トップハーフ)       │
   │                │                │                      │
   │                │                │◆ EOI (End of        │
   │                │←EOI送信───────│  Interrupt) 送信     │
   │                │                │                      │
   │                │                │◆ IRET命令で復帰     │
   │                │                │  RIP, CS, RFLAGS    │
   │                │                │  をPOPして元の処理へ │
   │                │                │                      │

2.2 コード例: x86-64 割り込みハンドラの骨格(C + インラインアセンブリ)

以下は、Linux カーネル風の割り込みハンドラ登録と実装の模式コードである。

/* コード例1: x86-64 割り込みハンドラの基本構造 */
 
#include <linux/interrupt.h>
#include <linux/module.h>
 
#define MY_DEVICE_IRQ  11
 
/* 割り込みコンテキスト情報 */
struct my_device_data {
    unsigned long irq_count;
    spinlock_t    lock;
    void __iomem *base_addr;
    /* デバイス固有のリングバッファ等 */
};
 
/*
 * トップハーフ: 割り込みハンドラ本体
 * - 割り込み禁止状態で実行される
 * - 最小限の処理のみ行う
 * - スリープ禁止(GFP_ATOMIC のみ使用可)
 */
static irqreturn_t my_device_isr(int irq, void *dev_id)
{
    struct my_device_data *data = dev_id;
    u32 status;
 
    /* デバイスの割り込みステータスレジスタを読み取り */
    status = ioread32(data->base_addr + STATUS_REG);
 
    /* 自デバイスの割り込みか確認(共有IRQ対応) */
    if (!(status & MY_DEVICE_IRQ_PENDING))
        return IRQ_NONE;  /* 他デバイスの割り込み */
 
    /* デバイス側の割り込みをクリア(ACK) */
    iowrite32(status, data->base_addr + STATUS_REG);
 
    spin_lock(&data->lock);
    data->irq_count++;
    spin_unlock(&data->lock);
 
    /* ボトムハーフをスケジュール */
    tasklet_schedule(&my_device_tasklet);
 
    return IRQ_HANDLED;
}
 
/*
 * ボトムハーフ: 遅延処理
 * - 割り込み有効状態で実行される
 * - 比較的長い処理が可能
 */
static void my_device_tasklet_fn(unsigned long arg)
{
    struct my_device_data *data = (struct my_device_data *)arg;
 
    /* 受信データの処理、バッファコピーなど */
    process_received_data(data);
 
    /* ユーザ空間への通知 */
    wake_up_interruptible(&data->wait_queue);
}
 
DECLARE_TASKLET(my_device_tasklet, my_device_tasklet_fn, 0);
 
/*
 * デバイス初期化時にIRQを登録
 */
static int my_device_probe(struct pci_dev *pdev,
                           const struct pci_device_id *id)
{
    struct my_device_data *data;
    int ret;
 
    data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;
 
    spin_lock_init(&data->lock);
 
    /*
     * request_irq() のフラグ:
     *   IRQF_SHARED   - 他デバイスとIRQ共有可能
     *   IRQF_ONESHOT  - threaded IRQ でワンショット
     */
    ret = request_irq(pdev->irq, my_device_isr,
                      IRQF_SHARED, "my_device", data);
    if (ret) {
        dev_err(&pdev->dev, "IRQ %d の登録に失敗: %d\n",
                pdev->irq, ret);
        return ret;
    }
 
    dev_info(&pdev->dev, "IRQ %d を登録\n", pdev->irq);
    return 0;
}
 
/*
 * デバイス除去時にIRQを解放
 */
static void my_device_remove(struct pci_dev *pdev)
{
    struct my_device_data *data = pci_get_drvdata(pdev);
    free_irq(pdev->irq, data);
}

2.3 割り込みコントローラの進化

割り込みコントローラの世代変遷
第1世代: 8259A PIC (1980s)
┌─────────────────────────────────────────┐
Master 8259A ──── Slave 8259A
IRQ0: Timer IRQ8: RTC
IRQ1: Keyboard IRQ9: Redirect
IRQ2: → Cascade IRQ10: (空き)
IRQ3: COM2 IRQ11: (空き)
IRQ4: COM1 IRQ12: PS/2 Mouse
IRQ5: LPT2/Sound IRQ13: FPU
IRQ6: Floppy IRQ14: Primary IDE
IRQ7: LPT1 IRQ15: Secondary IDE
制約: 最大15本のIRQ、優先順位固定
└─────────────────────────────────────────┘
第2世代: APIC (1990s - 現在)
┌─────────────────────────────────────────┐
I/O APIC ←→ System Bus ←→ Local APIC
(CPU毎に1個)
改善点:
- 224本のIRQベクタ (32-255)
- マルチプロセッサ対応
- プログラマブル優先度
- 特定CPUへの割り込み配送
└─────────────────────────────────────────┘
第3世代: MSI / MSI-X (2000s - 現在)
┌─────────────────────────────────────────┐
デバイスがメモリ書き込みで割り込みを通知
- 専用のIRQピン不要
- MSI: 最大32ベクタ
- MSI-X: 最大2048ベクタ
- PCIeデバイスの標準方式
- NVMe, 高速NIC の必須機能
└─────────────────────────────────────────┘

3. Linux カーネルのトップハーフ/ボトムハーフ設計

3.1 設計原則

割り込みハンドラ(トップハーフ)の実行中は、同じIRQラインの割り込みが禁止される。これにより、ハンドラが長時間実行されると、後続の割り込みが失われたり、システム全体の応答性が低下したりする。

この問題を解決するために、Linux カーネルは割り込み処理を2段階に分離する。

特性 トップハーフ ボトムハーフ
実行コンテキスト 割り込みコンテキスト softirq/tasklet: 割り込みコンテキスト, workqueue: プロセスコンテキスト
割り込み状態 同一IRQ禁止 割り込み有効
スリープ可否 不可 workqueue のみ可
メモリ確保 GFP_ATOMIC のみ workqueue なら GFP_KERNEL 可
実行タイミング 即座 遅延(ただし softirq は高速)
典型的な処理 ACK送信、フラグ設定、ボトムハーフのスケジュール データコピー、プロトコル処理、ユーザ通知

3.2 ボトムハーフの3方式比較

ボトムハーフ実行メカニズムの比較
┌──────────┐ ┌──────────┐ ┌──────────────┐
softirqtaskletworkqueue
静的定義動的生成カーネル
(10種類)可能スレッド上
で実行
複数CPU同一
同時実行taskletスリープ可
可能は直列化mutex使用可
高性能中間柔軟性高
NET_RX等USB等
└──────────┘ └──────────┘ └──────────────┘
性能: softirq > tasklet >> workqueue
柔軟性: workqueue > tasklet > softirq
実装難度: softirq > tasklet > workqueue

3.3 コード例: threaded IRQ の活用

Linux 2.6.30 以降では、request_threaded_irq() によってボトムハーフをカーネルスレッドとして実行できる。

/* コード例2: threaded IRQ によるハンドラ分離 */
 
#include <linux/interrupt.h>
 
/*
 * ハードIRQハンドラ(トップハーフ)
 * 最小限の処理のみ — デバイスのACKと判定
 */
static irqreturn_t my_hard_irq(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    u32 status = ioread32(dev->regs + IRQ_STATUS);
 
    if (!(status & DEVICE_IRQ_FLAG))
        return IRQ_NONE;
 
    /* デバイスの割り込みを確認応答 */
    iowrite32(status, dev->regs + IRQ_ACK);
 
    /* threaded handler の実行を要求 */
    return IRQ_WAKE_THREAD;
}
 
/*
 * スレッドハンドラ(ボトムハーフ)
 * プロセスコンテキストで実行 — スリープ可能
 */
static irqreturn_t my_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
 
    /* 重い処理をここで実行可能 */
    mutex_lock(&dev->data_mutex);
 
    /* DMA完了データの処理 */
    process_dma_buffer(dev);
 
    /* I2C/SPI 通信(スリープを伴う可能性あり) */
    update_device_config(dev);
 
    mutex_unlock(&dev->data_mutex);
 
    return IRQ_HANDLED;
}
 
static int my_device_init(struct platform_device *pdev)
{
    struct my_device *dev = platform_get_drvdata(pdev);
    int irq = platform_get_irq(pdev, 0);
 
    /*
     * request_threaded_irq():
     *   第2引数: ハードIRQハンドラ (NULL可 → デフォルトでIRQ_WAKE_THREAD)
     *   第3引数: スレッドハンドラ
     *   IRQF_ONESHOT: スレッドハンドラ完了までIRQを再有効化しない
     */
    return request_threaded_irq(irq,
                                my_hard_irq,
                                my_thread_fn,
                                IRQF_ONESHOT | IRQF_SHARED,
                                "my_device",
                                dev);
}

3.4 Linuxの softirq 一覧

Linuxカーネルでは以下の10種類の softirq が静的に定義されている(優先度順)。

番号 名称 用途
0 HI_SOFTIRQ 高優先度 tasklet
1 TIMER_SOFTIRQ タイマーコールバック
2 NET_TX_SOFTIRQ ネットワーク送信
3 NET_RX_SOFTIRQ ネットワーク受信
4 BLOCK_SOFTIRQ ブロックI/O完了
5 IRQ_POLL_SOFTIRQ IRQポーリング
6 TASKLET_SOFTIRQ 通常優先度 tasklet
7 SCHED_SOFTIRQ スケジューラ負荷分散
8 HRTIMER_SOFTIRQ 高精度タイマー
9 RCU_SOFTIRQ RCU処理

4. DMA (Direct Memory Access) の仕組み

4.1 DMAの基本原理

DMAは、CPUの介在なしにデバイスとメインメモリ間でデータを直接転送する技術である。CPUはDMAコントローラに転送パラメータ(ソースアドレス、宛先アドレス、転送サイズ)を設定するだけで、実際のデータ転送はDMAコントローラが実行する。

DMA転送の完全なシーケンス:

  CPU                 DMA Controller        Device           Memory
   │                      │                   │                │
   │ (1) DMAバッファ確保   │                   │                │
   │────────────────────────────────────────────────────────→│
   │                      │                   │                │
   │ (2) 転送パラメータ設定│                   │                │
   │  src_addr = 0xFE000  │                   │                │
   │  dst_addr = 0x80000  │                   │                │
   │  length   = 4096     │                   │                │
   │  direction = DEV→MEM │                   │                │
   │─────────────────────→│                   │                │
   │                      │                   │                │
   │ (3) DMA開始          │                   │                │
   │─────────────────────→│                   │                │
   │                      │                   │                │
   │ (4) CPU は別タスクへ │                   │                │
   │  ...                 │                   │                │
   │                      │ (5) バスマスタ    │                │
   │                      │  としてバスを     │                │
   │                      │  獲得             │                │
   │                      │                   │                │
   │                      │←─ データ読出 ────│                │
   │                      │                   │                │
   │                      │── メモリ書込 ──────────────────→│
   │                      │                   │                │
   │                      │  (6) 転送サイズ分 │                │
   │                      │  繰り返し         │                │
   │                      │                   │                │
   │                      │ (7) 転送完了      │                │
   │←── 割り込み ─────────│                   │                │
   │                      │                   │                │
   │ (8) 完了処理          │                   │                │
   │  (バッファ解放等)     │                   │                │
   │                      │                   │                │

4.2 DMAマッピングの種類

Linux カーネルにおけるDMAメモリマッピングには、用途に応じた複数の方式がある。

方式 API 用途 特徴
コヒーレントマッピング dma_alloc_coherent() デバイスとCPUが頻繁にアクセスするバッファ キャッシュ一貫性が自動維持される。リングバッファ、DMAディスクリプタに適する
ストリーミングマッピング dma_map_single() 一方向の一時的な転送 CPU側でキャッシュ管理が必要。高性能だがAPI使用に注意が必要
スキャッタ/ギャザー dma_map_sg() 不連続メモリ領域の転送 物理的に不連続なページを一度のDMA操作で転送。ネットワークバッファに最適

4.3 コード例: Linux DMA APIの使用

/* コード例3: DMAコヒーレントバッファの確保と使用 */
 
#include <linux/dma-mapping.h>
#include <linux/pci.h>
 
struct my_dma_device {
    struct pci_dev    *pdev;
    void              *dma_buf_virt;   /* CPU側仮想アドレス */
    dma_addr_t         dma_buf_phys;   /* デバイス側DMAアドレス */
    size_t             buf_size;
};
 
static int setup_dma_buffer(struct my_dma_device *dev)
{
    /* DMAマスクの設定 — 32bit DMA対応デバイスの場合 */
    if (dma_set_mask_and_coherent(&dev->pdev->dev, DMA_BIT_MASK(32))) {
        dev_err(&dev->pdev->dev, "32bit DMA未サポート\n");
        return -EIO;
    }
 
    dev->buf_size = PAGE_SIZE * 4;  /* 16KB */
 
    /*
     * dma_alloc_coherent():
     *   - CPUとデバイスの双方からアクセス可能なバッファを確保
     *   - キャッシュコヒーレンシが自動的に維持される
     *   - 戻り値: CPU側の仮想アドレス
     *   - dma_buf_phys: デバイス側が使用するDMAアドレス
     */
    dev->dma_buf_virt = dma_alloc_coherent(&dev->pdev->dev,
                                            dev->buf_size,
                                            &dev->dma_buf_phys,
                                            GFP_KERNEL);
    if (!dev->dma_buf_virt) {
        dev_err(&dev->pdev->dev, "DMAバッファ確保失敗\n");
        return -ENOMEM;
    }
 
    dev_info(&dev->pdev->dev,
             "DMAバッファ確保: virt=%p phys=%pad size=%zu\n",
             dev->dma_buf_virt, &dev->dma_buf_phys, dev->buf_size);
 
    return 0;
}
 
static void cleanup_dma_buffer(struct my_dma_device *dev)
{
    if (dev->dma_buf_virt) {
        dma_free_coherent(&dev->pdev->dev,
                          dev->buf_size,
                          dev->dma_buf_virt,
                          dev->dma_buf_phys);
        dev->dma_buf_virt = NULL;
    }
}
 
/*
 * ストリーミングDMAの使用例:
 * 一方向転送(デバイス → メモリ)
 */
static int start_dma_read(struct my_dma_device *dev,
                           void *buffer, size_t len)
{
    dma_addr_t dma_handle;
 
    /*
     * dma_map_single():
     *   - 既存のカーネルバッファをDMAマッピング
     *   - DMA_FROM_DEVICE: デバイスがメモリに書き込む方向
     */
    dma_handle = dma_map_single(&dev->pdev->dev,
                                 buffer, len,
                                 DMA_FROM_DEVICE);
 
    if (dma_mapping_error(&dev->pdev->dev, dma_handle)) {
        dev_err(&dev->pdev->dev, "DMAマッピング失敗\n");
        return -EIO;
    }
 
    /* デバイスにDMAアドレスと長さを設定 */
    iowrite32(lower_32_bits(dma_handle),
              dev->regs + DMA_SRC_ADDR_LO);
    iowrite32(upper_32_bits(dma_handle),
              dev->regs + DMA_SRC_ADDR_HI);
    iowrite32(len, dev->regs + DMA_LENGTH);
 
    /* DMA転送開始 */
    iowrite32(DMA_START, dev->regs + DMA_CONTROL);
 
    return 0;
}
 
/*
 * DMA完了後のクリーンアップ
 * (割り込みハンドラから呼ばれる)
 */
static void finish_dma_read(struct my_dma_device *dev,
                             void *buffer, size_t len,
                             dma_addr_t dma_handle)
{
    /*
     * dma_unmap_single():
     *   - DMAマッピングを解除
     *   - CPUキャッシュの無効化を実行
     *   - この後 buffer の内容をCPUから読めるようになる
     */
    dma_unmap_single(&dev->pdev->dev,
                      dma_handle, len,
                      DMA_FROM_DEVICE);
 
    /* ここで buffer の内容を処理 */
}

4.4 スキャッタ/ギャザーDMA

ネットワークパケットやファイルI/Oでは、転送すべきデータが物理的に不連続なメモリページに分散していることが多い。スキャッタ/ギャザーDMAは、この不連続なメモリ領域を1回のDMA操作で転送する技術である。

スキャッタ/ギャザーDMAの概念:

  [通常のDMA — 連続メモリ必要]

  物理メモリ:
UsedFREEUsedFREEUsed
↓
  コピーして連続領域を作る必要あり(CPU負荷)

  [スキャッタ/ギャザーDMA — 不連続OK]

  SG List (Scatter/Gather List):
Entry 2:
│      │
    DMAコントローラがSG Listを  │ Data │ page E
    順に処理、CPU介在不要       └──────┘

この技術は以下の場面で特に有効である。

  • ネットワーク: パケットヘッダとペイロードが別バッファにある場合(ゼロコピー送信)
  • ストレージ: ファイルシステムの複数ブロックを一度に読み書きする場合
  • 仮想化: ゲストOSの物理メモリがホスト上で不連続な場合

5. IOMMU — DMAのアドレス仮想化と保護

5.1 IOMMUの必要性

DMAはCPUを介さずにメモリにアクセスできるが、これはセキュリティ上の大きなリスクを伴う。悪意のあるデバイス(または不具合のあるドライバ)が任意の物理メモリアドレスにDMA転送を行えば、カーネルメモリの破壊やデータ漏洩が発生しうる。

IOMMU (Input/Output Memory Management Unit) は、デバイスのDMAアドレスを仮想化し、許可されたメモリ領域のみにアクセスを制限する。CPUにとってのMMUと同様の役割を、I/Oデバイスに対して果たす。

IOMMUの位置づけ:

  CPU側:                              デバイス側:
CPU───→MMU───→物理メモリ←──IOMMU←───Device
│                        │
              ▼                        ▼
         ページテーブル            I/Oページテーブル
         (仮想→物理)              (DMAアドレス→物理)

  MMU : CPUの仮想アドレス → 物理アドレスの変換
  IOMMU: デバイスのDMAアドレス → 物理アドレスの変換
デバイスが DMA addr 0x2000 にアクセス
IOMMU がI/Oページテーブルを参照
[許可] → 物理addr 0xA8000 に変換して転送
[拒否] → DMA Fault 発生 → カーネルに通知

5.2 IOMMUの主要な用途

用途 説明
DMA保護 デバイスがアクセス可能な物理メモリ領域を制限する
デバイスの仮想化パススルー VT-d/AMD-Vi により、仮想マシンにデバイスを直接割り当て(PCIパススルー)する際に、ゲストの物理アドレスをホストの物理アドレスに変換する
DMAリマッピング 32bit DMA制限のあるデバイスでも、4GB超のメモリにアクセス可能にする(バウンスバッファの回避)
割り込みリマッピング デバイスからの割り込みを検証し、不正な割り込みインジェクションを防止する

5.3 Linuxにおける IOMMU の設定

# コード例4: IOMMU関連のカーネルパラメータと確認コマンド
 
# カーネルブートパラメータ(GRUB設定)
# Intel VT-d の有効化
GRUB_CMDLINE_LINUX="intel_iommu=on"
 
# AMD-Vi の有効化
GRUB_CMDLINE_LINUX="amd_iommu=on"
 
# IOMMUグループの確認
# 各デバイスがどのIOMMUグループに属するかを表示
for d in /sys/kernel/iommu_groups/*/devices/*; do
    n=$(echo "$d" | rev | cut -d/ -f1 | rev)
    g=$(echo "$d" | rev | cut -d/ -f3 | rev)
    echo "IOMMU Group $g: $(lspci -nns "$n")"
done
 
# dmesg での IOMMU 初期化確認
dmesg | grep -i iommu
# 出力例:
# [    0.123456] DMAR: IOMMU enabled
# [    0.234567] DMAR: Intel(R) Virtualization Technology for Directed I/O
 
# /proc/interrupts で割り込み分布を確認
cat /proc/interrupts | head -20
 
# 特定IRQのアフィニティ確認
cat /proc/irq/24/smp_affinity
# 出力例: f  (CPU 0-3 に配送)
 
# IRQアフィニティの設定(CPU 2 のみに配送)
echo 4 > /proc/irq/24/smp_affinity
# ビットマスク: 4 = 0100 → CPU 2

6. 現代のI/O技術

6.1 MSI / MSI-X (Message Signaled Interrupts)

従来の割り込み方式では、デバイスは専用のIRQピン(物理配線)を使ってCPUに割り込みを通知していた。MSI/MSI-Xでは、デバイスが特定のメモリアドレスにデータを書き込むことで割り込みを通知する。

従来方式 vs MSI/MSI-X:

  [従来 (ピンベース割り込み)]
Device ├──────────→I/O ├───────→CPU
AAPIC
  ┌────────┐  IRQピン  │        │
  │ Device ├──────────→│        │  問題:
  │   B    │           └────────┘  - ピン数に限り (24本)
  └────────┘                       - 共有IRQで性能低下
- 動的な配送先変更が困難

  [MSI-X]
Device── メモリ書き込み ────────→Local
A(addr=0xFEE00xxx,APIC
data=vector_num)(CPU 0)
まで(別アドレス/データ)Local
│ (CPU 3) │
  利点:                               └─────────┘
  - デバイス毎に最大2048ベクタ
  - 各ベクタを異なるCPUに配送可能
  - IRQ共有不要 → ハンドラが高速
  - NVMe: キュー毎にMSI-Xベクタを割当

6.2 MSI-X の性能上の利点

特性 ピンベース割り込み MSI MSI-X
ベクタ数 1 (共有) 最大32 最大2048
IRQ共有 必要 不要 不要
CPUターゲティング 制限あり 限定的 ベクタ毎に自由
レイテンシ 高い 低い 低い
マルチキュー対応 不可 制限あり 完全対応
PCIe互換性 レガシー 標準 推奨

6.3 NVMe (Non-Volatile Memory Express)

NVMeは、SSD (NAND Flash / 3D XPoint) の性能を最大限に引き出すために設計された、PCIeネイティブのストレージプロトコルである。旧来のAHCI (Advanced Host Controller Interface) がHDDの回転待ちを前提に設計されていたのに対し、NVMeは大量の並列I/Oを効率的に処理する。

AHCI vs NVMe アーキテクチャ比較:

  [AHCI (SATAベース)]
CPU────(最大32コマンド)──→SATA SSD
深度: 32
↑ 全コマンドが1キューに直列化 = ボトルネック

  [NVMe (PCIeベース)]
CPUSubmission Q 1 ────→NVMe SSD
CoreSubmission Q 2 ────→
0内部
Completion Q 0 ←───コントロ
CPUSubmission Q 4 ────→Flash
Coreチャネル
1Completion Q 2 ←───×8-16
最大 65,535 キュー × 65,536 エントリ/キュー
  各CPUコアに専用のSubmission/Completionキューペアを割当
  MSI-Xベクタもキュー毎に割当 → ロックフリー設計
比較項目 AHCI (SATA) NVMe
キュー数 1 最大 65,535
キュー深度 32 最大 65,536
ホストインタフェース SATA (6 Gbps) PCIe Gen4 x4 (64 Gbps)
割り込み方式 ピンベース / MSI MSI-X (キュー毎)
コマンド発行 レジスタ書込×4回 Doorbell レジスタ×1回
CPU使用率 高い 低い
IOPS (4K Random Read) 約100,000 約1,000,000+

6.4 RDMA (Remote Direct Memory Access)

RDMAは、ネットワーク越しに相手マシンのメモリに直接アクセスする技術である。通常のネットワーク通信では、データはアプリケーション→カーネル→NICドライバ→NIC→ネットワーク→NIC→NICドライバ→カーネル→アプリケーションという多段階のコピーとコンテキストスイッチを経る。RDMAはこれを「ゼロコピー」「OSバイパス」で実現する。

通常のネットワーク通信 vs RDMA:

  [通常のTCP/IP通信]
  App → [copy] → Kernel TCP/IP Stack → [copy] → NIC Driver → NIC
                     ↓ 割り込み / コンテキストスイッチ多数
  NIC → NIC Driver → [copy] → Kernel TCP/IP Stack → [copy] → App

  合計4回のメモリコピー + 複数回のコンテキストスイッチ

  [RDMA]
  App ──────→ RNIC (RDMA NIC) ──────→ Network
                ↓ Hardware offload         ↓
  Network ──→ RNIC ──────→ App のメモリに直接書き込み

  ゼロコピー、OSバイパス、CPU関与なし

  RDMA動詞 (Verbs):
RDMA Read : リモートメモリの読み取り
RDMA Write : リモートメモリへの書き込み
Send/Recv : メッセージパッシング
Atomic : リモートでのCAS/Fetch-Add

RDMAの主要なトランスポート技術は以下の3つである。

技術 物理層 帯域幅 レイテンシ 用途
InfiniBand 専用ファブリック 200-400 Gbps (HDR/NDR) < 1 us HPC、AIクラスタ
RoCEv2 Ethernet 25-400 Gbps 1-2 us データセンター
iWARP Ethernet + TCP 10-100 Gbps 5-10 us 汎用

7. 割り込みアフィニティとパフォーマンスチューニング

7.1 IRQバランシングの課題

マルチコアシステムでは、割り込みがどのCPUで処理されるかが性能に大きく影響する。デフォルトでは、Linux の irqbalance デーモンが割り込みをCPU間に分散するが、高性能を要求するワークロードでは手動チューニングが必要になる場合がある。

割り込みアフィニティ設定の考え方:
NUMA Node 0 NUMA Node 1
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
CPU 0CPU 1CPU 2CPU 3
└──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘
┌──┴────────┴──┐ ┌──┴────────┴──┐
L3 CacheL3 Cache
MemoryMemory
ControllerController
└──────┬───────┘ └──────┬───────┘
┌──────┴───────┐ ┌─────┴────────┐
PCIe RootPCIe Root
ComplexComplex
└──────┬───────┘ └──────┬───────┘
┌──┴──┐ ┌───┴──┐
NICNVMe
eth0nvme0
└─────┘ └──────┘
ベストプラクティス:
  - NICの割り込み → NICと同じNUMAノードのCPUに配送
  - NVMeの割り込み → NVMeと同じNUMAノードのCPUに配送
  - NUMAをまたぐメモリアクセスはレイテンシが増大する

7.2 コード例: 割り込みアフィニティの設定スクリプト

# コード例5: NICの割り込みアフィニティを手動設定するスクリプト
 
#!/bin/bash
# nic_irq_affinity.sh — NIC割り込みをNUMAローカルCPUに固定
 
DEVICE="eth0"
NUMA_NODE=$(cat /sys/class/net/${DEVICE}/device/numa_node)
 
echo "=== ${DEVICE} の割り込みアフィニティ設定 ==="
echo "NUMA Node: ${NUMA_NODE}"
 
# NUMAノードに属するCPUリストを取得
CPULIST=$(cat /sys/devices/system/node/node${NUMA_NODE}/cpulist)
echo "Local CPUs: ${CPULIST}"
 
# irqbalance を停止(手動設定と競合するため)
systemctl stop irqbalance 2>/dev/null
 
# デバイスのIRQ番号一覧を取得
IRQS=$(grep "${DEVICE}" /proc/interrupts | awk '{print $1}' | tr -d ':')
 
CPU_IDX=0
CPUS=($(echo "${CPULIST}" | tr ',' ' ' | tr '-' ' '))
 
for IRQ in ${IRQS}; do
    # CPUをラウンドロビンで割当
    TARGET_CPU=${CPUS[$((CPU_IDX % ${#CPUS[@]}))]}
 
    # アフィニティマスクを計算
    MASK=$(printf "%x" $((1 << TARGET_CPU)))
 
    echo "  IRQ ${IRQ} → CPU ${TARGET_CPU} (mask: ${MASK})"
    echo "${MASK}" > /proc/irq/${IRQ}/smp_affinity
 
    CPU_IDX=$((CPU_IDX + 1))
done
 
echo "=== 設定完了 ==="
 
# 確認
echo ""
echo "現在の割り込み分布:"
grep "${DEVICE}" /proc/interrupts

7.3 NAPI — ネットワーク割り込みの最適化

高速ネットワーク(10GbE以上)では、パケット毎に割り込みが発生すると、割り込みのオーバーヘッド自体がCPUを圧迫する(割り込みストーム)。Linux NAPI (New API) はこの問題を、割り込みとポーリングのハイブリッド方式で解決する。

NAPI の動作フロー:

  パケット到着レート: 低い                  高い
  ←─────────────────────────────────────────→

  [割り込みモード]          [ポーリングモード]
  パケット毎に             割り込みを無効化し
  割り込み発生              CPUがNICを定期的にポーリング
(1) パケット到着 → NICが割り込み発生
(2) 割り込みハンドラ: napi_schedule()
→ 以降の割り込みを無効化
(3) softirq (NET_RX_SOFTIRQ) が起動
(4) NAPI poll関数がNICのリングバッファを
繰り返しチェック (ポーリング)
(5) バジェット(通常64パケット)分を処理
(6a) まだパケットあり → (4)に戻る
(6b) パケット枯渇 → napi_complete_done()
→ 割り込みを再有効化 → (1)に戻る

この設計により、低負荷時は割り込みの低レイテンシを活かし、高負荷時はポーリングでスループットを最大化する自動的な切り替えが実現される。


8. アンチパターンと対策

8.1 アンチパターン1: 割り込みハンドラ内での長時間処理

[問題のあるコード]

static irqreturn_t bad_isr(int irq, void *dev_id)
{
    /* 危険: 割り込みハンドラ内でmutexを取得 */
    mutex_lock(&data->big_lock);         /* スリープ可能 → BUG */

    /* 危険: 大量データのコピー処理 */
    memcpy(user_buf, dma_buf, 1048576);  /* 1MB コピー → 長時間 */

    /* 危険: カーネルメモリの通常確保 */
    buf = kmalloc(65536, GFP_KERNEL);    /* スリープ可能 → BUG */

    mutex_unlock(&data->big_lock);
    return IRQ_HANDLED;
}

問題点:

  • 割り込みコンテキストではスリープ不可。mutex_lock()GFP_KERNEL はスリープする可能性がある
  • 長時間の処理は他の割り込みをブロックし、システム全体の応答性を劣化させる
  • 最悪の場合、ウォッチドッグタイマーが発火してシステムがリセットされる

対策:

  • トップハーフでは最小限の処理(ACK、フラグ設定)のみ行う
  • 重い処理はボトムハーフ(tasklet、workqueue、threaded IRQ)に委譲する
  • 割り込みコンテキストでは spin_lock()GFP_ATOMIC のみ使用する
[修正後のコード]

static irqreturn_t good_isr(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    u32 status;

    status = ioread32(dev->regs + IRQ_STATUS);
    if (!(status & MY_IRQ_FLAG))
        return IRQ_NONE;

    /* 最小限の処理: ACKとフラグ設定のみ */
    iowrite32(status, dev->regs + IRQ_ACK);

    spin_lock(&dev->lock);
    dev->pending_status |= status;
    spin_unlock(&dev->lock);

    /* 重い処理はボトムハーフへ */
    return IRQ_WAKE_THREAD;
}

static irqreturn_t good_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    /* プロセスコンテキスト: mutex、GFP_KERNEL、スリープ全て可能 */
    mutex_lock(&dev->big_lock);
    memcpy(user_buf, dma_buf, 1048576);
    buf = kmalloc(65536, GFP_KERNEL);
    process_data(dev);
    mutex_unlock(&dev->big_lock);

    return IRQ_HANDLED;
}

8.2 アンチパターン2: DMAバッファのキャッシュ一貫性違反

[問題のあるコード]

/* ストリーミングDMAで受信したデータを読むケース */
dma_handle = dma_map_single(dev, buffer, len, DMA_FROM_DEVICE);

/* デバイスにDMA転送を開始させる */
start_device_dma(dev, dma_handle, len);

/* 転送完了を待つ */
wait_for_completion(&dev->dma_done);

/* 危険: unmap前にCPUがバッファにアクセス */
process_data(buffer);    /* キャッシュに古いデータが残っている可能性 */

/* 後から unmap — 手遅れ */
dma_unmap_single(dev, dma_handle, len, DMA_FROM_DEVICE);

問題点:

  • dma_unmap_single() はCPUキャッシュの無効化(invalidate)を行う
  • unmap前にCPUがバッファを読むと、キャッシュに残っている古いデータを読む可能性がある
  • この種のバグは、特定のアーキテクチャ(ARMなど、キャッシュコヒーレンシが弱い環境)でのみ再現し、x86では発見しにくい

対策:

  • 必ず dma_unmap_single() の後にバッファにアクセスする
  • 繰り返しDMAを行う場合は dma_sync_single_for_cpu() / dma_sync_single_for_device() を使用する
[修正後のコード]

dma_handle = dma_map_single(dev, buffer, len, DMA_FROM_DEVICE);
start_device_dma(dev, dma_handle, len);
wait_for_completion(&dev->dma_done);

/* 正しい順序: まず unmap してからアクセス */
dma_unmap_single(dev, dma_handle, len, DMA_FROM_DEVICE);
process_data(buffer);    /* キャッシュは無効化済み → 正しいデータ */

/* もしくは、マッピングを維持したまま同期する場合 */
dma_sync_single_for_cpu(dev, dma_handle, len, DMA_FROM_DEVICE);
process_data(buffer);    /* 同期済み → 正しいデータ */

/* 再びデバイスにDMAさせる前に */
dma_sync_single_for_device(dev, dma_handle, len, DMA_FROM_DEVICE);
start_device_dma(dev, dma_handle, len);

8.3 アンチパターン3: NUMA非対応のIRQ配置

問題のある構成:

  NUMA Node 0                   NUMA Node 1
CPU 0-7CPU 8-15
MemoryQPI/UPIMemory
(ローカル)←─────────────→(ローカル)
│                            │
10GbE
NIC
問題: NICの割り込みをCPU 8-15(Node 1)で処理
  → パケットデータはNode 0のメモリに到着
  → CPU 8-15がNode 0のメモリにアクセス
  → NUMAリモートアクセスでレイテンシ50-100%増加

対策:

  • デバイスと同じNUMAノードのCPUにIRQアフィニティを設定する
  • アプリケーションも同じNUMAノードで実行する(numactl --cpunodebind=0 --membind=0
  • irqbalance--banirq オプションで特定IRQの自動移動を禁止する

9. 組み込みシステムにおける割り込みとDMA

9.1 ARM アーキテクチャの割り込みコントローラ (GIC)

ARMプロセッサでは、GIC (Generic Interrupt Controller) が割り込み管理を担う。GICv3/v4 はサーバ向けARMでも使用されており、仮想化対応の割り込み配送機能を持つ。

ARM GICv3 アーキテクチャ:
GIC
┌──────────────┐ ┌───────────────────────┐
DistributorRedistributor
(CPUインタフェース)
SPI管理┌──────┐ ┌──────┐
(共有割込)──→CPU 0CPU 1
Re-Re-
優先度制御distdist
アフィニティ└──┬───┘ └──┬───┘
└──────┬───────┘ └──────┼────────┼───────┘
┌─────┴──┐ ┌───┴────┐
CPU CoreCPU Core
01
└────────┘ └────────┘
│
  割り込みの種類:
SGI (Software Generated Interrupt)
ID 0-15: CPU間通信 (IPI)
PPI (Private Peripheral Interrupt)
ID 16-31: CPUローカルタイマー等
SPI (Shared Peripheral Interrupt)
ID 32-1019: 外部デバイス割り込み
LPI (Locality-specific Peripheral Int.)
ID 8192+: MSI/MSI-X相当 (GICv3以降)

9.2 組み込みLinuxでのデバイスツリーによる割り込み定義

組み込みLinuxでは、ハードウェア構成をデバイスツリー (Device Tree) で記述する。割り込みの接続関係もデバイスツリーで定義する。

/* デバイスツリーでの割り込み定義例 */
/ {
    interrupt-controller@f9010000 {
        compatible = "arm,gic-400";
        #interrupt-cells = <3>;
        interrupt-controller;
        reg = <0xf9010000 0x10000>,
              <0xf9020000 0x20000>;
    };
 
    /* UARTデバイスの割り込み定義 */
    uart0: serial@e0000000 {
        compatible = "xlnx,xuartps";
        reg = <0xe0000000 0x1000>;
        /*
         * interrupts = <type irq_num flags>;
         *   type:  0 = SPI, 1 = PPI
         *   flags: 1 = edge-rising, 4 = level-high
         */
        interrupts = <0 27 4>;  /* SPI #27, level-high */
        interrupt-parent = <&intc>;
        clocks = <&clkc 23>;
    };
 
    /* DMAコントローラの定義 */
    dma0: dma@f8003000 {
        compatible = "arm,pl330", "arm,primecell";
        reg = <0xf8003000 0x1000>;
        interrupts = <0 13 4>,   /* fault */
                     <0 14 4>,   /* ch0 done */
                     <0 15 4>,   /* ch1 done */
                     <0 16 4>,   /* ch2 done */
                     <0 17 4>;   /* ch3 done */
        #dma-cells = <1>;
    };
 
    /* DMAを使用するデバイスの定義 */
    spi0: spi@e0006000 {
        compatible = "xlnx,zynq-spi-r1p6";
        reg = <0xe0006000 0x1000>;
        interrupts = <0 26 4>;
        dmas = <&dma0 0>, <&dma0 1>;
        dma-names = "tx", "rx";
    };
};

9.3 RTOS における割り込み処理

リアルタイムOS (RTOS) では、割り込み応答時間の保証が最重要となる。Linuxの PREEMPT_RT パッチやFreeRTOSの割り込み管理は、異なるアプローチでリアルタイム性を実現する。

特性 通常のLinux PREEMPT_RT Linux FreeRTOS
割り込みレイテンシ 数十 us 数 us 数百 ns - 数 us
割り込みハンドラ ハードIRQ + softirq ほぼ全てスレッド化 ISR + 遅延処理タスク
優先度逆転防止 なし (通常) 優先度継承mutex 優先度継承mutex
spinlock 割り込み禁止 rt_mutex (プリエンプション可能) クリティカルセクション
決定論的動作 保証なし ほぼ保証 保証

10. 演習問題

10.1 初級: 割り込み処理フローの理解

問題: 以下のシナリオで、割り込み処理の各ステップを時系列順に並べよ。

ユーザがキーボードの「A」キーを押した場合:

選択肢(順番を並べ替えよ):
(a) キーボードコントローラがIRQ 1を発行
(b) CPUが現在実行中の命令を完了
(c) IDT[33] からキーボード割り込みハンドラのアドレスを取得
(d) RFLAGS, CS, RIP をカーネルスタックにプッシュ
(e) I/O APIC がベクタ番号33をCPUに送信
(f) キーボードハンドラがスキャンコードを読み取り
(g) EOI (End of Interrupt) をLocal APICに送信
(h) IRET命令で中断したプロセスに復帰
(i) tasklet/workqueue でスキャンコードをキーコードに変換
(j) 入力イベントをユーザ空間の入力サブシステムに配送

解答例: 正しい順序: (a) → (e) → (b) → (d) → (c) → (f) → (i のスケジュール) → (g) → (h) → (i の実行) → (j)

詳しい解説:

  1. (a) キーボードコントローラがスキャンコードを生成し、IRQ 1 を発行
  2. (e) I/O APIC がIRQ 1 をベクタ番号33に変換し、ターゲットCPUの Local APIC に配送
  3. (b) CPUが現在実行中の命令を完了(命令境界で割り込みを受け付け)
  4. (d) CPUがRFLAGS、CS、RIPを自動的にカーネルスタックにプッシュ(Ring 3→Ring 0 の場合はTSSからRSP0をロード)
  5. (c) IDT[33] を参照してハンドラアドレスを取得、IF フラグをクリア
  6. (f) トップハーフ: I/Oポート 0x60 からスキャンコードを読み取り、バッファに格納
  7. ボトムハーフ(tasklet)をスケジュール
  8. (g) Local APIC に EOI を書き込み、割り込み処理の完了を通知
  9. (h) IRET命令で RIP, CS, RFLAGS をポップし、中断したプロセスに復帰
  10. (i) softirq/tasklet が起動、スキャンコードをキーコードに変換
  11. (j) Linuxの入力サブシステム (evdev) を通じてユーザ空間に配送

10.2 中級: DMAドライバの設計

問題: 以下の要件を満たす簡易DMAドライバの擬似コードを設計せよ。

要件:

  • PCIeデバイスからDMAで4KBのデータを受信する
  • ストリーミングDMAマッピングを使用する
  • threaded IRQ でDMA完了を処理する
  • エラーハンドリングを含める

設計のポイント:

/*
 * 解答の骨格(設計の考え方を示す)
 */
 
/* 1. デバイス初期化 */
static int my_probe(struct pci_dev *pdev, ...)
{
    /* (a) PCI デバイスの有効化 */
    pci_enable_device(pdev);
 
    /* (b) バスマスタを有効化(DMAに必須) */
    pci_set_master(pdev);
 
    /* (c) DMAマスクの設定 */
    dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
 
    /* (d) 受信バッファの確保 */
    buf = kmalloc(4096, GFP_KERNEL);
 
    /* (e) threaded IRQ の登録 */
    request_threaded_irq(pdev->irq, hard_isr, thread_fn,
                         IRQF_ONESHOT, "my_dma", dev);
}
 
/* 2. DMA受信の開始 */
static int start_receive(struct my_device *dev)
{
    /* (a) ストリーミングDMAマッピングの作成 */
    dev->dma_addr = dma_map_single(&dev->pdev->dev,
                                    dev->buf, 4096,
                                    DMA_FROM_DEVICE);
 
    /* (b) マッピングエラーの確認 — 必須! */
    if (dma_mapping_error(&dev->pdev->dev, dev->dma_addr))
        return -EIO;
 
    /* (c) デバイスにDMAアドレスとサイズを設定 */
    iowrite32(lower_32_bits(dev->dma_addr), dev->regs + DMA_ADDR_LO);
    iowrite32(upper_32_bits(dev->dma_addr), dev->regs + DMA_ADDR_HI);
    iowrite32(4096, dev->regs + DMA_SIZE);
 
    /* (d) DMA開始 */
    iowrite32(DMA_START_BIT, dev->regs + DMA_CTRL);
    return 0;
}
 
/* 3. トップハーフ: ACKのみ */
static irqreturn_t hard_isr(int irq, void *dev_id)
{
    u32 status = ioread32(dev->regs + IRQ_STATUS);
    if (!(status & DMA_COMPLETE_BIT))
        return IRQ_NONE;
    iowrite32(status, dev->regs + IRQ_ACK);
    return IRQ_WAKE_THREAD;
}
 
/* 4. ボトムハーフ: データ処理 */
static irqreturn_t thread_fn(int irq, void *dev_id)
{
    /* (a) DMAマッピングの解除 → キャッシュ無効化 */
    dma_unmap_single(&dev->pdev->dev,
                      dev->dma_addr, 4096,
                      DMA_FROM_DEVICE);
 
    /* (b) データの処理(ここでスリープ可能) */
    process_received_data(dev->buf, 4096);
 
    /* (c) 次の受信を開始 */
    start_receive(dev);
 
    return IRQ_HANDLED;
}

10.3 上級: 割り込み負荷の分析と最適化

問題: 10Gbps NIC を搭載したサーバで、以下の perf 出力が得られた。問題を特定し、最適化戦略を提案せよ。

# perf top -C 0 の出力 (CPU 0 のみ)

  45.3%  [kernel]  [k] native_queued_spin_lock_slowpath
  12.8%  [kernel]  [k] _raw_spin_lock
   8.2%  [kernel]  [k] net_rx_action
   6.5%  [kernel]  [k] mlx5e_napi_poll
   4.1%  [kernel]  [k] __netif_receive_skb_core
   3.7%  [kernel]  [k] ip_rcv
   2.9%  [kernel]  [k] tcp_v4_rcv

# /proc/interrupts (抜粋)
            CPU0       CPU1       CPU2       CPU3
  98:  125847293          0          0          0  IR-PCI-MSI  mlx5_comp0
  99:  118234567          0          0          0  IR-PCI-MSI  mlx5_comp1
 100:          0          0          0          0  IR-PCI-MSI  mlx5_comp2
 101:          0          0          0          0  IR-PCI-MSI  mlx5_comp3

分析と解答の方向性:

  1. 問題の特定:

    • CPU 0 に全ての NIC 割り込みが集中している(mlx5_comp0, comp1 が CPU 0 のみ)
    • native_queued_spin_lock_slowpath が45.3% → 深刻なロック競合
    • NIC の4キュー中2キューしかアクティブでなく、両方がCPU 0 に固定
  2. 最適化戦略:

    • (a) IRQアフィニティの再配置: 各キューの割り込みを異なるCPUに分散
    • (b) RPS (Receive Packet Steering) の有効化: ソフトウェアレベルでパケット処理をCPU間に分散
    • (c) XPS (Transmit Packet Steering) の設定: 送信側もCPUを分散
    • (d) NICのキュー数の確認: ethtool -l eth0 で利用可能なキュー数を確認し、4キュー全てを有効化
    • (e) NUMAアフィニティの確認: NICが接続されたNUMAノードのCPUを使用しているか確認
# 最適化コマンド例
# NICの全キューのIRQを各CPUに分散
echo 1 > /proc/irq/98/smp_affinity   # CPU 0
echo 2 > /proc/irq/99/smp_affinity   # CPU 1
echo 4 > /proc/irq/100/smp_affinity  # CPU 2
echo 8 > /proc/irq/101/smp_affinity  # CPU 3
 
# RPSの有効化(CPU 0-3に分散)
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus
echo f > /sys/class/net/eth0/queues/rx-1/rps_cpus

11. FAQ (よくある質問)

Q1: 割り込みハンドラ内で printk() を使ってもよいか?

A: printk() 自体は割り込みコンテキストから呼び出し可能である。カーネル内部ではロックフリーのリングバッファに書き込むだけなので、スリープしない。ただし、以下の点に注意が必要である。

  • 高頻度の割り込みで printk() を呼ぶと、リングバッファの溢れやコンソール出力のオーバーヘッドで性能が大幅に低下する
  • デバッグ目的なら printk_ratelimited()dev_dbg_ratelimited() を使い、出力頻度を制限する
  • 本番コードでは printk() をトップハーフから除去し、統計カウンタやtracepoint で代替する

Q2: request_irq()request_threaded_irq() はどちらを使うべきか?

A: 一般的なガイドラインは以下の通り。

ケース 推奨API 理由
処理が非常に短い(レジスタ読み書きのみ) request_irq() オーバーヘッドが最小
I2C/SPIなどスリープを伴うバス経由のデバイス request_threaded_irq() ハンドラ内でスリープが必要
処理量が中程度 request_threaded_irq() 柔軟性と安全性のバランスが良い
高性能ネットワーク/ストレージ request_irq() + NAPI/softirq レイテンシ最小化が必要

Linux 4.x以降では、新規ドライバには request_threaded_irq() の使用が推奨されている。これにより、RT (PREEMPT_RT) カーネルとの互換性も自動的に確保される。

Q3: DMAバッファのサイズに上限はあるか?

A: 技術的にはアーキテクチャのアドレス空間がDMAの上限を決めるが、実用上は以下の制約がある。

  • コヒーレントDMA (dma_alloc_coherent()): カーネルの連続物理メモリ確保に依存するため、通常は数MBが上限。起動直後ならより大きなブロックを確保できるが、メモリフラグメンテーションが進むと失敗しやすい。CMA (Contiguous Memory Allocator) を使えば数百MBまで確保可能
  • ストリーミングDMA (dma_map_single()): 既に確保されたバッファをマッピングするだけなので、バッファ自体が確保できればサイズ制約は緩い
  • スキャッタ/ギャザーDMA: 不連続ページを使用できるため、物理的な連続メモリの制約を受けない。SGリストのエントリ数に応じた大容量転送が可能

大容量DMAが必要な場合は、CMAの予約領域を増やすか、dma_alloc_attrs()DMA_ATTR_FORCE_CONTIGUOUS を指定する方法がある。

Q4: 割り込みが失われる (lost interrupt) ことはあるか?

A: エッジトリガ割り込みでは理論上発生しうる。

  • エッジトリガ: 信号の立ち上がり(Low→High)で割り込みを検出する。ハンドラ実行中に次の割り込みが発生し、ハンドラ完了前に信号がLowに戻ると、2回目の割り込みが失われる可能性がある
  • レベルトリガ: 信号がHighを維持している間、継続的に割り込みを通知する。デバイスが明示的にクリアするまで信号が維持されるため、割り込みの喪失は起こりにくい

PCIeのMSI/MSI-Xはメモリ書き込みベースであり、バスの信頼性に依存する。通常は失われないが、IOMMU障害などの極端な状況では発生しうる。ドライバ側ではウォッチドッグタイマーで定期的にデバイス状態をポーリングするのが堅牢な設計である。

Q5: ユーザ空間から割り込みを処理できるか?

A: Linux の UIO (Userspace I/O) フレームワークと VFIO (Virtual Function I/O) を使えば可能である。

  • UIO: /dev/uioXread() / select() することで、割り込みの発生をユーザ空間で待ち受けできる。DPDKのような高性能パケット処理フレームワークはこの手法を使う
  • VFIO: IOMMUによるメモリ保護付きでデバイスをユーザ空間に公開する。仮想化環境でのデバイスパススルーにも使用される

ただし、割り込みの「検知」はカーネル内で行われ、ユーザ空間にはイベント通知として伝達される。純粋にカーネルバイパスでハードウェア割り込みを処理するわけではない点に注意が必要である。


FAQ

Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?

実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。

Q2: 初心者がよく陥る間違いは何ですか?

基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。

Q3: 実務ではどのように活用されていますか?

このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。


12. まとめ — 重要概念の整理

技術要素の全体マップ

概念 本質 キーポイント
ハードウェア割り込み デバイスからCPUへの非同期通知 マスカブル/ノンマスカブルの区別、IDTによるディスパッチ
ソフトウェア割り込み プログラムからの意図的な割り込み システムコール (syscall/int 0x80)、デバッグトラップ
例外 CPU内部で発生するエラー Fault (再実行可) / Trap (次命令) / Abort (復帰不可)
トップハーフ 割り込みの即座の処理 最小限の処理、スリープ禁止、高速
ボトムハーフ 遅延された割り込み処理 softirq / tasklet / workqueue / threaded IRQ
DMA CPUを介さないデータ転送 コヒーレント / ストリーミング / スキャッタ・ギャザー
IOMMU DMAアドレスの仮想化・保護 VT-d / AMD-Vi、仮想化パススルーの基盤
MSI/MSI-X メモリ書き込みベースの割り込み PCIeデバイスの標準、マルチキュー対応
NVMe SSD専用高速I/Oプロトコル 65Kキュー × 65Kエントリ、MSI-X連携
RDMA ネットワーク越しの直接メモリアクセス ゼロコピー、OSバイパス、HPC/AI向け
NAPI 割り込み+ポーリングのハイブリッド 高速NICの割り込みストーム対策
IRQアフィニティ 割り込みのCPU配送先制御 NUMA対応、性能最適化の基本

設計判断のフローチャート

デバイスドライバの割り込み処理方式を選択する:

  割り込み処理に
  スリープが必要?
      │
      ├── YES → request_threaded_irq()
      │          IRQF_ONESHOT を使用
      │
      └── NO ──→ 処理は短時間?
                     │
                     ├── YES → request_irq() のみ
                     │          (トップハーフで完結)
                     │
                     └── NO ──→ 高スループット
                                  が必要?
                                    │
                                    ├── YES → request_irq()
                                    │          + softirq/NAPI
                                    │
                                    └── NO ──→ request_irq()
                                                + tasklet

  DMAバッファの方式を選択する:

  CPU・デバイスが
  同時にアクセス?
      │
      ├── YES → dma_alloc_coherent()
      │          (リングバッファ、ディスクリプタ)
      │
      └── NO ──→ メモリは連続?
                     │
                     ├── YES → dma_map_single()
                     │          (ストリーミングDMA)
                     │
                     └── NO ──→ dma_map_sg()
                                 (スキャッタ/ギャザー)

次に読むべきガイド


参考文献

  1. Corbet, J., Rubini, A., & Kroah-Hartman, G. Linux Device Drivers, 3rd Edition. O'Reilly Media, 2005. — Linuxデバイスドライバの標準的なリファレンス。割り込み処理 (Chapter 10) とDMA (Chapter 15) の解説が特に優れている
  2. Love, R. Linux Kernel Development, 3rd Edition. Addison-Wesley, 2010. — Chapter 7「Interrupts and Interrupt Handlers」で、トップハーフ/ボトムハーフの設計思想を詳述
  3. Bovet, D. P. & Cesati, M. Understanding the Linux Kernel, 3rd Edition. O'Reilly Media, 2005. — 割り込みディスクリプタテーブル(IDT)やAPICの内部構造を低レベルから解説
  4. Intel Corporation. Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 3: System Programming Guide, Chapter 6 "Interrupt and Exception Handling". — x86/x86-64 の割り込みアーキテクチャの公式仕様書
  5. ARM Limited. ARM Generic Interrupt Controller Architecture Specification (GICv3/GICv4). — ARM GICの公式仕様。組み込みLinux開発者向け
  6. NVM Express Workgroup. NVM Express Base Specification, Revision 2.0. — NVMeプロトコルの公式仕様。キュー構造と割り込み方式を定義
  7. Mellanox Technologies (NVIDIA). RDMA Aware Networks Programming User Manual. — RDMAプログラミングの実践的なガイド。InfiniBand/RoCEのVerbs APIを解説