レガシーガジェット研究所

気になったことのあれこれ。

Linux Kernel ~ 割り込み処理 ~

概要

「詳解Linux Kernel」を参考にVersion 2.6.11のコードリーディングをしていく。CPUのアーキテクチャは書籍に沿ってIntelx86とする。

今回は割り込み処理について見ていく。(*nは参考文献のn番目に対応する)

割り込み処理

例外ではほとんどの場合その例外の発生元となっているカレントプロセスにシグナルを送信することで処理する。例外処理もシグナルを受け取るまでは遅延される。

しかし割り込みの場合には別の無関係なプロセスが動作している際に発生することがあり、単純にカレントプロセスにシグナルを送信するだけではない。

割り込みは大きく3種類に分類される。

  • I/O割り込み

    当該割り込みでは割り込みに対応する動作を決定するために割り込みハンドラからデバイスに対して問い合わせを行う必要がある。

  • タイマー割り込み

    ローカルAPICタイマや外部タイマなどが発生させる割り込みで、カーネルに一定時間が経過したことを通知するために用いられる。タイマ割り込みは一般的にはI/O割り込みとして処理される。

  • プロセッサ割り込み

    マルチプロセッサシステムでCPUが他のCPUに対して発行する割り込みである。

I/O割り込み

I/O割り込みハンドラには複数のデバイスを同時に扱うことができる柔軟性が必要である。PCIバスのアーキテクチャでは複数のデバイスが同じIRQラインを共有することがある。そのため割り込みベクタだけでは情報が不十分となる。

割り込みハンドラの柔軟性は以下の2通りの方法で実現されている。

  • IRQ共有(IRQ Sharing)

    割り込みハンドラは複数の割り込みルーチン(ISR: Interrupt Service Routine)を実行する。ISRはIRQラインを共有するデバイスの1つ対応しており、ISRがそのデバイスを処理すべきかを判断しIRQを発行したデバイスに対応する処理を行う。

  • IRQ動的割り当て(IRQ Dynamic Allocation)

    デバイスドライバIRQラインの割り当てを遅延し、デバイスを使用する時にのみIRQを割り当てることで同じIRQを使用するデバイスもベクタを共有できる。

割り込みハンドラには優先度があるが、割り込みハンドラ実行中は対応するIRQは一時的に使用不可なので時間のかかる処理は後回しにすべきである。加えて割り込まれたプロセスはTASK_RUNNING状態で保たなければならない。休止するとシステムがフリーズしてしまうためである。このためディスクI/Oなどの処理は行うことができない。

Linuxでは割り込み動作を以下の3種類に分類している。

  • 緊急

    PICへの返答、PICやデバイスコントローラの操作、デバイス及びプロセッサのデータを更新など。緊急の処理はマスク可能割り込みを禁止したまま割り込みハンドラが即座に行う。

  • 非緊急

    プロセッサが使用するデータの更新など。割り込みを許可したまま割り込みハンドラが即座を行う。

  • 非緊急で遅延可能

    バッファの内容をプロセスのアドレス空間に移すなど動作。(キーボードの行バッファを端末処理プロセスに送信する)。プロセスは送信されてくるデータを待っている。

割り込みを発生した回路の種類に関係なく全てのI/O割り込みは以下の共通動作が存在する。

  • カーネルスタックにIRQの値とレジスタの内容を退避する。
  • IRQラインに管理しているPICに割り込みを発行できるよう応答(ACK)を返す。
  • IRQを共有する全てのデバイスのISRを呼び出す。
  • ret_from_intr()を呼び、処理を終了する。

割り込みベクタ

物理IRQは32 ~ 238(n + 32)の範囲であればどのベクタにでも割り当てることができる。(128のみシステムコールに使用)

IBM PC互換機のアーキテクチャでは一部のデバイスは特定のIRQラインに接続する必要がある。

  • インターバルタイマはIRQ 0
  • スレーブ8259A PICはIRQ 2(現在はAPICが主流である)
  • 外部算術演算プロセッサはIRQ 13。
  • 一部のI/Oデバイスは限定されたIRQでの使用できた。

Linuxで使用される割り込みベクタを以下に示す。

ベクタ 用途
0~19 マスク不可割り込みと例外
20~31 Intelが予約
32~127 外部割り込み(IRQ)
128 システムコール
129~238 外部割り込み(IRQ)
239 ローカルAPICのタイマ割り込み
240 ローカルAPICの温度割り込み
241~250 Linuxが予約(将来の拡張のため)
251~253 プロセッサ間割り込み
254 ローカルAPICのエラー割り込み
255 ローカルAPICの不正割り込み

IRQの設定が可能ばデバイスIRQラインを選択する方法を以下に示す。

  • ハードウェアのジャンパ設定を利用する
  • バイス追加時にデバイス付属のユーティリティを利用する
  • システム起動時にハードウェアのプロトコルを用いて検出する

I/Oデバイスに対するIRQ割り当ての例を以下に示す。

IRQ INT バイス
0 32 タイマ
1 33 キーボード
2 34 PIC(カスケード接続)
3 35 2番目のシリアルポート
4 36 1番目のシリアルポート
6 38 フリッピーディスク
8 40 システムクロック
10 42 NIC
11 43 USBポート、サウンドカード
12 44 PS/2マウス
13 45 算術演算プロセッサ
14 46 EIDEディスクコントローラの1番目のチェイン
15 47 EIDEディスクコントローラの2番目のチェイン

