4. 高い精度のタイミング制御

4.1. ディレイ

まず最初に断っておきますが、ユーザプログラムにおいて正確なタイミング保証 をすることはできません。これは、Linux がマルチタスク・プリエンプティブ なシステムだからです。あなたのプロセスが、約 10 ミリ秒から数秒(非常に負荷の 高いシステムなどで)に渡ってなんらかの理由でスケジューリング対象から外され ることは、いつでもあり得ます。 しかし、I/O ポートを使うほとんどのアプリケーションでは、これは実際には問題 にはならないでしょう。 この時間をできるだけ小さくするためには、nice (マニュアルページ nice(2)を 参照のこと) を使ってプロセスの優先順位を高くしたり、またはリアルタイムス ケジュールのシステム(下記を参照のこと)を使うということもできます。 (訳注:nice を有効に使うことで、プロセスの持ち時間の再計算を頻繁に行い、 それによりある程度、優先的にスケジュールされるという効果があります。)

通常のユーザモードプロセスで扱える範囲よりもっと精細なタイミング制御を 求めるのなら、ユーザモードで「リアルタイム」をサポートするための対策も あります。 Linux 2.x カーネルはソフトリアルタイムをサポートしています。 詳細は sched_setscheduler(2) の マニュアルページを参照してください。 ハードリアルタイムをサポートする特別なカーネルもあります。 これについてのもっと詳しい情報は http://luz.cs.nmt.edu/ をご覧ください。 (訳注:上記以外にもハードリアルタイムをサポートする実装が存在します。 詳細はhttp://www.linux.or.jp/link/kernel.html#RTを参照してください。)

4.1.1. スリープ:sleep()usleep()

さて、もっと簡単なタイミングについてお話しましょう。数秒のディレイが 欲しい場合には、おそらく sleep() がいい でしょう。 数十ミリ秒以上のディレイ(最小のディレイは約 10 ミリ秒のようです。)の 場合には usleep() がお薦めです。 これらの関数を呼び出すと、CPU は他のプロセスに割り当てられます(つまり プロセスが「眠る」)ので、CPU タイムが無駄になることはありません。 詳細については sleep(3)usleep(3) のマニュアルページをご覧くだ さい。

約 50 ミリ秒よりも小さいディレイに関しては(おそらくプロセッサやマシンの 速度、システムの負荷にも依存しますが)、上に述べたような CPU を放してしまう というアプローチではうまくいきません。通常、Linux のスケジューラがあなたの プロセスに対して制御を戻す前に(x86 アーキテクチャでは)少なくとも 10-30 ミ リ秒はかかるからです。 そのようなわけで、usleep(3) に小さいディレイ を指定すると通常は指定した値をちょっとだけ越えるディレイが発生します。 また最小の値は約 10 ミリ秒ということになります。

4.1.2. nanosleep()

2.0.x シリーズの Linux カーネルには nanosleep() という新たなシステムコールが付け加わりました。(詳細は nanosleep(2) のマニュアルページを参照してください。) このシステムコールを使えば(数マイクロ秒といった)短い時間のスリープまたは ディレイが可能となります。

プロセスが (sched_setscheduler()を 用いて)ソフトリアルタイムスケジューリングされている場合、2 ミリ秒以下の ディレイに対しては、nanosleep() はビジール ープを使います。 それ以上のディレイに対しては usleep() と同様 スリープします。 (訳注:ソフトリアルタイムでなければ、usleep() と同程度の時間分解能になるようです。)

このビジーループは udelay() (多くのカーネル ドライバが使うカーネルの内部ファンクション)を使っていて、ループの長さは BogoMips 値を使って計算されます。 (この手のビジーループの速さなどは BogoMips 値が正確に反映されるものの例です。) どのように動作するかについて詳細は /usr/include/asm/delay.h を参照してください。

4.1.3. ポートI/Oを使ったディレイ

