基礎●●●○○

割り込み

Interrupt ・ わりこみ

デバイスやタイマがCPUの実行を中断させて出来事を知らせる仕組み。待たずに反応できる計算機の反射神経。

✦ 30秒で試す — 航海CPUは、1秒に何十万回も「ちょっと待って」と言われているCPUの本来の仕事がより多く残るのは、どちら?予想してみる

概要

割り込み(interrupt)は、キーボードやディスク、ネットワークカードといったデバイス、あるいはタイマが、CPUがいま実行している仕事を一時中断させて「こちらで出来事が起きた」と知らせる仕組みです。CPUは相手の様子を自分から見に行くのではなく、知らせが来たときにだけ手を止めて反応します。これがあるおかげで、CPUは遅いデバイスを待つ間も別の仕事を進められます。計算機の反射神経にあたる仕組みです。

たとえばキーを1つ押すと、キーボードのコントローラが割り込みを上げ、CPUはいまの作業をきりのよいところで止めて「どのキーが押されたか」を受け取り、処理を終えたら元の作業に戻ります。一瞬で終わるので普段は意識しませんが、割り込みを受け取って捌くのはOSのカーネルの仕事です。複数のプロセスを交代で動かすマルチタスクも、ディスクやネットワークを待たずに進む非同期処理も、この土台の上に築かれています。

なぜ生まれたか

割り込みが無い世界では、CPUがディスクにデータを要求したあと、読み出しが終わったかどうかを知るには、自分で「もう終わった?」「まだ?」と状態を確かめ続けるしかありません。これを「ポーリング(polling、状態を繰り返し確認すること)」と呼びます。ディスクの読み出しはCPUの命令実行に比べて何万倍も遅いため、その間ずっと確認を回すのは時間の浪費です。確認の間隔を空けて別の仕事を挟めば浪費は減りますが、今度は完了に気づくのが遅れます。「浪費」と「反応の鈍さ」のどちらかを必ず払わされるのが、ポーリングの限界でした。

そこで発想を逆転させたのが割り込みです。CPUは「終わったら知らせてくれ」とだけ頼んで別の仕事に取りかかり、デバイスは完了した瞬間にCPUへ信号を送ります。CPUはそこで初めて手を止めます。待つ間もCPUは遊ばず、しかも完了に即座に反応できる。1950年代の計算機に入出力の完了を知らせる仕組みとして現れたこの考え方は、やがて「実行中のプログラムから強制的に制御を取り上げる」手段にもなり、複数のプログラムを安全に交代させるOSの誕生を支えました。

詳細

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

両者の差は、CPUの時間の使われ方と、矢印の向きに表れます。ポーリングではCPUがデバイスへ何度も問い合わせに行き、割り込みではデバイスのほうからCPUへ知らせが来ます。

ポーリング — CPU が見に行くデバイス読み出し中完了CPU確認確認確認確認やっと気づく「終わった?」を繰り返す間、本来の仕事は進まない。間隔を空ければ気づくのが遅れる割り込み — デバイスが知らせるデバイス読み出し中完了CPU別の仕事を進める処理続きの仕事割り込み信号
ポーリング対割り込み。ポーリングは CPU が見に行き(上向きの矢印)、割り込みはデバイスが知らせに来る(下向きの矢印)

知らせが届くまで — コントローラとベクタ

デバイスがCPUに直接つながっているわけではありません。間に割り込みコントローラ(x86 の APIC、ARM の GIC など)が入り、たくさんのデバイスから来る要求を受け付けて、優先度を判定し、どのCPUコアに届けるかを決めます。現代の PCIe デバイスは専用の信号線を使わず、決められた番地へのメモリ書き込みで割り込みを伝える MSI/MSI-X という方式が主流で、ネットワークカードなら受信キューごとに別々の割り込みを別々のコアへ振り分けられます。

知らせを受けたCPUは、実行中の命令をきりのよいところまで終え、いまの状態(プログラムカウンタやフラグなどのレジスタ)を退避し、特権モードに切り替わります。そして割り込みの番号を手がかりに割り込みベクタ表(x86 では IDT)を引き、その番号に登録されたハンドラへ飛びます。ベクタ表を用意して各番号にハンドラを登録しておくのは、起動時のカーネルの仕事です。