カーネルIRQ番号に対応するデバイスの知っておく必要があるが、この対応情報はデバイスドライバ初期化時に決定する。

IRQのデータ構造

当該データ構造を以下に示す。

// include/linux/irq.h
/*
 * This is the "IRQ descriptor", which contains various information
 * about the irq, including what kind of hardware handling it has,
 * whether it is disabled etc etc.
 *
 * Pad this out to 32 bytes for cache and indexing reasons.
 */
typedef struct irq_desc {
    hw_irq_controller *handler;
    void *handler_data;
    struct irqaction *action;  /* IRQ action list */
    unsigned int status;      /* IRQ status */
    unsigned int depth;       /* nested irq disables */
    unsigned int irq_count;       /* For detecting broken interrupts */
    unsigned int irqs_unhandled;
    spinlock_t lock;
} ____cacheline_aligned irq_desc_t;

extern irq_desc_t irq_desc [NR_IRQS];

各メンバの詳細は以下。

メンバ 説明
handler IRQラインを制御するPICオブジェクト
handler_data PICメソッドが使用するデータへのポインタ
action IRQが発生した時に起動するISRを指定。IRQに対応しているirqactionの先頭ディスクリプタリストの先頭要素を返す
status IRQラインの状態
depth IRQラインが許可されていればdepthの値は0で、禁止されている場合には正の値
irq_count IRQラインで発生した割り込みのカウンタ
irqs_unhandled IRQラインで発生した処理されない割り込みのカウンタ
lock スピンロックを利用してIRQディスクリプタとPICへのアクセスを順次処理化

NR_IRQSは以下のように定義されている。

// include/asm-i386/mach-default/irq_vectors_limits.h
#ifdef CONFIG_X86_IO_APIC
#define NR_IRQS 224

statusは以下のように定義されており各ビットでフラグとして管理されていることがわかる。

// include/linux/irq.h
/*
 * IRQ line status.
 */
#define IRQ_INPROGRESS 1  /* IRQ handler active - do not enter! */
#define IRQ_DISABLED   2  /* IRQ disabled - do not enter! */
#define IRQ_PENDING    4  /* IRQ pending - replay on enable */
#define IRQ_REPLAY 8  /* IRQ has been replayed but not acked yet */
#define IRQ_AUTODETECT 16 /* IRQ is being autodetected */
#define IRQ_WAITING    32 /* IRQ not yet seen - for autodetection */
#define IRQ_LEVEL  64 /* IRQ level triggered */
#define IRQ_MASKED 128    /* IRQ masked - shouldn't be seen again */
#define IRQ_PER_CPU    256    /* IRQ is per CPU */

各フラグの詳細を以下に示す。

フラグ名 説明
IRQ_INPROGRESS 割り込みハンドラの実行中
IRQ_DISABLED IRQラインの禁止中
IRQ_PENDING IRQ発生後、PICに応答(ACK)を送信済みで且つカーネルが割り込みハンドラを実行していない状態
IRQ_REPLAY IRQラインは禁止され、以前発生したIRQに対応する応答(ACK)をPICに送信していない状態
IRQ_WAITING バイス検出処理のためカーネルIRQラインを使用中で、対象の割り込みを発生させていない状態
IRQ_LEVEL x86では未使用
IRQ_MASKED 未使用
IRQ_PER_CPU x86では未使用

irq_desc_tディスクリプタdepthメンバとstatusIRQラインの使用中、禁止中を示す。disable_irq()又はdisable_irq_nosync()がコールされる度にdepthをインクリメントする。depthが0だった場合にはIRQラインを禁止しstatusIRQ_DISABLEフラグを設定する。逆にenable_irq()はコールされる度にdepthをデクリメントする。depthが0になった時にIRQラインを許可しIRQ_DISABLEフラグを落とす。

先ほど見たirq_deschadlerメンバはhw_interrupt_typeであり、当該構造体は以下のように定義されている。

/*
 * Interrupt controller descriptor. This is all we need
 * to describe about the low-level hardware. 
 */
struct hw_interrupt_type {
    const char * typename;
    unsigned int (*startup)(unsigned int irq);
    void (*shutdown)(unsigned int irq);
    void (*enable)(unsigned int irq);
    void (*disable)(unsigned int irq);
    void (*ack)(unsigned int irq);
    void (*end)(unsigned int irq);
    void (*set_affinity)(unsigned int irq, cpumask_t dest);
};

hw_interrupt_typeはドライバから見た時の割り込みの発生源であるコントローラに接続されたPICをオブジェクトとして表現している。 上記の各メンバはIRQラインの動作に対応しており、起動(startup)や停止(shutdown)、有効/無効(enable/disable)、応答(ack)や割り込み終了時(end)などの処理を定義する。

例えばシングルプロセッサで2つの8259A PICを装備している(16個のIRQライン)場合、当該オブジェクトの初期化は以下のようになっている。

