まずはソースリストです。時間計測に
#include <stdio.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdlib.h>
#include <math.h>
#define TestCount 1000
#ifdef USE_RDTSC /* Pentium 以降のRDTSC命令を使用 */
/* 1秒あたりの 1 tick */
#define TickPerMSec 200e3 /* CPU の周波数に依存: だいたい公称値 */
/* 現在時間取得: 単位 tick */
unsigned long long GetTick(void)
{
unsigned int h,l;
/* read Pentium cycle counter */
__asm__(".byte 0x0f,0x31" : "=a" (l),"=d" (h));
return ((unsigned long long int)h<<32)|l;
}
#else
/* 1秒あたりの 1 tick */
#define TickPerMSec 100.0
/* 現在時間取得: 単位 tick */
unsigned long long GetTick(void)
{
struct timeval tv;
gettimeofday(&tv,NULL);
return (unsigned long long)tv.tv_sec*1000000+tv.tv_usec;
}
#endif
int main(int argc,char **argv)
{
int cycle=15;
int i,j;
double a=1;
FILE *fp;
unsigned long long ticks[TestCount];
if(argc>1) cycle=atoi(argv[1]);
for(i=0;i<TestCount+3;i++) /* 一定周期ループ */
{
/* 計測 */
if(i>2) /* 最初は絶対ずれるからパス */
ticks[i-3]=GetTick();
else
GetTick(); /* 空読み */
/* ダミー処理 */
for(j=0;j<100;j++) a=sin(cos(a));
/* 休止 */
usleep(cycle*1000); /* msec->usec */
}
fp=fopen("cycle.xy","w");
for(i=0;i<TestCount-1;i++)
{ /* number, interval, time */
fprintf(fp,"%d\t%lf\t%lf\n",i,
(ticks[i+1]-ticks[i])/TickPerMSec,
(ticks[i]-ticks[0])/TickPerMSec);
}
fclose(fp);
return 0;
}
このプログラムを
gcc -o cycle cycle.c -lmなどとコンパイルし、実行すると、しばらく(20-30秒)して cycle.xy というファイルができます。 その一例を示します。これはあくまで一例です。 環境に依存しますし、二度と同じ結果はでません。 また、Linux 2.0 では周期が30[ms]になるようです。
このプログラムは 引数を指定するとその時間([ms])だけ休むようになっています(
より間隔を短く&細かく設定したい場合には、Linuxのソースを数文字変更(0をひとつ足すなど)すれば希望通りになります。
その話のまえに、Linuxカーネルの動作の解析をしておきます。いわば、本手法の原理にあたる部分です。
なんでもいいから早く試してみたいという方は、カーネルソースを再構築用に展開し (各ディストリビューションのマニュアルをご確認下さい)、 /usr/src/linux/include/asm/param.h の HZ という数値の定義を 100 から1000 にして再構築し、そのカーネルで起動してみてください (モジュールの再構築、インストールも必要)。 2[ms]以上, 1[ms]単位で周期が決められるようになっているはずです。
Linux では 多くのUNIX系のOS同様、プログラムの実行をプロセスという単位で行います。このプロセスには幾つか状態があります。
Linux のスケジューラは、カーネル空間の
さて、その選定条件ですが、基本的には
この持ち時間は次に述べるタイマ割り込みのときにCPUを使用していたプロセスから減らしていきます。 つまりCPUを使い続けるようなプロセスはどんどん持ち時間が減り、結果的に時々実行可能状態になるようなプロセスより実質的な優先度が下になります。 それに対して、タイマ割り込みになる前に sleep などでCPUをOSに返してしまえば、CPUは使っていても持ち時間そのままなので、実行可能状態に戻れば、CPUをもらいやすいことになります。
ちなみに、実行可能状態にあるプロセスの持ち時間がすべて0になると持ち時間の再計算が行われます。それはすべてのプロセスに対して
これらのことから、
Linux ではタイマ割り込みをつかって、内部で幾つかの処理を定期的に行っています。
この周期が多くのLinuxでは
このタイマ割り込みで行う処理は
上の2つの組合わせともうひとつのルールによって一定周期実行の原理が説明できます。
Linux の場合、
少し処理をして、タイマ割り込みが来る前 sleep で休眠するようなプロセスは、スケジューリングのところで述べたように、基本的に持ち時間は高くなっています。 その結果、CPUを得やすいわけです。その結果が上のグラフになります。 多少乱れが生じているのは、カーネルそのものタイマ割り込み時の処理量に変動があったり、システムコール中だったりするためと考えられます。
さて、タイマ割り込みで同時に複数のプロセスが実行可能状態になったら、どうなるでしょうか。 そのときはもちろん、持ち時間の多い方が優先です。 UNIXにおいては、多くのプロセスが走っていますが、実はそのほとんどが休眠状態です(サーバプロセスで要求町待機)。 つまり、目的の制御用に周期的に実行しているプロセスの他のプロセスが起きてしまったらそっちにCPUをとられる可能性があります。 CPUを得たプロセスが単純な処理ならいいのですが、最悪の場合は次のタイマ割り込みまでCPUが得られないということがあり得ます(それどころか、そのさき2〜3回ということも)。 この状態を回避するには、最初から持ち時間を多くしておくに限ります。具体的には
nice --20 a.outとします。これは 優先度を20上げることを意味します(標準で0、-20が優先度最大、なので、これで最大になります)。
一連の流れを整理します。
このような仕掛けによって、一見いいかげんなプログラムで、タイマ割り込みで量子化された周期で、周期的に処理を実行することが可能なのです。
この方法が、いい加減なように見えて期待通り動くのは、このようにOSの動作が都合良く設計されていたためですが、逆にOSの動作が 分かってしまったので、いい加減な方法ではなく、理論に基づいた手法となったのです。