strace に clock_gettime() が出てこない理由:Linux の vDSO

プログラムは clock_gettime() を呼んでいるのに strace には出てこないことがあります。Linux カーネルがプロセスに組み込む vDSO とは何か、トレース結果をどう読めばよいかをまとめます。

linuxvdsostracesyscallclock_gettime

時刻を頻繁に読むプログラムを strace でトレースすると、clock_gettime が1行も出てこないことがあります。コードには clock_gettime() の呼び出しがあり、出力も正しいのにです。トレースが取りこぼしたのではなく、その呼び出しがシステムコールを使わずに終わった可能性が高いと考えられます。Linux でこれを可能にしているのが vDSO です。

この記事では、Linux カーネルのドキュメントと Linux man-pages をもとに、vDSO とは何か、strace の結果をどう解釈すればよいかを説明します。各クロック(clock ID)が何を測るかは timeのreal・user・sysの違い で扱ったので、ここでは繰り返しません。

vDSO はカーネルが組み込む小さな共有ライブラリ

vDSO(virtual dynamic shared object)は、カーネルがユーザー空間プログラムのアドレス空間に自動でマップする小さな共有ライブラリです。カーネルはプログラムをロードするときに、完全な ELF イメージを自らマップします。カーネルの ABI ドキュメントはこれを「一部のアーキテクチャで」起きることとして説明しており、vdso(7) のマニュアルもベースアドレスは「vDSO が存在すれば」渡されると書いています。どの Linux 環境にも必ずあると考えてはいけません。

名前もアーキテクチャごとに異なります。vdso(7) の表によると、x86-64 と aarch64 では linux-vdso.so.1、i386 では linux-gate.so.1、ppc/64 では linux-vdso64.so.1 です。glibc の ldd の出力でこの名前を見かけることは多いものの、マニュアルは正確な名前に依存するコードを書かないよう求めています。

なぜシステムコールを省けるのか

システムコールには、ユーザー空間を抜けてカーネルに入る切り替えのコストがかかります。vdso(7) は、あまりに頻繁に呼ばれるためにこのコストが全体の性能を左右するほどになるシステムコールがあると説明しています。現在時刻を問い合わせる呼び出しがその代表です。タイムスタンプ、時間計測のループ、ポーリングは、どれも「いま何時か」を繰り返し尋ねます。

現在時刻は秘密の情報でもありません。root でも一般ユーザーでも同じ答えが返ります。そこでカーネルは、答えを出すのに必要な情報をプロセスが読めるメモリに置き、vDSO 内の関数がそのメモリを読んで答えを組み立てます。vdso(7) の言い方では、gettimeofday(2) の呼び出しが「システムコールから、通常の関数呼び出しと数回のメモリアクセスに変わる」ということです。

この経路が使われるのは、そのアーキテクチャとカーネルがその関数を vDSO で提供している場合に限られます。どれだけ速くなるかの数値は原典になく、この記事でも示しません。

直接呼ぶのではなく libc が使う

一般的なアプリケーションのコードは vDSO を意識する必要がありません。いつもどおり clock_gettime() を呼べば、C ライブラリが vDSO で使える機能を確かめて利用します。vdso(7) は、C ライブラリを通さずにアプリケーションから vDSO を直接呼ぼうとしているなら「おそらくやり方を間違えている」とまで書いています。カーネルの ABI ドキュメントは、glibc に動的リンクしたプログラムは vDSO を自動的に使うと説明しています。静的リンクや他の libc については同じことを述べていないので、そうした環境ではその libc のドキュメントを別途確認してください。

直接呼ぶべきでない理由は、ドキュメントのあちこちに書かれています。

vDSO の関数の呼び出し規約はプラットフォームの ABI に従います。カーネルの ABI ドキュメントによれば、多くのプラットフォームでは普通の C 関数のように呼びますが、powerpc などではシステムコールと同じ規約を使います。不正なポインタを渡すと、-EFAULT ではなく SIGSEGV を受け取ることもあるとされています。

シンボル名とその一覧もアーキテクチャごとに違います。vdso(7) の表では、x86-64 では __vdso_clock_gettime、aarch64 では __kernel_clock_gettime です。時刻関連のシンボルが表にまったくないアーキテクチャもあります。

vDSO のシンボルにはバージョンが付いています(GNU のシンボルバージョン形式)。たとえば x86-64 の __vdso_clock_gettime は LINUX_2.6、aarch64 の __kernel_clock_gettime は LINUX_2.6.39 です。シンボルを探すときは、期待するバージョンを必ず指定します。カーネルの ABI ドキュメントは、あるバージョンのシンボル集合とその ABI は安定しているとしながら、アーキテクチャによって異なりうると付け加えており、このドキュメントが確認済みなのは x86_64 だけだという注記も残しています。

プロセスは vDSO の場所をどう知るのか

カーネルはプログラムを起動するとき、補助ベクタ(auxiliary vector)の AT_SYSINFO_EHDR エントリに vDSO のベースアドレスを入れて渡します。この値は glibc の getauxval() で読めます。アドレスは通常 execve のたびにランダム化されるので、特定の位置にあると仮定してはいけません。