// arch/i386/kernel/i8259.c
static struct hw_interrupt_type i8259A_irq_type = {
    "XT-PIC",
    startup_8259A_irq,
    shutdown_8259A_irq,
    enable_8259A_irq,
    disable_8259A_irq,
    mask_and_ack_8259A,
    end_8259A_irq,
    NULL
};

複数のデバイスIRQラインを共有することができるため、カーネルは先ほど見たirq_descのメンバのactionであるirqactionディスクリプタを用いて、ハードウェアデバイスとその割り込みを管理する。

irqactionは以下のように定義されている。

struct irqaction {
    irqreturn_t (*handler)(int, void *, struct pt_regs *);
    unsigned long flags;
    cpumask_t mask;
    const char *name;
    void *dev_id;
    struct irqaction *next;
    int irq;
    struct proc_dir_entry *dir;
};

当該構造体のメンバの詳細を以下の示す。

メンバ 説明
handler I/Oデバイスに対応する割り込みルーチンへのポインタ
flags IRQラインとI/Oデバイスの関係を表す
name I/Oデバイスの名前/proc/interruptsで使用
dev_id I/Oデバイスを認識するために用いる
next irqactionディスクリプタリストの次の要素をさすポインタ。リスト内要素は同じIRQを共有するデバイスを参照する
irq IRQライン番号
dir IRQ nに対応する/proc/irq/nディレクトリのディスクリプタをさす

irqaction構造体のメンバであるflagsに代入されるフラグ値の定義は以下。

// include/asm-i386/signal.h
#define SA_INTERRUPT   0x20000000 /* dummy -- ignored */

/*
 * These values of sa_flags are used only by the kernel as part of the
 * irq handling routines.
 *
 * SA_INTERRUPT is also used by the irq handling routines.
 * SA_SHIRQ is for shared interrupt support on PCI and EISA.
 */
#define SA_PROBE       SA_ONESHOT
#define SA_SAMPLE_RANDOM   SA_RESTART
#define SA_SHIRQ       0x04000000

フラグの詳細を以下に示す。

名前 説明
SA_INTERRUPT 割り込みを禁止したまま、ハンドラを実行する必要がある
SA_SHIRQ 他のデバイスIRQラインの共有を許可する
SA_SAMPLE_RANDOM ランダムな事象の発生源とする。/dev/random/dev/urandomで使用されている

CPU毎に情報を保持するための構造体であるirq_statの定義を以下に示す。

// include/linux/irq_cpustat.h
extern irq_cpustat_t irq_stat[];       /* defined in asm/hardirq.h */

// include/asm-i386/hardirq.h
typedef struct {
    unsigned int __softirq_pending;
    unsigned long idle_timestamp;
    unsigned int __nmi_count; /* arch dependent */
    unsigned int apic_timer_irqs; /* arch dependent */
} ____cacheline_aligned irq_cpustat_t;

irq_cpustat_tのメンバの詳細を以下に示す。

メンバ名 説明
__softirq_pending 保留中のソフト割り込みを示すフラグ
idle_timestamp CPUがidle状態になった時刻
__nmi_count NMI割り込みの発生回数
apic_timer_irqs ローカルAPICのタイマ割り込み発生回数

マルチプロセッサでのIRQ分散について

カーネルはハードウェアからの割り込みをラウンドロビン方式で分配する。システム起動処理でsetup_IO_APIC_irqs()を用いてI/O APICの初期化を行い、全てのCPUがsetup_local_APIC()でTPR(Task Priority Register)を固定値に設定することで、CPUは自身の優先度に関わらず全てのIRQ信号を処理できるようになる。マルチAPICシステムではローカルAPICの調停優先度レジスタ(APR)の値を利用することでIRQ信号は全てのCPUに均等に分配される。

ハードウェアデバイスIRQ信号を生成するとマルチAPICシステムは単一のCPUを選択肢、当該CPUのローカルAPICに信号を送信する。IRQ信号を受信したAPICは対応するCPUに割り込みを生成する。

しかしハードウェアでの分配はしばし失敗することがあるのでカーネルkirqdというカーネルスレッドでCPUへのIRQ割り当てを調整する。kirqdはマルチAPICシステムのIRQアフィニティという機能を用いる。当該機能ではI/O APICの割り込み転送テーブルを変更することで特定のCPUに割り込み信号を分配することができる。set_ioapic_affinity_irq()という関数を用いるか、/proc/irq/N/smp_affinity(N=割り込みベクタ)にCPUマスク値を書き込むことで変更できる。kirqdは周期的にdo_irq_balanceを実行し全てのCPUが受信した割り込みを発生回数を把握し、必要であれば負荷分散を行う。

複数のカーネルスタック

プロセスのthread_info構造体はカーネルモードスタックと共用体(thread_union)として扱われる。カーネルスタック(thread_union)のサイズはコンパイル時のオプションで指定でき、8KBの場合例外および割り込み、遅延処理はカレントプロセスのカーネルスタックをを使用するが、4KBの時は以下の3種類のカーネルスタックを使用する。

  • 例外スタック

    例外を処理する際に使用。プロセスのthread_unionに含まれる。カーネルはプロセス毎に異なる例外スタックを使用する。

  • ハードIRQスタック

    割り込みを処理する際に使用。システム上のCPU毎に単一のハードIRQスタックを割り当てる。当該スタックは単一のページフレームに収まる(今回だと4KB)。

  • ソフトIRQスタック

    遅延処理(ソフト割り込み、 タスクレット)を実行する時に使用。システム上のCPU毎に単一のハードIRQスタックを割り当てる。当該スタックは単一のページフレームに収まる(今回だと4KB)。

