時間・時刻処理について(5)

このエントリーをはてなブックマークに追加

前回で秒以下の時間の取得が出来るようになりました。
今回は標準関数の範囲内では実現できなかった基本的なスリープ系の処理について解説します。
今回解説する関数によって秒以下のスリープが可能になります。

スリープ系関数

そもそもスリープは時間処理系において重要な処理ですが、なぜか標準関数の範囲内では出てきませんでした。
これはそもそも時間の管理がOSが行っているので、標準化が難しかったからでしょう。
今回解説する関数は4つです。この中で通常よく使う関数はsleep(3)とnanosleep(3)でしょう。

関数名説明
sleep(3)指定した秒数の間スリープします
usleep(2)指定したマイクロ数の間スリープします(現在は非推奨です)
nanosleep(2)指定したナノ秒の間スリープします
clock_nanosleep(2)指定したクロック(ナノ秒)の間スリープします

また、このような細かい時間処理を行う前には時間や時刻についてを思い出してください。
特にtickの部分は大切です。無制限に短いスリープが使えるという訳では無いので気をつけてください。


sleep(3)

さてまずは基本的なsleep(3)関数です。POSIXに準拠しているので大抵のシステムで動くはずです。
動作としては秒単位のスリープを実現します。

//sleep.c
#include <stdio.h>
#include <time.h>
#include <sys/time.h>

int main(void)
{
  struct timeval t0;
  struct timeval t1;

  gettimeofday(&t0, NULL);

  sleep(2);

  gettimeofday(&t1, NULL);

  if (t1.tv_usec < t0.tv_usec) {
    printf("%d.%d\n", t1.tv_sec - t0.tv_sec - 1, 1000000 + t1.tv_usec - t0.tv_usec);
  }
  else {
    printf("%d.%d\n", t1.tv_sec - t0.tv_sec, t1.tv_usec - t0.tv_usec);
  }

  return 0;
}


実行するとほぼ指定した秒数スリープしている事が分かります。
多少の時間差があるのはOSとプロセスの切り替えが行われている為です。

$ gcc -o sleep sleep.c
$ ./sleep
2.55

非常に単純な処理ですが、sleep(3)が呼ばれると現在のCPU資源を明示的に他プロセス/スレッドに譲ります。
Linux等通常のOSは常にマルチプロセスということを忘れないでください。CPU時間を無駄遣いしないように気をつけるのがマナーというものです。

usleep(2)

次はusleep(2)です。sleep(3)よりも短いマイクロ秒(1/1000000秒)でのスリープが出来ます。
注意事項としてこの関数を新規開発で使用してはいけません。manには次のように書かれています。

4.3BSD, POSIX.1-2001. POSIX.1-2001 では、この関数は過去のものと宣言されている。
代わりに nanosleep(2) を使うこと。 POSIX.1-2008 では、 usleep() の規定が削除されている。


この関数をわざわざ紹介したのは過去のソースコードを読む場合などの状況が考えられるからです。
では一応ソースコードを紹介します。使い方はsleep(3)とほとんど代わりはありません。

//usleep.c
#include <stdio.h>
#include <time.h>
#include <sys/time.h>

int main(void)
{
  struct timeval t0;
  struct timeval t1;

  gettimeofday(&t0, NULL);

  usleep(1000000);

  gettimeofday(&t1, NULL);

  if (t1.tv_usec < t0.tv_usec) {
    printf("%d.%d\n", t1.tv_sec - t0.tv_sec - 1, 1000000 + t1.tv_usec - t0.tv_usec);
  }
  else {
    printf("%d.%d\n", t1.tv_sec - t0.tv_sec, t1.tv_usec - t0.tv_usec);
  }

  return 0;
}


実行してみると、正しくスリープしている事が分かります。sleep(2)と同じく多少のずれはありますが。

$ gcc -o usleep usleep.c
$ ./usleep
1.50

nanosleep(2)

usleep(2)に代わりsleep(3)よりも短いスリープが必要な場合はこちらを使用します。指定できる時間はナノ秒(1/1000000000秒)と非常に短いです。
sleep(3)よりも時間精度が短いためこちらの方が使用頻度が高いと思われます。
また、nanosleep(2)には利点がありますが、この部分をmanから引用します。

sleep(3)やusleep(3)に比べるとnanosleep()には以下のような利点がある:停止期間の指定に関して高い時間分解能が提供されている。
シグナルと互いに影響を及ぼすことがないとPOSIX.1で明示的に規定されている。シグナルハンドラによって割り込まれた際に、停止を再開するのが より簡単にできる。


ではソースコードをまずは見てみます。

//nanosleep.c
#include <stdio.h>
#include <time.h>
#include <sys/time.h>

int main(void)
{
	struct timeval t0;
	struct timeval t1;
	struct timespec ts;

	ts.tv_sec = 1;
	ts.tv_nsec = 0;

	gettimeofday(&t0, NULL);

	nanosleep(&ts, NULL);

	gettimeofday(&t1, NULL);

	if (t1.tv_usec < t0.tv_usec) {
		printf("%d.%d\n", t1.tv_sec - t0.tv_sec - 1, 1000000 + t1.tv_usec - t0.tv_usec);
	}
	else {
		printf("%d.%d\n", t1.tv_sec - t0.tv_sec, t1.tv_usec - t0.tv_usec);
	}

	return 0;
}

