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に近い値を返すことになる。かくして、カーネル内の様々な処理のスケジュールが狂い、カーネルパニックが発生してリブートしてしまったのである。
再検索