全てのハードIRQスタックはhardirq_stackに、全てのソフトIRQスタックはsoftirq_stackに格納される。以下を見るとirq_ctxthread_info構造体と同じストラクチャーになっているのがわかる。

// arch/i386/kernel/irq.c
/*
 * per-CPU IRQ handling contexts (thread information and stack)
 */
union irq_ctx {
    struct thread_info      tinfo;
    u32                     stack[THREAD_SIZE/sizeof(u32)];
};

static char softirq_stack[NR_CPUS * THREAD_SIZE]
        __attribute__((__aligned__(THREAD_SIZE)));

static char hardirq_stack[NR_CPUS * THREAD_SIZE]
        __attribute__((__aligned__(THREAD_SIZE)));

上記の配列の要素はirq_ctx_init()のコードを見てもわかる通り、irq_ctx型の共用体となっている。

// arch/i386/kernel/irq.c
/*
 * allocate per-cpu stacks for hardirq and for softirq processing
 */
void irq_ctx_init(int cpu)
{
    union irq_ctx *irqctx;

    if (hardirq_ctx[cpu])
        return;

    irqctx = (union irq_ctx*) &hardirq_stack[cpu*THREAD_SIZE];
    irqctx->tinfo.task              = NULL;
    irqctx->tinfo.exec_domain       = NULL;
    irqctx->tinfo.cpu               = cpu;
    irqctx->tinfo.preempt_count     = HARDIRQ_OFFSET;
    irqctx->tinfo.addr_limit        = MAKE_MM_SEG(0);

    hardirq_ctx[cpu] = irqctx;

    irqctx = (union irq_ctx*) &softirq_stack[cpu*THREAD_SIZE];
    irqctx->tinfo.task              = NULL;
    irqctx->tinfo.exec_domain       = NULL;
    irqctx->tinfo.cpu               = cpu;
    irqctx->tinfo.preempt_count     = SOFTIRQ_OFFSET;
    irqctx->tinfo.addr_limit        = MAKE_MM_SEG(0);

    softirq_ctx[cpu] = irqctx;

    printk("CPU %u irqstacks, hard=%p soft=%p\n",
        cpu,hardirq_ctx[cpu],softirq_ctx[cpu]);
}

hardirq_stack及びsoftirq_stackによってカーネルは即座にCPUに対応するハードIRQスタック及びソフトIRQスタックを求めることが可能となる。

割り込みハンドラのレジスタ退避処理

CPUが割り込みを受け取ると対応するIDTゲートに格納されているアドレスを参照してコードを実行する。レジスタの退避及び復帰処理はアセンブリで記述されている。

レジスタの退避処理は割り込みハンドラの最初の仕事でIRQ nに対応する割り込みハンドラのアドレスはinterrupt配列のinterrupt[n]に格納されている。interriptNR_IRQSの数エントリを保持する。APICの場合には224個(256個のうち32個はCPU用に予約されている)、8259A PICの場合には16個となる。interruptのエントリが持つ処理は以下のように定義されている。

// arch/i386/kernel/entry.S
/*
 * Build the entry stubs and pointer table with
 * some assembler magic.
 */
.data
ENTRY(interrupt)
.text

vector=0
ENTRY(irq_entries_start)
.rept NR_IRQS
    ALIGN
1: pushl $vector-256
    jmp common_interrupt
.data
    .long 1b
.text
vector=vector+1
.endr

.rept~.endr間にあるコードが繰り返す形で実行されることで以下のようになる。*1

vector=0
ENTRY(irq_entries_start)

    ALIGN
1: pushl $vector-256 # $vector == 0
    jmp common_interrupt
.data
    .long 1b
.text
vector=vector+1

1: pushl $vector-256 # $vector == 1
    jmp common_interrupt
.data
    .long 1b
.text
vector=vector+1

1: pushl $vector-256 # $vector == 2
    jmp common_interrupt
.data
    .long 1b
.text
vector=vector+1

vector=vector+1がインクリメントされる形でNR_IRQS個のエントリ分が作成される。

ベクタ番号から256引いているのは($vector-256)IRQを負の値で表現するためで、これはシステムコールを正の値で予約しているからである。全ての割り込みハンドラはこの値を見て動作する。

// arch/i386/kernel/entry.S
#define SAVE_ALL \
   cld; \
   pushl %es; \
   pushl %ds; \
   pushl %eax; \
   pushl %ebp; \
   pushl %edi; \
   pushl %esi; \
   pushl %edx; \
   pushl %ecx; \
   pushl %ebx; \
   movl $(__USER_DS), %edx; \
   movl %edx, %ds; \
   movl %edx, %es;

:
(省略)
:

common_interrupt:
    SAVE_ALL
    movl %esp,%eax
    call do_IRQ
    jmp ret_from_intr

