このページの目次

glibc システムコールラッパーの内部構造

カバー範囲: glibc システムコールラッパー → INLINE_SYSCALL → vsyscall/vDSO → gettimeofday 高速パス → システムコールレイテンシ → 自作 syscall ラッパー 対象: glibc 2.x、musl との比較、主に x86-64

概要

ユーザー空間のコードは syscall 命令を直接記述しません。glibc は各システムコールをラップする層を設けており、この層で以下の処理を行っています:引数の渡し方(レジスタ割り当て)、戻り値から errno への変換、取消ポイント(cancellation point)、および vDSO 高速パス。この層を理解することで、「なぜ clock_gettime が getpid よりも高速なのか」や「なぜ自作の syscall ラッパーが時として高速になるのか」といった疑問に答えられます。

glibc: INLINE_SYSCALL マクロ

// sysdeps/unix/sysv/linux/x86_64/sysdep.h
// glibc システムコールのラップ階層:

// 最下層: アセンブリマクロ
#define INTERNAL_SYSCALL(name, nr, args...)                \
({                                                          \
    unsigned long int resultvar;                            \
    LOAD_ARGS_##nr(args)          /* 引数をレジスタに配置 */    \
    LOAD_REGS_##nr                 /* rdi, rsi, rdx, ... をロード */ \
    register long int _a1 asm ("rdi");                      \
    asm volatile (                                          \
        "syscall\n\t"                                       \
        : "=a" (resultvar)                                  \
        : "0" (__NR_##name) ASM_ARGS_##nr                   \
        : "memory", "cc", "r11", "cx");                     \
    (long int) resultvar;                                   \
})

// アプリケーション層: errno への変換
#define INLINE_SYSCALL(name, nr, args...)                  \
({                                                          \
    long int sc_ret = INTERNAL_SYSCALL(name, nr, args);    \
    if (__glibc_unlikely(INTERNAL_SYSCALL_ERROR_P(sc_ret))) \
    {                                                       \
        __set_errno(INTERNAL_SYSCALL_ERRNO(sc_ret));       \
        sc_ret = -1;                                        \
    }                                                       \
    sc_ret;                                                 \
})

重要な詳細:

  • asm volatile: コンパイラが syscall を跨いでメモリアクセスを並べ替えるのを防ぎます("memory" クラバー)
  • "cc" クラバー: syscall はフラグレジスタを変更する可能性があります
  • "r11", "cx" クラバー: syscall 命令はこれらのレジスタを破壊します
  • 戻り値のチェック: __glibc_unlikely を使用し、errno 処理パスが頻繁に通らないことをコンパイラに指示します

gettimeofday / clock_gettime: vDSO 高速パス

// glibc の clock_gettime 実装 (簡略化):
int __clock_gettime64(clockid_t clock_id, struct __timespec64 *tp)
{
    // vDSO 高速パスを試みる (ユーザー空間で実行、syscall なし)
    if (__glibc_likely(clock_id < CLOCK_THREAD_CPUTIME_ID))
    {
        __vdso_clock_gettime_func vdso_func =
            GLRO(dl_vdso_clock_gettime);  // 起動時に vDSO から取得

        if (vdso_func != NULL)
        {
            int ret = vdso_func(clock_id, tp);
            if (ret == 0) return 0;
        }
    }
    // vDSO が利用できない場合 → syscall にフォールバック
    return INLINE_SYSCALL(clock_gettime64, 2, clock_id, tp);
}

// vDSO バージョン (arch/x86/entry/vdso/vclock_gettime.c):
//   1. TSC を読む (rdtsc、約10サイクル)
//   2. timekeeper 変換を適用 (乗算 + シフト、約20サイクル)
//   3. seqlock をチェック (read_seqbegin/read_seqretry、約5サイクル)
//   → 合計約35サイクル、対 syscall は約70サイクル → 約2倍高速

glibc と musl: システムコールラッパーの違い

glibcmusl
ラップの複雑さ複数階層のマクロ (INLINE_SYSCALL → INTERNAL_SYSCALL)単一階層のインラインアセンブリ
errno の処理TLS 関数呼び出しインラインで直接 __errno_location() に書き込み
取消ポイント (cancellation)read/write などの「低速」syscall 前にチェックより単純なモデル
vDSO起動時に初期化 + キャッシュ同上、ただしより軽量
コードサイズシステムコールあたり約50行(すべてのマクロ展開を含む)システムコールあたり約5行

システムコールレイテンシの分析

# システムコールレイテンシの測定 (最小限の引数0のsyscall):
# getpid() → syscall 39 (x86-64, getpid)
#   PID がキャッシュされているため → 直接返却 → PCB を実際に読みに行かない

# システムコールレイテンシ測定ツール: libMicro, lmbench
典型的なレイテンシ (3.5GHz x86-64):
  getpid (キャッシュ済み):                ~70ns (~250サイクル)
  getppid:                        ~90ns
  gettimeofday (vDSO):            ~15ns (~50サイクル)  ← 極めて高速!
  gettimeofday (syscall フォールバック): ~80ns
  write(1 byte to /dev/null):     ~200ns
  read(1 byte from /dev/zero):    ~200ns
  futex(FUTEX_WAIT, uncontended): ~150ns
  futex(FUTEX_WAIT, contended):   ~5-20μs (コンテキストスイッチを含む)

レイテンシの内訳:
  syscall 命令:                 ~20サイクル
  カーネルエントリ/アウト (swapgs + スタック切替): ~20サイクル
  実際の syscall ロジック:             ~30サイクル以上
  Spectre/Meltdown 対策:         ~20サイクル (影響を受けるCPUではKPTI有効)

手書き syscall: より高速なラッパー

// glibc の read() には一定のオーバーヘッド (errno 変換など) がある
// 熱いパスにいて、自分でエラー処理ができる場合 → 手動で syscall を書く:

static inline long __read(int fd, void *buf, size_t count) {
    long ret;
    register long rdi asm("rdi") = fd;
    register long rsi asm("rsi") = (long)buf;
    register long rdx asm("rdx") = count;
    register long rax asm("rax") = __NR_read;
    asm volatile("syscall"
                 : "=a"(ret)
                 : "r"(rdi), "r"(rsi), "r"(rdx), "a"(rax)
                 : "rcx", "r11", "memory");
    return ret;  // 負数 = -errno
}

// どの場合に価値があるか?
//   - io_uring の熱いパス → すでにラップされているため、不要
//   - カスタムアロケータ → mmap ラッパーが必要になる場合がある
//   - パフォーマンスベンチマーク → glibc のオーバーヘッドを除外するため

参考文献

  • ソースコード⁠: glibc sysdeps/unix/sysv/linux/x86_64/sysdep.h, sysdeps/unix/sysv/linux/clock_gettime.c, musl arch/x86_64/syscall_arch.h
  • LWN: "vDSO and clock_gettime", "System call overhead"
  • ツール⁠: man syscalls, strace -c (syscall count/time)

キーワード: INLINE_SYSCALL, syscall wrapper, vDSO, clock_gettime, errno, cancellation point, glibc, musl, syscall latency