"okkyの銀河制圧奇譚"で、Linux Kernelのスケジューラで使用するTSCをns単位に換算するルーチンでの掛け算オーバーフローが起因して、リブートする等の問題があこることの議論がされている。
しかし、問題が発現していない環境もあって、現象の理解が必要だとおもわれた。ブログ上でのコメントでのレポートでは、656日稼動しているという。
こういうときには、実験的に確認するのがよい。
#include<stdio.h>
static __inline__ unsigned long long rdtsc(void)
{
unsigned hi, lo;
__asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}
int main(){
unsigned long long tick, result;
unsigned long cyc2ns;
unsigned long cpu_khz = 1770000;
tick = rdtsc();
printf("TSC= %llu\n", tick);
cyc2ns = 1000000 * (1 << 10) / cpu_khz;
printf("SC= %lu\n", cyc2ns);
result = tick * cyc2ns;
printf("MULT= %llu\n", result);
printf("OVER> %llu\n", ((unsigned long long) 1 << 63));
}
問題のルーティンに似せた、簡単なこのようなコードを書き、現象の理解をおこなう。 とりあえず、実行したカーネルは、Ubnutu 11.10上の3.0.0である。
miurahr@miurahr-note:~$ ./a.out
TSC= 18906864235232
SC= 578
MULT= 10928167527964096
OVER> 9223372036854775808
miurahr@miurahr-note:~$ ./a.out
TSC= 27953476532
SC= 578
MULT= 16157109435496
OVER> 9223372036854775808
この2つの実行の間に、実行されたPCはスリープされている。このスリープのresume時にcyc2ns_offsetを再計算するようにされている。
上記の結果からわかるように、64bitのTSCが折り返されて、小さくなっている。もちろん、複数CPUが搭載されているので、かならずしも同じCPUの値を取得しているとは言えないのである。とはいえ、得られたヒントは大きい。
static inline unsigned long long __cycles_2_ns(unsigned long long cyc)
{
int cpu = smp_processor_id();
unsigned long long ns = per_cpu(cyc2ns_offset, cpu);
ns += cyc * per_cpu(cyc2ns, cpu) >> CYC2NS_SCALE_FACTOR;
return ns;
}
当該コード部分に注目すると、cyc2ns_offsetに、指摘されている演算結果を加えるコードになっている。どういうことか。ノートPCはとくに、使用時間をのばすために、CPUの速度を動的に変更して、負荷の低い時のCPUの消費電力を削減する。こうすると、当然クロックあたりの時間幅、上記のcyc2nsの値が変化する。したがって、単純にTSCの累計にそのときのCPU速度で計算してしまっては、正しい値になりえない。
で、どうするかというと、CPU速度の変化をさせる時に、そのときの経過時刻をns単位でもとめ、変更後の速度で計算した場合の累計時刻との差分をcyc2ns_offsetに格納する。