引数について解説します。
まず、nanosleep(2)のプロトタイプですが次のように宣言されています。
第1引数はstruct timespec型で秒とナノ秒があります。
第2引数はシグナルで割り込まれた場合の残り時間を保存する場所でこれにより残り時間のスリープを再度行うことができます。ただあまり使うことはないでしょう。

int nanosleep(const struct timespec *req, struct timespec *rem);

struct timespec {
	time_t tv_sec;        /* 秒 */
	long   tv_nsec;       /* ナノ秒 */
}

reqは実際にスリープする時間をtimespec型で指定します。
remですが、スリープ中にシグナルが発生すると残り時間をremに設定します(remがNULLでない場合)。
詳しい関数の仕様についてはmanを見てもらう必要がありますが、シグナルを気にしない場合はremはNULLで大丈夫です。
また、usleep(2)よりも細かい精度を持つため、通常はnanosleep(2)を使う方が汎用性は高くなります。


実行してみると、正しくスリープしている事が分かります。多少のずれに関してはsleep(3)、usleep(3)と同様の現象です。
注意事項として、nanosleep(2)はナノ秒までスリープが可能ですが、gettimeofday(2)はマイクロ秒までしか計測できません。getrusage(2)も同様です。

$ gcc -o nanosleep nanosleep.c
$ ./nanosleep
1.53

clock_nanosleep(2)

nanosleep(3)と同様にナノ秒のスリープが可能です。Linux 2.6系で入った関数ですが、nanosleep(3)と精度が同じならば何故あるのか? が疑問になります。
その答えは、manにあります。

int clock_nanosleep(clockid_t clock_id, int flags,
                    const struct timespec *request,
                    struct timespec *remain);

nanosleep(2) と違うのは、呼び出し側が停止期間をどのクロックに対して計測するのかを選択できる点と、停止期間を絶対値でも相対値でも指定できる点である。

CLOCK_REALTIME システム全体で使われる実時間クロック。 このクロックは変更可能である。
CLOCK_MONOTONIC 過去のある時点からの時間を計測する、単調増加のクロック。起点となる時点はシステム起動後には変更されない。 このクロックは変更することができない。
CLOCK_PROCESS_CPUTIME_ID そのプロセスの全スレッドで消費されるCPU 時間を計測するプロセス単位の クロック。このクロックは設定可能である。

これらのクロックの詳細については clock_getres(2) を参照。

flags が 0 の場合、request に指定された値はclock_id で指定されたクロックの現在の値からの相対的な期間と解釈される。

flags が TIMER_ABSTIME の場合、 request は指定されたクロックで計測される絶対時刻と解釈される。request が指定されたクロックの現在の値以下の場合、clock_nanosl eep() は、呼び出したスレッドの停止を行わず、すぐに返る。


つまり、スリープする対象を細かく指定が可能で、よりシステムを考慮したプログラミングが出来るのです。
ではソースコードを見ていきます。

//clock_nanosleep.c
#include <stdio.h>
#include <time.h>

int main(void)
{
	struct timeval t0;
	struct timeval t1;
	struct timespec ts;

	ts.tv_sec = 1;
	ts.tv_nsec = 0;

	gettimeofday(&t0, NULL);

	clock_nanosleep(CLOCK_REALTIME, 0, &ts, NULL);

	gettimeofday(&t1, NULL);

	if (t1.tv_usec < t0.tv_usec) {
		printf("%d.%d\n", t1.tv_sec - t0.tv_sec - 1, 1000000 + t1.tv_usec - t0.tv_usec);
	}
	else {
		printf("%d.%d\n", t1.tv_sec - t0.tv_sec, t1.tv_usec - t0.tv_usec);
	}

	return 0;
}

この程度ならば特にclock_idやflgを修正しても特に変化は見られないでしょう。
特に、時間計測にtime(2)を使用しているので秒以下の測定は出来ないという事もあります。
しかし、スレッドと組み合わせてリアルタイム性を求める場合等には非常に有効な手段です。
欠点として、POSIX準拠の関数でありながら、Linuxでは2.6から新規に追加されている点です。

古いシステムまで含めた移植性を考慮しなければならない場合はまだ使用しない方が良いのかもしれません。古いシステムを使うのは例えば開発が終了したハードウェア、例えばZaurus(SL)は今でも愛好者がいますが、Linuxは2.4のままだったはずです。(筆者は持っていませんが...)

Linuxではコンパイル時に`-lrt'が必要になるので忘れないでください。
コンパイル時のオプションはOSによって異なる場合があります。できる限り使用する関数のmanには一度目を通すようにしてください。

$ gcc -o clock_nanosleep clock_nanosleep.c -lrt
$ ./clock_nanosleep
1.48

まとめ

今回はスリープをメインに解説をしてきました。
スリープはプロセスやスレッド等でCPU占有を避ける為にも必須な機能です。
システムプログラミングはシステムを直接触るので、これを気にシステム全体への注意も身に付けてもらえればと思います。
さて、次回は時間の設定について解説します。