common_interruptは割り込みハンドラで共通となっている。SAVE_ALLマクロでレジスタ群を退避し、ユーザセグメントをds及びesに読み込む。SAVE_ALLマクロの後はスタックポインタ(esp)をeaxに保存し、do_IRQ()を呼び出す。当該関数の終了後、ret_from_intr()へと処理が遷移する。

do_IRQ()

定義は以下のようになっており、先ほど見たSAVE_ALLマクロで、スタックに保存したレジスタ値のアドレスを持っているスタックポインタ(esp)をeaxに代入した。その値を引数として受け取っている。fastcallは以下のように__attribute__((regparm(3)))と定義されており関数の引数をレジスタを用いて渡す際に使用される。*3

do_IRQ()pt_regs構造体を引数にとっているが、先ほどのSAVE_ALLマクロとpt_regs構造体を見比べると、SAVE_ALLマクロでスタックに積まれたレジスタの値がpt_regs構造体の順になっているのがわかる。

// include/asm-i386/linkage.h
#define fastcall   __attribute__((regparm(3)))

// linux-2.6.11/include/asm-i386/ptrace.h
struct pt_regs {
    long ebx;
    long ecx;
    long edx;
    long esi;
    long edi;
    long ebp;
    long eax;
    int  xds;
    int  xes;
    long orig_eax;
    long eip;
    int  xcs;
    long eflags;
    long esp;
    int  xss;
};

// arch/i386/kernel/irq.c
/*
 * do_IRQ handles all normal device IRQ's (the special
 * SMP cross-CPU interrupts have their own specific
 * handlers).
 */
fastcall unsigned int do_IRQ(struct pt_regs *regs)
:
(省略)
:

SAVE_ALLマクロではebxからxesのみを保存しているように見えるが、実際には割り込みハンドラのレジスタ退避処理(common_interrupt:にジャンプする前のENTRY(irq_entries_start)の部分)で行われていたpushl $vector-256orig_eaxとなり、割り込みが起こった際にCPUの制御回路によりeipからxssが積まれる。これによりpt_regs構造体のデータを生成している。

関数本体の処理は以下のようになっている。

// arch/i386/kernel/irq.c
fastcall unsigned int do_IRQ(struct pt_regs *regs)
{   
    /* high bits used in ret_from_ code */
    int irq = regs->orig_eax & 0xff;
#ifdef CONFIG_4KSTACKS
    union irq_ctx *curctx, *irqctx;
    u32 *isp;
#endif

    irq_enter();
#ifdef CONFIG_DEBUG_STACKOVERFLOW
    /* Debugging check for stack overflow: is there less than 1KB free? */
    {
        long esp;

        __asm__ __volatile__("andl %%esp,%0" :
                    "=r" (esp) : "0" (THREAD_SIZE - 1));
        if (unlikely(esp < (sizeof(struct thread_info) + STACK_WARN))) {
            printk("do_IRQ: stack overflow: %ld\n",
                esp - sizeof(struct thread_info));
            dump_stack();
        }
    }
#endif

#ifdef CONFIG_4KSTACKS

    curctx = (union irq_ctx *) current_thread_info();
    irqctx = hardirq_ctx[smp_processor_id()];

    /*
    * this is where we switch to the IRQ stack. However, if we are
    * already using the IRQ stack (because we interrupted a hardirq
    * handler) we can't do that and just have to keep using the
    * current stack (which is the irq stack already after all)
    */
    if (curctx != irqctx) {
        int arg1, arg2, ebx;

        /* build the stack frame on the IRQ stack */
        isp = (u32*) ((char*)irqctx + sizeof(*irqctx));
        irqctx->tinfo.task = curctx->tinfo.task;
        irqctx->tinfo.previous_esp = current_stack_pointer;

        asm volatile(
            "       xchgl   %%ebx,%%esp      \n"
            "       call    __do_IRQ         \n"
            "       movl   %%ebx,%%esp      \n"
            : "=a" (arg1), "=d" (arg2), "=b" (ebx)
            :  "0" (irq),   "1" (regs),  "2" (isp)
            : "memory", "cc", "ecx"
        );
    } else
#endif
        __do_IRQ(irq, regs);

    irq_exit();

    return 1;
}

上記ではまずirq_enter()で、カレントプロセスのthread_info構造体のメンバであるpreempt_countの値を増加させる。これにより割り込みハンドラのネスト数をカウントする。

irq_enter();

次にCONFIG_DEBUG_STACKOVERFLOWカーネルスタックに十分なスペースがあることを確認している。

Make extra checks for space avaliable on stack in some critical functions. This will cause kernel to run a bit slower, but will catch most of kernel stack overruns and exit gracefuly. Say Y if you are unsure. 引用: *2

#ifdef CONFIG_DEBUG_STACKOVERFLOW
    /* Debugging check for stack overflow: is there less than 1KB free? */
    :
    (省略)
    :
#endif

CONFIG_4KSTACKS内ではカーネルスタックが4KBだった際の処理が行われおり、具体的には以下のような処理が行われる。

  • current_thread_info()thread_infoのアドレスを取得する。
curctx = (union irq_ctx *) current_thread_info();

current_thread_info()ではスタックポインタを切り上げることでthread_union共用体の先頭アドレスにあるthread_info構造体のポインタを取得している。

// include/linux/sched.h
union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