ハンドラCPU割り込みコントローラデバイスハンドラCPU割り込みコントローラデバイス別のプロセスを実行中命令の区切りでレジスタを退避し特権モードへ状態を復元し 中断した命令列を再開読み出しを依頼し DMA で転送させる転送完了 IRQ を上げる優先度を判定して割り込み番号を届けるベクタ表で番号に対応するハンドラへ飛ぶ状態を読み 受け取ったことを知らせる重い後処理はボトムハーフへ回して戻る

図の最初の行に出てくる DMA(Direct Memory Access)は、割り込みと対になる仕組みです。ディスクやネットワークカードは、CPUに1バイトずつ運ばせるのではなく、自分でメモリへデータを書き込み、転送がすべて終わったときに一度だけ割り込みを上げます。CPUは「どこへ何バイト読むか」を指示するだけで、転送の間は別の仕事に回れます。割り込みが「待たなくてよい」を実現し、DMA が「運ばなくてよい」を実現している、という分担です。

退避・処理・復帰と、トップハーフ/ボトムハーフ

ハンドラは必要な処理を済ませたら、退避しておいた状態を復元して、中断していた仕事を何事もなかったかのように再開させます。割り込まれた側から見れば、一瞬立ち止まっただけです。ただし、ハンドラが動いている間は同じ種類の割り込みが止められていることが多く、ハンドラが長引くとほかの知らせを取りこぼしたり、応答が遅れたりします。

そこで Linux などのカーネルは、処理を2つに割ります。割り込みを受けた直後に最小限のこと(デバイスへの応答、データの位置の記録)だけを済ませるトップハーフと、パケットのプロトコル処理のような重い後始末を、割り込みを許した状態で後から実行するボトムハーフ(Linux では softirq・tasklet・workqueue、あるいは専用のカーネルスレッドで動く threaded IRQ)です。「知らせを受け取る」と「知らせの中身を処理する」を分けるのは、割り込みを扱う設計の基本形です。

マスクと優先度

割り込みはいつでも受けられるわけではありません。カーネルは、データ構造を書き換えている最中など中断されると困る短い区間では、割り込みを一時的に禁止(マスク)します。コントローラの側でも、特定の番号だけを止めたり、優先度の低い割り込みを高い割り込みの処理中は待たせたりできます。ただし、ハードウェアの致命的な異常を知らせる NMI(Non-Maskable Interrupt)のように、止めることのできない割り込みもあります。マスクしている時間が長いほど、ほかの出来事への反応が遅れるので、カーネルの開発ではこの区間をできるだけ短く保つことが重視されます。

割り込みの仲間 — 例外とシステムコール

CPUの流れを止めて特権モードのコードに制御を移す出来事は、割り込みだけではありません。発生源で分けると次のように整理できます。

ハードウェア割り込みは、デバイスがCPUの実行とは無関係なタイミングで上げるものです(非同期)。マルチタスクの要になるタイマ割り込みも、実体はCPUに付いたタイマ装置が上げるハードウェア割り込みの一種で、役割が特別なので別枠で語られることが多いだけです。例外(トラップ)は、ゼロ除算や不正なメモリアクセスのように、実行中の命令そのものが原因でCPU内部から起きるもので、同じ命令を実行すれば同じ場所で起きます(同期)。仮想メモリで、まだメモリに載っていないページに触れたときに起きる「ページフォールト」は、例外を使って不足分を読み込んでから命令をやり直す、日常的に使われる仕組みです。

システムコールは、アプリケーションが自分から特権モードに入る正式な入口で、割り込みと親戚関係にあります。古い 32 ビット版 Linux では int 0x80 というソフトウェア割り込み命令でカーネルに入っていましたが、現代の x86-64 では専用の syscall 命令が主流で、ベクタ表を引く割り込みの手順を通らずにより速く入れるようになっています。ARM の svc 命令は、同期的な例外の一種としてカーネルに入ります。「カーネルに制御を移す仕組み」という根は共通でも、いまのシステムコールを「ソフトウェア割り込み」と呼ぶのは正確ではありません。

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

