はじめに
C言語にはたくさんのスリープ関数が存在しますよね。どこで何を使えばいいのか分からなくなることもしばしば。だからといって、適当にスリープ関数を選んでいては、真のCプログラマにはなれません。Cプログラマは、多くの選択肢から最良のものを選び、他を寄せ付けないパフォーマンスを発揮させなければならない使命を持っているのです。
冗談は置いといて
この記事では高精度なスリープ処理を実現できる、usleep, nanosleep, clock_nanosleepの3つの動作の違いについて書いていこうと思います。
対象のLinuxカーネルのバージョンは5.4.2です。
最初に結論
この節以降はLinuxカーネルのソースコードを読んだりするわけですが、そんなことは知らんから、早く結論を教えろという人のために、結論を最初に書いておきます。
なんと、この3つの関数はほぼ同じ動作をします!
clock_nanosleepだけは、引数によって、挙動を変えることができますが、clock_nanosleepの第一引数にCLOCK_MONOTONICを渡すと、3つの関数の動作は同じになります。
ですが、これらの関数はそれぞれ使い勝手が異なります。それぞれの場面で呼びやすい関数を用いることが最適だと思われます。
ソースコードレベルで一致していることを確認したい方は以降の節をご覧ください。
そもそもどういう関数なのか
usleep, nanosleep, clock_nanosleepはそれぞれどういう関数なのか見ていこうと思います。
これらの情報はmanのLinux Programmer's Manualから取ってきています。
usleep
usleep - マイクロ秒単位で実行を延期する
# include <unistd.h>
int usleep(useconds_t usec);
nanosleep
nanosleep - 高精度なスリープ
# include <time.h>
int nanosleep(const struct timespec *req, struct timespec *rem);
clock_nanosleep
clock_nanosleep - 指定したクロックでの高精度な実行停止 (sleep)
# include <time.h>
int clock_nanosleep(clockid_t clock_id, int flags,
const struct timespec *request,
struct timespec *remain);
まあ、大体やりたいことは同じっぽいです。ナノ秒レベルでスリープをしたい様です。
システムコールかライブラリ関数か
次に、それぞれの関数をシステムコールかライブラリ関数かで分類してみたいと思います。
(まあ、システムコールもアセンブラをラップするために、ライブラリ側がインターフェイスを提供しているわけですが、ここではLinuxカーネルのシステムコールとして定義されているかされていないかで分類します。)
usleep | nanosleep | clock_nanosleep |
---|---|---|
ライブラリ関数 | システムコール | システムコール |
こんな感じになっています。
では、次にソースコードレベルでそれぞれの関数を見ていきます。
ソースコードを読んで見る
usleep
では、usleepの定義を見ていきます。
usleepはglibcによって定義される関数でLinuxのシステムコールではありません。すると、usleepはその関数の中でsleepを行うためのシステムコールを呼び出しているであろうことが予想できます。実際にglibcのソースコードを覗いてみると、次のようになっていました。
int
usleep (useconds_t useconds)
{
struct timespec ts = { .tv_sec = (long int) (useconds / 1000000),
.tv_nsec = (long int) (useconds % 1000000) * 1000ul };
/* Note the usleep() is a cancellation point. But since we call
nanosleep() which itself is a cancellation point we do not have
to do anything here. */
return __nanosleep (&ts, NULL);
}
このソースコードとコメントを見るに、usleepはnanosleepシステムコールを呼び出しているようです。つまり、
glibc「nanosleepの引数のtimespec構造体作るの面倒だろ?ラッパ関数定義しといたぜ」
というような定義になっています。
したがって、マイクロ秒を受け取り、それを元にtimespec構造体を生成、最終的にnanosleepシステムコールを呼び出す関数がusleepだと言えそうです。ひとまずこれで、usleepとnanosleepは同様の動作をするだろうということがわかりました。
nanosleep
nanosleepはLinuxのシステムコールであるため、Linuxカーネルのソースコードを読んでいきます。nanosleepがシステムコールとして定義されているのは
/kernel/time/hrtimer.c line:1945
https://elixir.bootlin.com/linux/v5.4.2/source/kernel/time/hrtimer.c#L1945
です。
SYSCALL_DEFINE2(nanosleep, struct __kernel_timespec __user *, rqtp,
struct __kernel_timespec __user *, rmtp)
{
struct timespec64 tu;
if (get_timespec64(&tu, rqtp))
return -EFAULT;
if (!timespec64_valid(&tu))
return -EINVAL;
current->restart_block.nanosleep.type = rmtp ? TT_NATIVE : TT_NONE;
current->restart_block.nanosleep.rmtp = rmtp;
return hrtimer_nanosleep(&tu, HRTIMER_MODE_REL, CLOCK_MONOTONIC);
}
最終的にhrtimer_nanosleepという関数を呼び出していることがわかります。この関数はHigh Resolution Timerと呼ばれる高精度タイマを利用したsleep処理を行う関数です。hrtimer_nanosleep関数の第三引数にはCLOCK_MONOTONICが渡されています。おや、これはclock_nanosleepにも渡すことができる定数だったはず。なにやら匂ってきたところで、clock_nanosleepシステムコールの実装を見ていこうと思います。
clock_nanosleep
最後にclock_nanosleepのソースコードを読んでいきます。このシステムコールは
/kernel/time/posix-stubs.c line: 124
https://elixir.bootlin.com/linux/v5.4.2/source/kernel/time/posix-stubs.c#L124
に定義されています。
SYSCALL_DEFINE4(clock_nanosleep, const clockid_t, which_clock, int, flags,
const struct __kernel_timespec __user *, rqtp,
struct __kernel_timespec __user *, rmtp)
{
struct timespec64 t;
switch (which_clock) {
case CLOCK_REALTIME:
case CLOCK_MONOTONIC:
case CLOCK_BOOTTIME:
break;
default:
return -EINVAL;
}
if (get_timespec64(&t, rqtp))
return -EFAULT;
if (!timespec64_valid(&t))
return -EINVAL;
if (flags & TIMER_ABSTIME)
rmtp = NULL;
current->restart_block.nanosleep.type = rmtp ? TT_NATIVE : TT_NONE;
current->restart_block.nanosleep.rmtp = rmtp;
return hrtimer_nanosleep(&t, flags & TIMER_ABSTIME ?
HRTIMER_MODE_ABS : HRTIMER_MODE_REL,
which_clock);
}
この関数では、まず、指定したclockidを調べています。CLOCK_REALTIME, CLOCK_MONOTONIC, CLOCK_BOOTTIME以外では、エラーになるようです。その後を読んでみると、なんと、flagsのチェック以外、前節で見たnanosleepシステムコールの定義とほぼ同じことをしています。
一応、nanosleepと異なる記述である
flags & TIMER_ABSTIME ?
HRTIMER_MODE_ABS : HRTIMER_MODE_REL
この部分も簡単に説明しておきます。
flagsはclock_nanosleepシステムコールに渡すことができるフラグです。このフラグにTIMER_ABSTIMEが立っていると、HRTIMER_MODE_ABSが、立っていないとHRTIMER_MODE_RELがhrtimer_nanosleep関数に渡されます。manの記述によると、TIMER_ABSTIMEフラグを指定すると、
flags が TIMER_ABSTIME の場合、 remain 引き数は使用されず、不要である (絶対値での停止では、同じ request 引き数を使って再度呼び出すことができる)。
とのことでした。
つまり、この処理は、clock_nanosleepのフラグを処理するために追加されていたものだとわかりました。
ソースコードを読んだ結論
結論は上の方で言ってしまっていますが、どうやら、これら3つの関数はほぼ同じ動作をする様です。言い換えれば、どの方法も最終的にはhrtimer_nanosleep関数を呼び出しており、その関数に渡す引数の決定過程が異なるということになります。強いて違いを述べるとすれば、その柔軟性だと思います。usleepやnanosleepはスリープのフラグとしてCLOCK_MONOTONICのみしか利用することができませんが、clock_nanosleepは、自由にプログラマが決めることができます。
おわりに
結局差があまり無いという結論でしたが、この結論にたどり着くためにはオープンソースの文化が必須でした。
オープンソース最高、これからも自由なソフトウェアが発展すると良いですね!