// include/asm-i386/thread_info.h
/* how to get the thread information struct from C */
static inline struct thread_info *current_thread_info(void)
{
    struct thread_info *ti;
    __asm__("andl %%esp,%0; ":"=r" (ti) : "0" (~(THREAD_SIZE - 1)));
    return ti;
}
  • 現在実行中のCPUに対応するハードIRQスタックを取得する。
irqctx = hardirq_ctx[smp_processor_id()];

smp_processor_id()は以下のように定義されており、thread_info構造体からメンバであるcpuを取得しているのがわかる。

// include/linux/smp.h
#define smp_processor_id() __smp_processor_id()

// include/asm-i386/smp.h
#define __smp_processor_id() (current_thread_info()->cpu)
  • 現在使用しているthread_infoとハードIRQスタックを比較し異なれば、現在使用しているカーネルスタックをハードIRQスタックをスイッチし、__do_IRQ()を呼び出す。
if (curctx != irqctx) {
    int arg1, arg2, ebx;

    /* build the stack frame on the IRQ stack */
    isp = (u32*) ((char*)irqctx + sizeof(*irqctx));
    irqctx->tinfo.task = curctx->tinfo.task;
    irqctx->tinfo.previous_esp = current_stack_pointer;

    asm volatile(
        "       xchgl   %%ebx,%%esp      \n"
        "       call    __do_IRQ         \n"
        "       movl   %%ebx,%%esp      \n"
        : "=a" (arg1), "=d" (arg2), "=b" (ebx)
        :  "0" (irq),   "1" (regs),  "2" (isp)
        : "memory", "cc", "ecx"
    );
} else

コメントにもあるが、割り込みハンドラ実行中に割り込みハンドラが呼ばれる場合もあり、curctxirqctxを比較した際に同一のものであった(割り込みハンドラのネストが発生している)場合には、カレントプロセスのカーネルスタックをそのまま使用する。

curctxirqctxを比較した際に同一のものでなかった(割り込みハンドラのネストが発生していない)場合、そのまま切り替えを行わず__do_IRQ()を呼び出す。

__do_IRQ(irq, regs);

最後にirq_exit()を呼び出し、割り込みカウンタを減らす。

irq_exit();

__do_IRQ()

fastcall unsigned int __do_IRQ(unsigned int irq, struct pt_regs *regs)
{
    irq_desc_t *desc = irq_desc + irq;
    struct irqaction * action;
    unsigned int status;

    kstat_this_cpu.irqs[irq]++;
    if (desc->status & IRQ_PER_CPU) { // x86では使用されない
        irqreturn_t action_ret;

        desc->handler->ack(irq);
        action_ret = handle_IRQ_event(irq, regs, desc->action);
        if (!noirqdebug)
            note_interrupt(irq, desc, action_ret);
        desc->handler->end(irq);
        return 1;
    }

    spin_lock(&desc->lock);
    desc->handler->ack(irq);
    status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
    status |= IRQ_PENDING; /* we _want_ to handle it */

    action = NULL;
    if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
        action = desc->action;
        status &= ~IRQ_PENDING; /* we commit to handling */
        status |= IRQ_INPROGRESS; /* we are handling it */
    }
    desc->status = status;

    if (unlikely(!action))
        goto out;

    for (;;) {
        irqreturn_t action_ret;

        spin_unlock(&desc->lock);

        action_ret = handle_IRQ_event(irq, regs, action);

        spin_lock(&desc->lock);
        if (!noirqdebug)
            note_interrupt(irq, desc, action_ret);
        if (likely(!(desc->status & IRQ_PENDING)))
            break;
        desc->status &= ~IRQ_PENDING;
    }
    desc->status &= ~IRQ_INPROGRESS;

out:
    desc->handler->end(irq);
    spin_unlock(&desc->lock);

    return 1;
}

if (desc->status & IRQ_PER_CPU) {の部分はx86では使用されないため割愛する。

まず以下の箇所では複数のCPUから同時にアクセスが起こる可能性があるため当該ディスクリプタのスピンロック(*4)を取得している。そしてPICの割り込みに応答(desc->handler->ack(irq))する。(8259A PICでは対応するIRQラインを禁止し、APICではハンドラの前処理を行う。これはAPICでは割り込みハンドラの処理が実行されるまで同じ割り込みを受け付けないため)、ステータスをIRQ_PENDINGに変更する(実際割り込みハンドラはIDTの割り込みゲートを用いて呼び出されるため、CPUの制御回路が自動的にeflagレジスタのIFフラグを0(割り込み禁止)にしている)。IRQ_PENDINGは応答は行なったが実際の処理は行なっていないことを示している。

spin_lock(&desc->lock);
desc->handler->ack(irq);
/*
 * REPLAY is when Linux resends an IRQ that was dropped earlier
 * WAITING is used by probe to mark irqs that are being tested
 */
status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
status |= IRQ_PENDING; /* we _want_ to handle it */

以下の箇所は割り込みを実行しない場合の処理となる。まずステータスがIRQ_DISABLED若しくはIRQ_INPROGRESSの場合、そしてactionがNULLの場合割り込み処理を行わない。

action = NULL;
if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
    action = desc->action;
    status &= ~IRQ_PENDING; /* we commit to handling */
    status |= IRQ_INPROGRESS; /* we are handling it */
}
desc->status = status;

