208.5日問題

読み:にひゃくはってんごにちもんだい
品詞:固有名詞

Linuxカーネルにあった不具合の一つ。起動からの経過時間(uptime)が208日を過ぎると、突然再起動する可能性がある。

目次

Pentium 4以降の全てのx86プロセッサー(互換プロセッサー含む)において、約208.5日間連続運転すると、突然再起動する可能性がある。

  • 影響: Linux Kernel 2.6.28 ‐ 2.6.32.49/3.0.12/3.1.4

問題

  • 突然の再起動によるデータの喪失

原因

Linux Kernel 2.6.28で導入された __cycles_2_ns() パッチに不具合があった。

Pentium 4以降には、Time Slice Stamp Counterと呼ばれる64ビットのカウンターが搭載されている。このカウンターはクロック単位でカウントアップされる。1GHzであれば、秒間1G増えることになる。つまり、概ねナノ秒単位のカウンターとして使用できる。

ただし省電力制御のためCPUクロックは随時変化するため、Linuxカーネルはcycles_2_ns()という関数を用意し、TSCの値をナノ秒単位に換算する機構を用意した。

しかしながら、そのお粗末な計算方法に問題があり、約208.5日経過していると計算中に数値がオーバーフローしてしまい、変換関数は異常な値を返すため、結果システムがクラッシュする。

修正

__cycles_2_ns()関数を修正した。修正は「sched, x86: Avoid unnecessary overflow in sched_clock」と題されている。

 static inline unsigned long long __cycles_2_ns(unsigned long long cyc)
 {
+    unsigned long long quot;
+    unsigned long long rem;
     int cpu = smp_processor_id();
     unsigned long long ns = per_cpu(cyc2ns_offset, cpu);
-    ns += cyc * per_cpu(cyc2ns, cpu) >> CYC2NS_SCALE_FACTOR;
+    quot = (cyc >> CYC2NS_SCALE_FACTOR);
+    rem = cyc & ((1ULL << CYC2NS_SCALE_FACTOR) - 1);
+    ns += quot * per_cpu(cyc2ns, cpu) +
+        ((rem * per_cpu(cyc2ns, cpu)) >> CYC2NS_SCALE_FACTOR);
     return ns;
 }

検証

TSCは概ねナノ秒単位のカウンターである。1GHzちょうどで動作するならTSCは1ナノ秒単位でカウントアップしていることになる。更に言えば、2GHzちょうどなら0.5ナノ秒単位となる。

得られた値をscale factor倍すればナノ秒単位であり、クロックに合わせてscale factorを定義すればよいのだが、TSCは元々ほぼナノ秒単位なので、これをナノ秒単位にするとなると逆に難しい。何しろほぼナノ秒なのであるから、普通に考えればscale factorは1前後の値の小数となり、この1前後の値での浮動小数点演算が必要となってしまうからである。

そこで誰かが考えた方法は、1000倍にして計算したあと、10ビットの右シフトで1/1024するという手法であった。これなら、若干の誤差は生じるものの、浮動小数点演算も、大きな数の除算も不要で、全てが簡単な演算で済む。(この時点で嫌な予感がしたなら、エンジニアとしての素質あり)

具体的には、64ビットのTSC値に、32ビットのcyc2ns_scale(これはper_cpu()関数の返却値)を掛け、これを10ビット右シフトして64ビット変数に求めるという方法である。

さて、1/1024すればナノ秒が求まるということは、その直前はピコ秒オーダーの値となっていることになる。ピコ秒のオーダーで時間を扱うとなると、1秒を扱うのに40ビットが必要となる。となると、秒以上の値を扱う余裕は24ビットしかないことになる。

有効ビット長24ビットを秒単位で表現したときに表現できる時間は次のとおり。

224 / ( 24 × 60 × 60 ) = 194.18074日

実際には、10ビットシフトして消すため、有効ビット長54ビットのナノ秒単位であるので、表現可能な時間は次のようになる。

254 / ( 24 × 60 × 60 × 1000 × 1000 × 1000 ) = 208.499983日

つまりこの方法では、約208.5日経つと演算が途中でオーバーフローしてしまい、__cycles_2_ns()はいきなり0に近い値を返すことになる。かくして、カーネル内の様々な処理のスケジュールが狂い、カーネルパニックが発生してリブートしてしまったのである。

用語の所属
Linuxカーネル

コメントなどを投稿するフォームは、日本語対応時のみ表示されます


KisoDic通信用語の基礎知識検索システム WDIC Explorer Version 7.04a (27-May-2022)
Search System : Copyright © Mirai corporation
Dictionary : Copyright © WDIC Creators club