getauxval() は POSIX の関数ではなく GNU の拡張で、glibc 2.16 で追加されました。目的のエントリがなければ 0 を返し、glibc 2.19 以降はその場合に errno を ENOENT に設定します。2.19 より前は「エントリがない」と「値が本当に 0」を区別する方法がありませんでした。確認するときは、呼び出し前に errno を 0 にしておき、戻り値が 0 なら errno もあわせて見ます。0 で errno が ENOENT ならエントリがありません。0 なのに errno が変わっていなければ、2.19 より前の glibc ではエントリがないのか値が 0 なのかわからないため、下の例ではこの場合を別に表示します。

どこまでが POSIX で、どこからが Linux の実装か

clock_gettime() 自体は POSIX のインターフェースです。マニュアルの STANDARDS には POSIX.1-2024 とあり、POSIX.1-1996 からある関数です。一方、vDSO は Linux の実装方法です。clock_gettime(2) のマニュアルは vDSO を規格の説明ではなく「C library/kernel differences」の項で取り上げ、「一部のアーキテクチャでは clock_gettime() の実装が vDSO で提供される」とだけ書いています。

つまり、プログラムが当てにできるのは clock_gettime() が仕様どおりに動くことまでです。その呼び出しがシステムコールを通るのか vDSO 内で終わるのかは、アーキテクチャ、カーネル、C ライブラリの組み合わせで変わる実装の詳細です。

clock_gettime() が常に vDSO で終わるわけではない

vDSO に clock_gettime があっても、すべての呼び出しがシステムコールなしで終わるわけではありません。vdso(7) には、実際にそうならなかった例が書かれています。ppc/64 では Linux 4.16 より前、ppc/32 では Linux 5.6 より前に、CLOCK_REALTIME_COARSE と CLOCK_MONOTONIC_COARSE が vDSO の __kernel_clock_gettime でサポートされておらず、その場合は本来のシステムコールにフォールバックしていました。clock_gettime(2) のマニュアルも、この2つの COARSE クロックにはアーキテクチャごとのサポートと、おそらく vDSO 側のサポートも必要だと書いています。

いま使っているシステムでどのクロックが vDSO で処理されるかは、これらのドキュメントだけでは一般化できません。この記事も、特定の clock ID がどのシステムでも速いとは約束しません。

strace の結果の読み方

strace はシステムコールをトレースするツールです。vdso(7) は、vDSO がエクスポートするシンボル(システムコール)は strace の出力に現れないとはっきり書いています。同じ理由で seccomp(2) のフィルタからも見えません。

したがって、解釈は次のようになります。

  • トレースに clock_gettime の行がないことは、プログラムが時刻を尋ねなかった証拠にはなりません。vDSO で処理された可能性があります。
  • トレースに clock_gettime の行があれば、その呼び出しは実際にカーネルに入っています。vDSO がそのケースを処理できなかったのか、C ライブラリが vDSO を使わなかったのかもしれませんが、どちらなのかはトレースだけではわかりません。
  • seccomp フィルタで時刻関連のシステムコールをブロックしたり記録したりする場合も、同じ点を考慮する必要があります。

自分で確かめる

次は、vDSO のベースアドレスを表示し、clock_gettime() を3回呼ぶ小さな C プログラムです。この記事の執筆時にこのコードをコンパイルや実行はしていないため、出力例は載せていません。

#include <errno.h>
#include <stdio.h>
#include <sys/auxv.h>
#include <time.h>

int main(void)
{
    errno = 0;
    unsigned long base = getauxval(AT_SYSINFO_EHDR);
    if (base != 0)
        printf("AT_SYSINFO_EHDR = %#lx\n", base);
    else if (errno == ENOENT)
        printf("AT_SYSINFO_EHDR not found\n");
    else
        printf("AT_SYSINFO_EHDR: 0 or not found (ambiguous before glibc 2.19)\n");

    for (int i = 0; i < 3; i++) {
        struct timespec ts;
        if (clock_gettime(CLOCK_MONOTONIC, &ts) == -1) {
            perror("clock_gettime");
            return 1;
        }
        printf("%lld.%09ld\n", (long long) ts.tv_sec, ts.tv_nsec);
    }
    return 0;
}

glibc 2.17 以降なら cc -o vdso-demo vdso-demo.c でビルドできます。それより古い glibc では clock_gettime() が librt にあるため -lrt が必要です。

strace ./vdso-demo

出力に clock_gettime の行が何回出るかを見ます。printf が出力に使う write など、ほかのシステムコールは表示されることがあります。時刻が3回表示されたのに clock_gettime の行がなければ、それらの呼び出しはシステムコールなしで終わったと読めます。逆に3行見えるなら、この環境ではシステムコールの経路を通ったということです。

補助ベクタはプログラムを変更しなくても見られます。getauxval(3) のマニュアルは、LD_SHOW_AUXV 環境変数を設定してプログラムを実行すると、そのプログラムが受け取った補助ベクタを確認できると説明しており、次の例を挙げています。

LD_SHOW_AUXV=1 sleep 1

ここに AT_SYSINFO_EHDR のエントリがあれば、カーネルが vDSO を渡しています。ただし、それだけでは clock_gettime() の特定のクロックが vDSO で処理されることまでは確認できません。実行中の別のプロセスの補助ベクタは、権限があれば /proc/<pid>/auxv から読めます。

参考資料

確認日:2026-10-18。man-pages の git 上の原文と、man7.org の HTML(man-pages 6.19 基準)をあわせて参照しました。

ブログへ