割り込みの最も重要な応用が、タイマ割り込みによるマルチタスクです。CPUを握ったプログラムが自分から手放さなければ、ほかのプログラムは永遠に動けません。そこでOSは一定間隔でタイマ割り込みを受け取るようにしておき、割り込みが来るたびに制御をスケジューラに渡します。スケジューラは「そろそろ交代させよう」と判断し、CPUを別のプロセスやスレッドに切り替えます。実行中のプログラムの意思とは無関係に交代させるこの方式を「プリエンプティブ(横取り式)なマルチタスク」と呼びます。現代の Linux は、することのないコアでは定期的なタイマ割り込みを止め(tickless)、次に用事がある時刻にだけ割り込みを予約して電力を節約しています。

割り込みが多すぎる — 割り込みの嵐と NAPI

割り込みは、出来事がまばらなときには理想的ですが、出来事が密になると逆に足かせになります。高速なネットワークカードが1パケットごとに割り込みを上げると、CPUは退避とハンドラと復帰を延々と繰り返すだけで、本来の仕事がまったく進まなくなります。これを割り込みの嵐(interrupt storm)や、受信ライブロック(受信処理に追われて何も進まない状態)と呼びます。

対策の1つ目は、デバイス側で複数の出来事をまとめて1回の割り込みにする割り込みの間引き(interrupt coalescing)です。2つ目が、Linux のネットワーク処理で使われる NAPI です。最初のパケットでは割り込みで気づき、そのままそのデバイスの割り込みを止めて、受信キューに溜まったパケットをポーリングでまとめて処理します。キューが空になったら割り込みを再び許可して、次の知らせを待ちます。まばらなときは割り込み、密なときはポーリング。「知らせてもらう」と「見に行く」のどちらが得かは負荷で入れ替わる、という現場の知恵です。

下の部品で確かめてください。デバイスのボタンを押すたびに出来事が1つ起き、CPUの時間帯にその処理が刻まれます。3つの方式で、ゆっくり押したときと、押しっぱなしで嵐を起こしたときを比べてみてください。

⚡ 体験: 出来事を起こして CPU の時間帯を見る
方式
IRQデバイスネットワークカード割り込みコントローラAPIC・GICCPU仕事中▶ 届いた押しっぱなしで大量に受信バッファ(DMA で置かれる)CPUコア0いま−50−100−150−200−250−300 µs
本来の仕事退避・復帰ハンドラ確認(ポーリング)到着
本来の仕事100%気づくまで(平均)—割り込み0 回確認0 回取りこぼし0

CPU はいま仕事(金)を続けています。左の「届いた」を押すと、デバイスにパケットが1つ届きます。CPU の時間帯に何が刻まれるかを見てください。

非同期 I/O の土台として

視点を上のレイヤに移すと、割り込みは非同期処理の最下層にあることが見えてきます。JavaScript のイベントループのような「I/O の完了を待たずに次へ進み、終わったら通知を受けて続きを処理する」モデルは、突き詰めればカーネルがデバイスからの割り込みで完了を受け取り、待っていたプロセスを起こしているからこそ成り立ちます。アプリケーションの await の裏側には、DMA で運ばれたデータと、完了を告げる割り込みが横たわっています。

試問

ネットワークカード(デバイス)にパケットが届くと、CPU の時間帯(右端がいま)にその処理が刻まれます。「届いた」ボタンを押しっぱなしにするか「嵐を流し続ける」を押すと、パケットが絶え間なく押し寄せます。方式は、届くたびに CPU を中断させる「割り込み」、CPU が一定間隔で見に行く「ポーリング」、両者を混ぜた「NAPI」から選べます。

✦ 命題挑戦中

パケットの嵐を流し続けながら、CPU の本来の仕事を35%以上残し、1つも取りこぼさずに受け続けよ

  • 嵐(押しっぱなし/流し続ける)を 300 µs 以上続ける(いま 0 µs)
  • その間、本来の仕事が 35% 以上残っている
  • 嵐が始まってから、取りこぼしが 0 個
⚡ 体験: 割り込みの嵐の中で、CPU の仕事を守る
方式
IRQデバイスネットワークカード割り込みコントローラAPIC・GICCPU仕事中▶ 届いた押しっぱなしで大量に受信バッファ(DMA で置かれる)CPUコア0いま−50−100−150−200−250−300 µs
本来の仕事退避・復帰ハンドラ確認(ポーリング)到着
本来の仕事100%気づくまで(平均)—割り込み0 回確認0 回取りこぼし0

CPU はいま仕事(金)を続けています。左の「届いた」を押すと、デバイスにパケットが1つ届きます。CPU の時間帯に何が刻まれるかを見てください。