/*
 * If there is no IRQ handler or it was disabled, exit early.
 * Since we set PENDING, if another processor is handling
 * a different instance of this same irq, the other processor
 * will take care of it.
 */
if (unlikely(!action))
    goto out;

以下に上記の割り込みを行わない三種類のパターンを示す。

  • IRQ_DISABLED

    発生したIRQラインが禁止されている状況でCPUが__do_IRQ()を呼び出す場合。これはバグのあるマザーボードなどのための処理で、PICに対応するIRQラインを禁止しても不正な割り込みを発生させることがある。

  • IRQ_INPROGRESS

    マルチプロセッサシステムでは既に発生している割り込みと同一の割り込みをCPUが受けとる場合があり、Linuxでは2つ目の割り込みを遅延処理する形をとっている。これはデバイスドライバを再入可能にする必要がなく(アーキテクチャがシンプルになる)、且つ割り込みを行わないCPUは元の処理に戻るためハードウェアキャッシュを汚す必要がなくシステム性能向上にも繋がる。

  • actionがNULL

    割り込みに対応するISR(Interrupt Service Routinue)が存在しない場合に起こる。

上記のいずれにも該当しない場合にはステータスをIRQ_INPROGRESSに変更する。

for (;;) {
    irqreturn_t action_ret;

    spin_unlock(&desc->lock);

    action_ret = handle_IRQ_event(irq, regs, action);

    spin_lock(&desc->lock);
    if (!noirqdebug)
        note_interrupt(irq, desc, action_ret);
    if (likely(!(desc->status & IRQ_PENDING)))
        break;
    desc->status &= ~IRQ_PENDING;
}
desc->status &= ~IRQ_INPROGRESS;

最後に以下の箇所。ここではスピンロックを解放し(ステータスを外部からも変更できるよう)、handle_IRQ_event()を実行する。完了後最後ロックを取得し、ステータスがIRQ_PENDING出なかった場合にはループを抜け、再度IRQ_PENDINGになっていた場合にはループを抜けずもう一度handle_IRQ_event()を実行する。

for (;;) {
    irqreturn_t action_ret;

    spin_unlock(&desc->lock);

    action_ret = handle_IRQ_event(irq, regs, action);

    spin_lock(&desc->lock);
    if (!noirqdebug)
        note_interrupt(irq, desc, action_ret);
    if (likely(!(desc->status & IRQ_PENDING)))
        break;
    desc->status &= ~IRQ_PENDING;
}
desc->status &= ~IRQ_INPROGRESS;

失われた割り込み

CPUがIRQを許可している状態で割り込みを受け取り(IRQ_PENDING)、応答を返す前に他のCPUによってそのIRQが禁止された(IRQ_DISABLED)場合、割り込みハンドラが実行されず処理が終了してしまうことがある。この問題を対処するためカーネルIRQを許可する際に使用する関数であるenable_irq()では割り込みが禁止され、尚且つIRQ_PENDINGが立っている際には対応する割り込みが失われたと判断する。(IRQ_PENDINGは割り込みハンドラを実行する前に落とされるため)

// kernel/irq/manage.c
void enable_irq(unsigned int irq)
{
    irq_desc_t *desc = irq_desc + irq;
    unsigned long flags;

    spin_lock_irqsave(&desc->lock, flags);
    switch (desc->depth) {
    case 0:
        WARN_ON(1);
        break;
    case 1: {
        unsigned int status = desc->status & ~IRQ_DISABLED;

        desc->status = status;
        if ((status & (IRQ_PENDING | IRQ_REPLAY)) == IRQ_PENDING) {
            desc->status = status | IRQ_REPLAY;
            hw_resend_irq(desc->handler,irq);
        }
        desc->handler->enable(irq);
        /* fall-through */
    }
    default:
        desc->depth--;
    }
    spin_unlock_irqrestore(&desc->lock, flags);
}

処理されていない割り込みがある場合にはhw_resend_irq()で強制的に対応する割り込みを発生させる。

handle_IRQ_event()

__do_IRQ()から呼び出されていたhandle_IRQ_event()を見ていく。

// kernel/irq/handle.c
/*
 * Have got an event to handle:
 */
fastcall int handle_IRQ_event(unsigned int irq, struct pt_regs *regs,
                struct irqaction *action)
{
    int ret, retval = 0, status = 0;

    if (!(action->flags & SA_INTERRUPT))
        local_irq_enable();

    do {
        ret = action->handler(irq, action->dev_id, regs);
        if (ret == IRQ_HANDLED)
            status |= action->flags;
        retval |= ret;
        action = action->next;
    } while (action);

    if (status & SA_SAMPLE_RANDOM)
        add_interrupt_randomness(irq);
    local_irq_disable();

    return retval;
}

まずSA_INTERRUPTが設定されていない場合にはlocal_irq_enable()でローカルCPUに対する割り込みを許可する。

// kernel/irq/manage.c
/*
 * SA_SHIRQ        Interrupt is shared
 * SA_INTERRUPT        Disable local interrupts while processing
 * SA_SAMPLE_RANDOM    The interrupt can be used for entropy
 */ 

次にループ内でirqaction構造体のメンバであるhandler()を実行する。引数は次の3つで以下のように使用される。

