基礎●●●○○

割り込み

Interruptわりこみ

ハードウェアや事象がCPUの実行を中断して通知する仕組み。ポーリングせずに反応できる計算機の反射神経。

概要

割り込み(interrupt)は、キーボードやディスク、ネットワークカードといったデバイス、あるいはタイマや異常事態が、CPUがいま実行している仕事を一時中断させ、「こちらを先に処理してほしい」と割り込んでくる仕組みです。CPUは自分から相手の状況を見に行くのではなく、相手から知らせが来たときにだけ反応します。いわば計算機の反射神経にあたる仕組みで、これがあるおかげでCPUは無駄に待たされずに済みます。

たとえばキーを1つ押すと、キーボードのコントローラがCPUに割り込みをかけ、CPUはいまの作業を中断して「どのキーが押されたか」を受け取り、処理を終えたら元の作業に戻ります。この一連の動きは一瞬で終わるため普段は意識しませんが、OSカーネルの動作の根っこにあり、マルチタスクや非同期処理といった上位の仕組みはすべてこの土台の上に築かれています。

なぜ生まれたか

割り込みが無い世界を想像してみましょう。CPUがディスクにデータを要求したとして、その読み出しが完了したかどうかを知るには、CPUが自分で「もう終わった?」「まだ?」と何度も問い合わせ続けるしかありません。これを「ポーリング(polling、繰り返し状態を確認すること)」と呼びます。ディスクの読み出しはCPUの命令実行に比べれば途方もなく遅いため、その間ずっと問い合わせのループを回すのは、待ち時間をまるごと浪費することを意味します。しかも問い合わせに専念している間、CPUは他の有益な仕事を1つも進められません。

そこで発想を逆転させたのが割り込みです。CPUは「終わったら知らせてくれ」とだけ頼んで、別の仕事に取りかかります。デバイスの側は処理が完了した瞬間にCPUへ割り込み信号を送り、CPUはそこで初めて手を止めて結果を受け取ります。待っている間もCPUは遊ばず、しかもデバイスの完了に即座に反応できる — ポーリングが抱えていた「待ち時間の浪費」と「反応の鈍さ」を同時に解いたのが、この受動的な通知への転換でした。

詳細

ポーリングと割り込みの違い

両者の差は、CPUの時間の使われ方を並べると一目でわかります。ポーリングではCPUが確認ループに張り付き、割り込みではCPUが別の仕事に回れます。

ポーリングCPU が自分で何度も確認しに行く確認確認確認確認完了を検知← 待ち時間をすべて浪費割り込みCPU は別の仕事へ、完了時に通知が来る別の仕事を進める割り込み処理← デバイスが完了を通知
ポーリング対割り込み — CPU の時間の使われ方の違い

割り込みの種類

割り込みと一口に言っても、発生源によっていくつかの種類に分かれます。第一が「ハードウェア割り込み」で、ディスクやネットワークカード、キーボードなど外部デバイスが非同期に発生させるものです。第二が「タイマ割り込み」で、一定間隔でCPUに定期的に届く時報のような割り込みです(後述するようにマルチタスクの要になります)。第三が「例外(トラップ)」で、ゼロ除算や不正なメモリアクセスのように、実行中の命令そのものが原因でCPU内部から発生する同期的な割り込みです。

これらの親戚として押さえておきたいのがシステムコールです。アプリケーションがカーネルの機能を呼ぶとき、多くの実装ではソフトウェア割り込み(意図的に割り込みを起こす専用命令)を使ってユーザーモードからカーネルモードへ切り替えます。つまり「割り込みでカーネルに制御を移す」という仕組みは、外部デバイスからの通知だけでなく、プログラムが自らカーネルに用事を頼む経路にも使われているのです。

割り込みハンドラ — 退避・処理・復帰

割り込みが来ると、CPUはいま実行中の仕事の状態(レジスタの値など、どこまで進んでいたか)を退避してから、その割り込みに対応する「割り込みハンドラ」という処理ルーチンへジャンプします。ハンドラは必要な処理を手早く済ませ、終わったら退避しておいた状態を復元して、中断していた仕事を何事もなかったかのように再開します。この「退避 → 処理 → 復帰」の流れは、割り込まれた側から見ればまるで一瞬立ち止まっただけのように振る舞います。ハンドラは他の割り込みを妨げないよう、できるだけ短く済ませるのが鉄則で、重い後処理は後回しにする設計が一般的です。

タイマ割り込みがマルチタスクを可能にする

割り込みの最も重要な応用が、タイマ割り込みによるマルチタスクです。もしCPUを握ったプログラムが自分から手放さなければ、他のプログラムは永遠に動けません。そこでOSは、一定間隔でタイマ割り込みを受け取るようにしておき、割り込みが来るたびに制御をスケジューラに渡します。スケジューラは「そろそろ次のプログラムに交代させよう」と判断し、CPUを別のプロセスやスレッドに切り替えます。この、実行中のプログラムの意思とは無関係に強制的に交代させられる仕組みを「プリエンプティブ(横取り式)なマルチタスク」と呼びます。1つのCPUがあたかも複数のプログラムを同時に走らせているように見えるのは、この定期的な割り込みが土台にあるからです。

割り込みが多すぎる問題

割り込みは便利な反面、多すぎると逆に足かせになります。高速なネットワークカードが1パケットごとに割り込みをかけると、CPUは退避と復帰を延々と繰り返すだけで本来の処理が進まなくなります(割り込みの嵐、interrupt storm)。そこで実務では、割り込みとポーリングを状況で使い分けるハイブリッドな方式が使われます。たとえば Linux のネットワーク処理で使われる NAPI という仕組みは、通信が激しいときは最初の割り込みをきっかけに一時的にポーリングへ切り替えてまとめて処理し、割り込みの回数を抑えます。「知らせてもらう」と「見に行く」のどちらが得かは負荷次第で変わる、というのが現場の知恵です。

非同期IOの土台として

視点を上のレイヤに移すと、割り込みは非同期処理の最下層にあることが見えてきます。JavaScript のイベントループのような「IOの完了を待たずに次へ進み、終わったら通知を受けて続きを処理する」というモデルは、突き詰めればOSがデバイスからの割り込みで完了を受け取り、それを上位のプログラムへ伝えているからこそ成り立ちます。アプリケーション開発者が書く await の裏側には、OSレベルの割り込みによる完了通知が横たわっている — 割り込みは、現代の非同期プログラミング全体を静かに支える土台なのです。