ポート I/O を使えば数マイクロ秒のディレイがまた違った方法で作れます。 ポート 0x80 に何かバイトデータを出力または入力すると、プロセッサの種類や 速度に関係なく、ほぼ正確に 1 マイクロ秒待つことができます。 (入出力の方法については、前の方を読んで下さい。) これを必要な回数繰り返すことで数マイクロ秒待つこともできます。 標準的なマシンでは、このポート出力によってなにか変な副作用があったりし ないはずです。 (カーネルドライバでこれを使っているものもありますから。) この方法は {in|out}[bw]_p() でもディレイするために通常に使われている方法です。 (asm/io.h を見て下さい。)

ポートアドレス 0-0x3ff の範囲にある大抵のポートへの I/O 命令は実際のところ ほぼ正確に 1 マイクロ秒かかります。 だから例えばパラレルポートを直接扱っている場合なら、ディレイを作るためには そのポートに対して inb() をいくつか追加するだけ でいいわけです。

4.1.4. アセンブラ命令によるディレイ

そのプログラムが走るマシンのプロセッサの種類とクロック速度がわかっている 場合には、特定のアセンブラ命令をハードコードすることで、もっと短いディレイ を実現することもできます。 (しかし、プロセスがスケジューリングから外され、ディレイが長くなってしまう ことがあり得ることを忘れないで下さい。) 以下の表では、それぞれの命令で何クロック(内部クロック)消費されるかを示して います。 これによってどれくらいの時間を消費するかを知ることができます。 例えば 50MHz のプロセッサ (486DX-50 とか、486DX2-50) の場合には 1 クロックは 1/50000000 秒(= 200 ナノ秒)です。

Instruction   i386 clock cycles   i486 clock cycles
xchg %bx,%bx          3                   3
nop                   3                   1
or %ax,%ax            2                   1
mov %ax,%ax           2                   1
add %ax,0             2                   1

Pentium のクロック数は i486 と同じになるはずです。 ただし Pentium Pro/II では違います。 また add %ax, 0 は半クロックしか消費 しません。 この命令は時々他の命令とペアで実行されるからです。 (out-of-order 実行のため、相方は命令実行ストリームのすぐお隣の命令である 必要はないのです。)

上の表で、nopxchg は何も副作用はないはずです。 その他の命令はフラグレジスタを変更する可能性があります。 でも、gcc はそれを検出してくれますから、問題にはならないはずです。 xchg %bx, %bx がディレイ用の 命令としては無難な選択と言えるでしょう。

これらを使うには、プログラムの中で asm("命令") という書式を使って、上の表の命令を埋め込みます。 ひとつの asm() 文で複数の命令を埋め込む にはセミコロンで各命令をつなげます。 例えば asm("nop ; nop ; nop ; nop")nop 命令を4回実行して、i486 または Pentium プロセッサでは 4 クロックのディレイ(i386 では 12 クロック)になります。

asm() 文は、gcc によって、インラインアセ ンブラコードに変換されるので関数呼び出しのオーバーヘッドはありません。

Intel x86 アーキテクチャでは 1 クロックより短いディレイを作ることは 不可能です。

4.1.5. Pentiumのrdtscについて

Pentium プロセッサでは、リブートから現在までの経過クロックサイクル数 を知ることができます。 以下のコードです(このコードは RDTSC という CPU 命令を実行しています。):

   extern __inline__ unsigned long long int rdtsc()
   {
     unsigned long long int x;
     __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
     return x;
   }

この値がディレイとして必要な数だけ変化するのをビジーループ中で 監視すればよいでしょう。

4.2. 時間の測定

1 秒くらいの精度の時間ならば、time() を使うのが一番簡単でしょう。 もっと正確な時間が必要な場合には、gettimeofday() を使うと大体、マイクロ秒の精度があります。 (でも、前の方で述べた、スケジューリングのことは忘れないでください。) Pentium の場合には、上のコードを使うと 1 クロックサイクルの精度が出ます。

プロセスが、ある時間経過した後にシグナルを受け取るようにしたい場合には、 setitimer()alarm() を使います。 この関数の詳細についてはマニュアルページをご覧ください。