名前 用途
irq IRQ番号
dev_id バイス番号
regs pt_regs構造体

IRQ番号があるので単一のISRで複数のIRQラインを扱うことが可能となり、デバイスIDがあることでISRを共有しているデバイスを複数扱うことが可能となる。

actionirqaction構造体リストの先頭を指しており、action->nextにデータが存在すれば(リストに要素が残っていれば)次のirqactionを取得し処理を行う。

ISRでは実際に処理を行なった場合に1を返し、処理を行わなかった場合には0を返すことで割り込みカウンタを更新することができる。

最後にlocal_irq_disable()でローカルCPUに対する割り込みを禁止する。

IRQラインの動的割り当て

ベクタの一部は特定のデバイス用に予約されているが、それ以外は動的に割り当てを行う。一度に単一のデバイスのみにIRQラインの使用を許可することで、複数のデバイスIRQラインを共有することが可能となる。

IRQラインを使用するデバイスを動作状態にする際には以下のような手順を踏む。

  • request_irq()で新しいirqactionディスクリプタを作成し、受け取った引数で初期化する。

    以下はシリアルのドライバのものとなる。

request_irq(state->irq, rs_360_interrupt,
                        IRQ_FLG_LOCK, "ttyS", (void *)info);
  • setup_irq()ディスクリプタを適切なIRQリストに繋ぎ、ハンドラのstartup()を呼び出しIRQ信号を許可する。

    上記とはまた別だがtimerirqactionを登録している。

static struct irqaction irq0  = { timer_interrupt, SA_INTERRUPT, CPU_MASK_NONE, "timer", NULL, NULL};

void __init time_init_hook(void)
{
    setup_irq(0, &irq0);
}

setup_irq()がエラーを返す場合はそのIRQラインは他のデバイスによって使用中であるということを意味している。

ドライバの処理が終了した際にはfree_irq()を呼び出し、IRQリストからディスクリプタを削除しメモリ領域を解放する。

IPI(Inter processor interrupt: プロセッサ間割り込み)

IPIを使用するとあるCPUからシステム内の他のCPUに割り込み信号を送ることができる。IRQラインは使用せずCPUのローカルAPICにバスを返して直接送信される。

マルチプロセッサシステムでは以下の3種類のIPIを実行している。

名前 説明
CALL_FUNCTION_VECTOR 送り元CPU以外のCPUで指定した関数を実行する。他のCPUを停止したり、メモリタイプレンジレジスタ(Memory Type Range Register)を設定を行なったりするのに用いられる。
RESCHEDULE_VECTOR 割り込み応答を返し、際スケジューリング処理を行う。
INVALIDATE_TLB_VECTOR 送り元CPU以外のCPUのTLBを無効にする。

各値は以下のように定義されており、IPIの処理はBUILD_INTERRUPTマクロによりた対応する関数を結び付けられる。

// include/asm-i386/mach-default/irq_vectors.h
#define INVALIDATE_TLB_VECTOR  0xfd
#define RESCHEDULE_VECTOR  0xfc
#define CALL_FUNCTION_VECTOR   0xfb

// include/asm-i386/mach-default/entry_arch.h
#ifdef CONFIG_X86_SMP
BUILD_INTERRUPT(reschedule_interrupt,RESCHEDULE_VECTOR)
BUILD_INTERRUPT(invalidate_interrupt,INVALIDATE_TLB_VECTOR)
BUILD_INTERRUPT(call_function_interrupt,CALL_FUNCTION_VECTOR)
#endif

BUILD_INTERRUPTは以下のように定義されている。

// arch/i386/kernel/entry.S
#define BUILD_INTERRUPT(name, nr)  \
ENTRY(name)                \
   pushl $nr-256;         \
   SAVE_ALL            \
   movl %esp,%eax;         \
   call smp_/**/name;      \
   jmp ret_from_intr;

ベクタ番号から256を引いたものをスタックへ積み、SAVE_ALLマクロでpt_regs構造体を作成している(他のレジスタ値はCPUの制御回路でスタックに保存済み)。

call smp_/**/name;では"smp_"とnameを連結しており、reschedule_interruptであればsmp_reschedule_interrupt()が呼ばれることになる。

// arch/i386/kernel/smp.c
/*
 * Reschedule call back. Nothing to do,
 * all the work is done automatically when
 * we return from the interrupt.
 */
fastcall void smp_reschedule_interrupt(struct pt_regs *regs)
{
    ack_APIC_irq();
}

IPIは以下の関数を利用して送信することができる。

関数名 用途
send_IPI_all 全てのCPUにIPIを送信する
send_IPI_allbutself 送り元CPU以外の全てのCPUにIPIを送信する
send_IPI_self 自CPUにIPIを送信する
send_IPI_mask ビットマスクで指定したCPUグループにIPIを送信する

参考文献

  1. http://d.hatena.ne.jp/yamanetoshi/20060510/1147238630
  2. https://cateee.net/lkddb/web-lkddb/DEBUG_STACKOVERFLOW.html
  3. http://d.hatena.ne.jp/Yusuke_Yamamoto/20070701
  4. https://tech.nikkeibp.co.jp/it/article/Keyword/20070207/261219/