Linuxカーネルに関する技術情報を集めていくプロジェクトです。現在、Linuxカーネル2.6解読室の第2章までを公開中。
それでは初めに、応答性を必要とするハードウェア割り込みの実装から見ていきましょう。
Linuxカーネルが割り込みとして扱うものには、表2-2のものがあります(図2-1)。
割り込みの種類 | 説明 |
外部装置割り込み | SCSIホストアダプタ、ネットワークカード、端末装置などが生成する割り込み。その割り込みに対応するデバイスドライバの割り込みハンドラを起動する |
タイマー割り込み | タイマーコントローラが一定周期で割り込みを発生させる。Linuxでは2種類のタイマー割り込み(グローバルタイマー割り込み、ローカルタイマー割り込み)を利用している。この割り込みに関しては、「第4章 時計」で詳しく説明 |
プロセッサ間割り込み | マルチプロセッサ環境において、ほかのCPUへ何らかの事象を通知するために利用される。割り込み要求を受けたCPUは、その事象に対応する処理を実行する |
NMI(マスク不可割り込み) | 通常の割り込みは、マスクすることによって、CPUが割り込みを受け取ることを抑制できるが、NMIはマスクできない。通常は、緊急時の障害対応目的で利用される |
このほか、割り込みによく似たものとして「例外」があります。割り込みが外的要因であるのに対し、CPUの動作自体によって引き起こされた事象の場合を「例外」と呼びます。CPU内部で発生した事象(0除算などの数値演算例外や、ページアクセス違反例外など)を検出すると、その時点で動作していた処理を中断して、例外ハンドラを起動します。ただし、CPUアーキテクチャによって、実装として割り込みと区別しているものや、同列に扱っているものがあるので、ここでは例外について、これ以上触れないことにします。
では、LinuxカーネルのTCP/IPスタックを例に、実際の割り込み処理の動作を見てみましょう。図2-2のように、ネットワークカード上のコントローラ操作に関する処理のみ、割り込みハンドラで実行します。パケットのヘッダー解析など、すぐに実行せずとも済む処理はソフト割り込みハンドラが担当します。ソフト割り込みハンドラ処理中であっても、非同期に発生するネットワークカードからの割り込み要求に応じて割り込みハンドラが起動され、送受信処理を行います。
Linuxカーネルは、ハードウェア割り込みをデバイスドライバへの通知手段として利用しています。Linuxカーネルの割り込み管理機能は、発生した割り込みの種別(IRQ)を判別し、目的のデバイスドライバが用意している割り込みハンドラを呼び出します。
割り込み処理はCPUやハードウェアアーキテクチャに依存した処理が多くなります。以降の説明では、IntelCPUとPC/AT互換機ハードウェア用のLinuxを前提として話を進めていきます。
Linuxカーネルでは、発生した割り込み種別(IRQ)ごとに、起動する割り込みハンドラを登録できます。Linuxカーネル内に割り込みを管理するためのirq_descテーブルを用意し、このテーブルに割り込みハンドラを登録する構造になっています(図2-3)。Linuxカーネルでは、1つのIRQを複数のデバイスで共有できるよう、ある1つのIRQに対応するirq_descテーブルのエントリに、複数の割り込みハンドラを登録できる構造になっています。
Linuxカーネルは、起動時またはハードウェアのホットプラグ時に、ハードウェア構成に合わせて割り込みハンドラを登録します(表2-3)。各割り込みには特定のIRQが割り当てられ、irq_descテーブルのそのIRQに対応するエントリに割り込みハンドラが登録されます。
関数名 | 説明 |
request_irq | 指定したIRQに対する割り込みハンドラを登録する。引数で割り込みハンドラの属性を指定できる |
SA_INTERRUPT | 割り込みハンドラ実行中に、別の割り込みを受け付け可能。割り込みハンドラ実行のネストを許す |
SA_SHIRQ | 1つのIRQをほかのデバイスと共有可能 |
SA_SAMPLE_RANDOM | この割り込みを、乱数生成に利用する。本来の割り込みハンドラ起動のほかに、不定期に発生する割り込みを利用して乱数を生成する |
free_irq | 指定したIRQに対する割り込みハンドラの登録を解除する |
Linuxカーネルは、システムコール処理と割り込みハンドラの間で競合する資源を排他するために、割り込み禁止の状態を利用します。割り込みを禁止する方法は2種類存在し、CPUレベルで禁止するものと、割り込みコントローラレベル*1で禁止するものがあります。
CPUレベルで禁止した場合、発生したすべての割り込みは、CPUが割り込みを許可するまで(割り込み禁止状態を解除するまで)、そのCPUが割り込みを受けることはありません。マルチプロセッサ環境では、ほかのCPUがその割り込みを受け付け、割り込みハンドラを起動することはあります(表2-4)。
関数名 | 説明 |
local_irq_disable | この関数を呼び出したCPUに対する割り込みを禁止する。ほかのCPUへの割り込みは発生する。古いLinuxカーネルではcli関数という名称であったが、一般的な名称に変更された。 |
local_irq_enable | この関数を呼び出したCPUに対する割り込みを許可する。古いLinuxカーネルではsti関数という名称であったが、一般的な名称に変更された。 |
local_save_flags | この関数を呼び出したCPUの割り込み禁止/許可の状態を取得する。 |
local_irq_restore | この関数を呼び出したCPUの割り込み禁止/許可の状態を、指定した値に変更する。local_save_flags関数と対で利用する |
local_irq_save | local_save_flags関数に続けて、local_irq_disable関数を呼び出したものと等価 |
割り込みコントローラレベルで割り込みを禁止した場合、指定したIRQに対する割り込み要求は、割り込みコントローラ内に保留されます。割り込みが許可されるまで、どのCPUに対しても指定したIRQの割り込みが通知されることはありません(表2-5)。
関数名 | 説明 |
disable_irq | 指定した割り込み種別IRQに対応する割り込みを禁止する。ほかのCPU上でIRQに対応する割り込みハンドラが実行中であった場合、その割り込みハンドラの終了を待ち合わせる |
disable_irq_nosyncq | 指定した割り込み種別IRQに対応する割り込みを禁止する。ほかのCPU上でIRQに対応する割り込みハンドラが実行中であっても、とくに終了の待ち合わせは行わない |
enable_irq | 割り込み種別IRQに対応する割り込みを許可(禁止を解除)する |
またLinuxカーネル2.6は、Linuxカーネル2.4で提供されていたシステムグローバルなCPUレベルの割り込み禁止(global_irq_cli関数、global_irq_sli関数)を廃止しました。
CPUレベルでの割り込み禁止では、local_irq_disable関数を呼び出したCPU上での割り込み発生、つまり割り込みハンドラの実行を抑制できますが、ほかのCPUに対しては、割り込みが発生し、割り込みハンドラが動作することがあります。それに対しシステムグローバルなCPUレベルの割り込み禁止では、ほかのCPUでも割り込みハンドラが実行されないことが保証されます。
この仕組みは非常に便利なのですが、オーバーヘッド非常に大きいという問題点がありました。そのため、Linuxカーネル2.6では、この仕組みを利用しているコードは、スピンロックを利用するように全面的に書き直し、システムグローバルなCPUレベルの割り込み禁止機能は削除されることになりました。
外部装置は、何らかの状態変化(I/O完了や新しいデータの受信など)を知らせるために、割り込みコントローラに対して割り込み要求を行います。割り込みコントローラは、そのデバイスからの割り込みが禁止されていなければ、CPUに対して割り込み要求を行います(図2-4)。
CPUがハードウェア割り込みを受け付けると、割り込み発生前に実行されていた処理を中断して、割り込みエントリ関数do_IRQ関数を呼び出します。
Intel x86で割り込み発生時にdo_IRQ関数が呼び出されるようにするには、IDT(Interrupt Descriptor Table)と呼ばれるテーブルを適切に初期化しておく必要があります。具体的には、外部ハードウェアからの割り込みとして利用する割り込み番号(IRQと対応しますが、IRQそのものではありません)に対応するIDTのエントリに、do_IRQ関数を呼び出す処理を登録しておきます。
ここで、割り込みハンドラ制御の中心となるdo_IRQ関数を見てみましょう(リスト2-1)。
- fastcall unsigned int do_IRQ(struct pt_regs *regs)
- {
- int irq = regs->orig_eax & 0xff; ――<1>
- irq_enter(); ――<2>
- __do_IRQ(irq, regs);
- irq_exit(); ――<3>
- return 1;
- }
- fastcall unsigned int __do_IRQ(unsigned int irq, struct pt_regs *regs)
- {
- irq_desc_t *desc = irq_desc + irq; ――<4>
- struct irqaction * action;
- unsigned int status;
- kstat_this_cpu.irqs[irq]++;
- if (CHECK_IRQ_PER_CPU(desc->status)) {
- irqreturn_t action_ret;
- if (desc->handler->ack)
- desc->handler->ack(irq);
- action_ret = handle_IRQ_event(irq, regs, desc->action); ――<5>
- desc->handler->end(irq);
- return 1;
- }
- spin_lock(&desc->lock);
- if (desc->handler->ack)
- desc->handler->ack(irq); ――<6>
- status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
- status |= IRQ_PENDING; ――<7>
- action = NULL;
- if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
- action = desc->action;
- status &= ~IRQ_PENDING;
- status |= IRQ_INPROGRESS; ――<8>
- }
- desc->status = status; ――<9>
- if (unlikely(!action)) ――<10>
- goto out;
- for (;;) {
- irqreturn_t action_ret;
- spin_unlock(&desc->lock);
- action_ret = handle_IRQ_event(irq, regs, action); ――<11>
- spin_lock(&desc->lock);
- if (!noirqdebug)
- note_interrupt(irq, desc, action_ret, regs);
- if (likely(!(desc->status & IRQ_PENDING))) ――<12>
- break;
- desc->status &= ~IRQ_PENDING;
- }
- desc->status &= ~IRQ_INPROGRESS; ――<13>
- out:
- desc->handler->end(irq); ――<14>
- spin_unlock(&desc->lock);
- return 1;
- }
- 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(); ――<20>
- do {
- ret = action->handler(irq, action->dev_id, regs);――<21>
- if (ret == IRQ_HANDLED)
- status |= action->flags;
- retval |= ret;
- action = action->next;
- } while (action);
- if (status & SA_SAMPLE_RANDOM)
- add_interrupt_randomness(irq);――<22>
- local_irq_disable();――<23>
- return retval;
- }
do_IRQ関数は、IRQ番号(<1>)から、そのIRQに対応したirq_descテーブルのエントリを求めます(<4>)。
このエントリに登録されている割り込みハンドラを実行するhandle_IRQ_event関数(<11>)は、登録されている割り込みハンドラを呼び出します(<21>)。複数の割り込みハンドラが登録されていた場合、そのすべてのハンドラを呼び出します。
割り込み処理中は、プリエンプション、ソフト割り込みの実行を抑制する必要があります。do_IRQ関数は実行開始時にそれらを禁止し(<2>)、処理が終了するとそれらを許可します(<3>)。許可するときにソフト割り込み要求が保留されていないか確認し、もし保留されているときは、ソフト割り込みハンドラの起動を行います。
割り込み処理はネスト可能です。割り込みハンドラ実行中に、別の割り込みを受け付け可能にします。そのためには、CPUがIRQの割り込みを受けたとき、まず割り込みコントローラに、IRQの割り込みをCPUが受け付けたことを知らせます(<6>)。この処理によって、割り込みコントローラはまた別の割り込みを発生させることができるようになります。ただし、同じ種類のIRQの発生は抑制されます。
割り込みコントローラが割り込みを発生させたとしても、CPUが割り込みをマスクしているとCPUは割り込みを受け付けません。割り込みハンドラ実行中に、別の割り込みを受け付けるためには、CPUレベルでも割り込みを許可する必要があります(<20>)。ただし、割り込みハンドラの属性として割り込みのネストを許さない指定のときは許可しません。
目的のIRQに対する割り込み処理が完了すると、割り込みコントローラに、IRQに対応する処理が完了したことを通知します(<14>)。これによって、割り込みコントローラは、同じIRQの割り込みを再度発生させることができるようになります。
マルチプロセッサ環境における対応もなされています。別のCPUに対し同じIRQの割り込みが発生することがあります。このとき同じ割り込みハンドラが、同時に複数のCPU上で動作すると不都合が生じます。Linuxカーネルの割り込み処理は、この状態を検出し、割り込みハンドラが同時に1つだけ動作するように制御します。
割り込みハンドラ実行中は、そのIRQのハンドラが実行中状態であることを示すフラグ(IRQ_INPROGRESS)を立てておきます(<8>、<9>、<13>)。もしあるCPUが割り込みを受け付けたとき、ほかのCPUがそのIRQに対応する割り込みハンドラを実行中である場合、割り込みを保留したことを示すフラグ(IRQ_PENDING)を立て(<7>、<9>)、何も実行せずに終了します(<10>、図2-5)。
一方、ほかのCPUが割り込みハンドラを終了するとき、割り込みを保留したことを示すフラグ(IRQ_PENDING)が立っていたら、再度割り込みハンドラを実行します(<12>)。
また、各CPUローカルな割り込みに対する処理を高速化する仕組みも用意されています(<5>)。ほかのCPUとの排他を考慮しなくて良いため、オーバーヘッドを減らすことができます。x86アーキテクチャでは利用していません。x86アーキテクチャは複数の割り込みエントリを持てるため、CPUローカルな割り込みにはその仕組みを利用しています。
ここまで発生した割り込みに対する処理手順を見てきましたが、実際はどれくらいの割り込みハンドラが動作しているのでしょうか? ハードウェア割り込みの発生の統計情報は/proc/interruptsを参照することによって見ることができます。
実行例2-1では、システムの起動後に、IRQ 0のタイマー割り込みがCPU0上で468975451回、イーサネットの割り込み(eth0インターフェイス)がCPU0に対して1363610回、SCSIの割り込み(SCSIホストバスアダプタsym53c8xxからの割り込み)がCPU0に対して61574回発生していることが分かります。
# cat /proc/interrupts CPU0 CPU1 0: 468975451 468975450 IO-APIC-edge timer 1: 8 1 IO-APIC-edge i8042 2: 0 0 XT-PIC cascade 8: 1 0 IO-APIC-edge rtc 14: 2 0 IO-APIC-edge ide0 20: 1363610 1363732 IO-APIC-level eth0 24: 61574 61567 IO-APIC-level sym53c8xx NMI: 0 0 LOC: 937990951 937990962 ERR: 0 MIS: 0
また、2つのCPUに対して均等に割り込みが発生しています。CPU1に対しても、CPU0とほぼ同じ回数の割り込みが発生していることが分かります。
割り込みの発生を特定のCPUに割り付けることも可能です。/proc/irq/<IRQ番号>/smp_affinityに、割り込みを発生させるCPUを指定するビットマスクを与えることによって、割り込みハンドラを特定のCPUで処理させることが可能となります。<IRQ番号>には、割り込み要因(IRQ番号)を指定します。
実行例2-1のシステムで、SCSI割り込み(IRQは24)をCPU0に割り付け、イーサネット割り込み(IRQは20)をCPU1に割り付けたいときには、実行例2-2のように指定します。しばらく動作させた後に、/proc/interruptsを参照すると、たしかに指定したCPUが割り込みを受け付けていることが分かります。
# echo 00000002 > /proc/irq/20/smp_affinity # echo 00000001 > /proc/irq/24/smp_affinity