タイマ割り込みとタイマホイールの構造
何万個のタイマを抱えても挿入・満了が定数時間で済む理由が原理から分かります。階層型タイマホイールのバケット構造とカスケード、hrtimerの赤黒木との使い分けまで押さえられます。
- 1.タイマホイールは満了時刻をバケットに分配する配列で、挿入と削除を平均O(1)にする。粗い精度でよい大量のタイムアウト向け。
- 2.階層を重ねて遠い未来のタイマを上位ホイールに置き、tickごとに最下位だけ調べる。上位から下位へ移すカスケードで全体が償却O(1)になる。
- 3.正確なナノ秒期限が要る用途はhrtimerが赤黒木で時刻順に管理し挿入O(log n)。タイムアウトはホイール、締め切りはhrtimerと役割が分かれる。
なぜ「タイマ専用のデータ構造」が要るのか
カーネルは膨大な数のタイマを抱えます。TCP の再送タイムアウト、接続のアイドル切断、I/O の打ち切り、schedule_timeout による短い眠り——サーバー1台でも同時に何万個ものタイマが生きていることは珍しくありません。これらは大半が「期限まで待つが、ほとんどの場合は期限前にキャンセルされる」という性質を持ちます。つまり挿入とキャンセルが圧倒的に多く、実際に満了するものは少数です。
この性質に対し、素朴に「満了時刻でソートした連結リスト」や「優先度付きキュー」を使うと挿入が O(n) や O(log n) になり、毎秒大量に発生する登録・解除がボトルネックになります。そこで Linux は、精度を犠牲にする代わりに挿入・削除を平均 O(1) にする専用構造として タイマホイール(timer wheel) を採用しています。tick で数える粗い時間軸の上で動く timer_list がこれです。時間軸そのものの話は /os/kernel-timekeeping-timers/ を前提とします。
扱うのは jiffies 基準の timer_list(タイマホイール)の挿入・満了アルゴリズムと、そのO(1)償却がなぜ成り立つか、そして精密な期限を扱う hrtimer(赤黒木)との使い分けです。両者は同じ「タイマ」でも狙う精度と用途が違います。
タイマホイールの基本 ── ハッシュではなくバケット配列
タイマホイールは、満了時刻(jiffies 値)を添字にしたバケットの配列です。各バケットは、その時刻に満了するタイマの連結リストを持ちます。現在時刻を指す「指針(カーソル)」があり、tick のたびに 1 つ進みます。指針が指したバケットのリストを取り出し、中のタイマを満了させる——これがホイールという名の由来です。
バケット配列(例: 256スロット)
index = expires & (SLOTS - 1) ← 満了時刻の下位ビットでバケットを選ぶ
[0][1][2] ... [k] ... [255]
↑
現在の指針(jiffiesの下位ビット)
tickごとに指針を1つ進め、そのバケットのリストだけ満了処理する
ここが核心です。挿入は満了時刻からバケット添字をビット演算で即決できるので O(1)、削除は連結リストからの取り外しなので O(1)。満了処理も「指針が指す1バケットだけ」を見るので、その瞬間に満了するタイマ数に比例した手間で済みます。ソートされた構造のように全体を整列させる必要がありません。
代償は精度です。同じバケットに入ったタイマは同時刻として扱われ、tick 粒度(HZ=250 なら 4 ミリ秒)より細かい区別はできません。さらに後述の階層では、遠い未来のタイマほど粒度が荒くなります。だからこそこの構造は「タイムアウト」のように多少早い/遅いが許される用途に向くのです。
階層化 ── 1段では配列が足りない
問題は配列の長さです。1 tick=1 スロットの単一ホイールで「数十秒先」まで表すには、HZ=250 で数千〜数万スロットが要り、tick ごとに大量の空バケットを舐めることになりかねません。これを解くのが**階層型タイマホイール(hierarchical timing wheel)**です。
時計の文字盤が秒・分・時の輪を重ねるように、ホイールを複数段重ねます。下位の段ほど粒度が細かく射程が短く、上位の段ほど粒度が荒く射程が長い。Linux の現行実装は概念的にこう構成されます。
| 段(レベル) | 1スロットが表す幅 | 段全体の射程 | 役割 |
|---|---|---|---|
| 最下位 | 1 tick(最も細かい) | 近い未来(数百tick) | もうすぐ満了するタイマ |
| 中位 | 数十〜数百tick | 数秒〜十数秒先 | 中距離のタイマ |
| 上位 | さらに粗い粒度 | 遠い未来 | 長いタイムアウト |
挿入時は、満了までの残り時間(expires - now)の大きさを見てどの段に置くかを選び、その段の中ではビット演算でスロットを決めます。残りが大きいほど上位の粗い段に入るため、遠い未来のタイマほど粒度が荒く配置されるのがこの設計の肝です。近いものだけ細かく、遠いものはざっくり——これにより、どの段も現実的なスロット数(各段 64 程度の小さな配列)で全時間域をカバーできます。
階層化の帰結として、満了が遠いタイマほど誤差(早まり/遅れの幅)が大きくなります。これは欠陥ではなく設計上の割り切りです。30秒後のタイムアウトが数百ミリ秒ずれても実害はほぼ無い一方、その精度を捨てることで挿入を定数時間に保てます。精密さが要るなら最初からホイールではなく hrtimer を選ぶべき、という分担になります。
カスケードとO(1)償却 ── 上位から下位へ「降ろす」
階層型では、tick で進めるのは最下位の段だけです。では上位の段のタイマはいつ処理されるのか。ここで カスケード(cascade) が登場します。最下位の指針が一周して 0 に戻る瞬間、1 つ上の段の指針を 1 つ進め、そのスロットに溜まっていたタイマたちを取り出して下位の段へ入れ直すのです。
最下位ホイールが一周(指針が0に戻る)
→ 中位ホイールの指針を+1
→ そのスロットのタイマ群を取り出し
残り時間に応じて下位ホイールへ再配置(cascade)
中位が一周すれば、同様に上位 → 中位へ降ろす
降りてきたタイマは、より近づいた満了時刻に応じて細かい段へ再配置されます。こうして「遠いうちは粗い段で安く保持し、近づいたら細かい段へ移す」段階的な絞り込みが起きます。
償却計算量が O(1) になる理由はここにあります。1 つのタイマが一生のうちにカスケードで段を降りる回数は、段の数(時間域に対し対数的で、定数とみなせる小さな値)に限られます。各カスケードは連結リストの付け替えだけ。N 個のタイマを処理する総コストを N で割れば、1 タイマあたり定数回の再配置に収まります。tick ごとに見るのは原則「最下位の1スロット」だけで、空でも極小コストです。これが「ならし O(1)」の正体です。
償却 O(1) は平均の話で、特定の tick の処理時間が一定という意味ではありません。下位が一周する tick ではカスケードが走り、その回のコストはそのとき降ろすタイマ数に比例して跳ねます。多数のタイマが同時に降りる瞬間は処理が重くなり、割り込み文脈(あるいは softirq)でこれが起きると遅延要因になりえます。だから満了処理(TIMER_SOFTIRQ)は割り込みの下半分へ逃がされます。この二段構えは /os/interrupt-top-bottom-half/ の設計そのものです。
なお、近年の Linux はこの素朴なカスケードを見直し、満了時刻ではなくバケット単位でまとめて遅延を許す方式に変えています。キャンセルが多い性質を踏まえ「満了するまで動かさない(再配置を減らす)」ことで、登録・解除のさらなる軽量化を図っています。設計思想——粒度を距離で変え、頻繁な挿入・解除を最優先で安くする——は一貫しています。
hrtimer との使い分け ── ホイールか赤黒木か
精度が要る用途には、ホイールではなく hrtimer を使います。hrtimer は満了時刻をナノ秒の絶対時刻で持ち、tick の粒度に縛られません。データ構造もホイールではなく**赤黒木(red-black tree)**で、時刻順に並べます。
なぜ木なのか。hrtimer が必要とするのは「次に満了するのはどれか(=最も早い期限)」の即時参照です。赤黒木なら挿入・削除が O(log n)、最左端(最早期限)はキャッシュしたポインタで O(1) に取れます。カーネルはこの最早期限に合わせてハードウェアタイマへワンショットの割り込みを一発予約し、満了したらコールバックを呼び、次の最早期限へ予約し直します。tick に相乗りしないので、tick 粒度を超える精度が出せます。
| 観点 | タイマホイール(timer_list) | hrtimer(高分解能) |
|---|---|---|
| データ構造 | 階層型バケット配列+連結リスト | 赤黒木(最左端をキャッシュ) |
| 挿入・削除 | 平均 O(1) | O(log n) |
| 精度 | tick粒度・遠いほど粗い | クロックソース依存のns級 |
| 割り込み形態 | 周期tickに相乗り+カスケード | 期限ごとにワンショット予約 |
| 向く用途 | タイムアウト(多くは満了前に解除) | 周期再生・nanosleep・締め切り管理 |
判断は用途で割り切れます。「期限前にキャンセルされる前提で、多少ずれても困らない大量のタイムアウト」はホイール。挿入・解除が圧倒的多数を占めるこの種の負荷で、O(1) の安さが効きます。一方、「必ず満了させ、ずれが許されない精密な期限」は hrtimer。nanosleep、オーディオの周期、SCHED_DEADLINE の予算管理などです。後者の精度がプリエンプション設計に左右される話は /os/realtime-scheduling/ や /os/kernel-preemption/ と地続きで、hrtimer がナノ秒を「表現できる」ことと精度が「保証される」ことは別物です。
「タイマホイールの計算量は?」には挿入・削除が償却 O(1)、その代償が精度(特に遠い未来)と答えます。「なぜ赤黒木でなくホイールを使う場面があるのか」にはキャンセル主体で精度不問の大量タイムアウトでは、O(log n) より O(1) の挿入・解除が支配的に効くから。逆に hrtimer が赤黒木なのは最早期限を安く取り、ワンショット割り込みで tick 粒度を超える精度を出すため、と整理できれば対比は完成です。
まとめ
- タイマホイールは満了時刻をバケット添字に変換する配列で、挿入・削除を平均
O(1)にする。代償は精度で、tick 粒度より細かくは扱えない。キャンセル主体・精度不問の大量タイムアウトに最適。 - 階層化で遠い未来のタイマを粗い上位段に置き、tick では最下位だけを見る。カスケードで上位から下位へ段階的に降ろし、1 タイマの段降り回数が定数に収まることから償却
O(1)が成り立つ。 - 償却
O(1)は平均であり、カスケードが走る tick は処理が跳ねる。だから満了は softirq(割り込みの下半分)へ逃がす。 - 精密な期限は hrtimer が赤黒木で管理し、挿入
O(log n)・最早期限O(1)参照・ワンショット割り込みで ns 級精度を出す。タイムアウトはホイール、締め切りは hrtimer と役割が分かれる。
OS Article
タイマ割り込みとタイマホイールの構造を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
タイマ
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
階層を重ねて遠い未来のタイマを上位ホイールに置き、tickごとに最下位だけ調べる。上位から下位へ移すカスケードで全体が償却O(1)になる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「タイマ / タイマホイール」に近いか確認する。
- 強みである「タイマホイールは満了時刻をバケットに分配する配列で、挿入と削除を平均O(1)にする。粗い精度でよい大量のタイムアウト向け。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。