システムコール

ユーザー空間のプログラムが OS カーネルの機能を呼び出すインターフェース

OS低レベル

システムコールとは

システムコール (System Call, syscall) は、ユーザー空間のプログラムが OS カーネルの機能 (ファイル操作、ネットワーク、プロセス管理) を呼び出すインターフェースである。アプリケーションはハードウェアに直接アクセスできず、カーネルを経由する。

ユーザー空間とカーネル空間

CPU には特権レベルがあり、ユーザー空間のコードはカーネルのメモリやデバイスへ直接触れない。システムコールはこの境界を越える唯一の正規の入口で、呼び出した瞬間に CPU のモードがカーネル側へ切り替わり、処理が終わると元のモードへ戻る。

ユーザー空間 (アプリケーション)
  Node.js, Python, Go ...
  ↓ システムコール
カーネル空間 (OS)
  ファイルシステム, ネットワーク, メモリ管理
  ↓
ハードウェア
  CPU, メモリ, ディスク, NIC

主要なシステムコール

カテゴリごとに代表的なものを挙げる。名前は Linux のものだが、ファイル / プロセス / ソケットという区分自体は他の Unix 系 OS にも共通である。

カテゴリsyscall説明
ファイルopen, read, write, closeファイル操作
プロセスfork, exec, wait, exitプロセス管理
ネットワークsocket, bind, listen, acceptソケット操作
メモリmmap, brkメモリ割り当て

同じ処理でも OS ごとに呼び名や引数が異なるため、移植性が必要なコードは libc や言語のランタイムを経由し、直接 syscall を発行しない。

Node.js とシステムコール

高水準の API がどのシステムコールへ落ちるかを追うと、遅さの原因を当てやすくなる。

import { readFile } from 'fs/promises';

// readFile() → libuv → read() syscall → カーネル → ディスク
const data = await readFile('/etc/hosts', 'utf-8');

// fetch() → libuv → socket() + connect() + write() + read() syscall
const res = await fetch('https://api.example.com');

Node.js の fs, net, http モジュールは内部的にシステムコールを呼び出している。libuv がシステムコールを非同期に実行し、イベントループに結果を返す。

strace でシステムコールを観察

Linux では strace で、プロセスが実際に発行したシステムコールを引数と戻り値ごと観察できる。macOS に strace は無く、dtruss などが近い役割を担う。

# Node.js のシステムコールを表示
strace -f node -e "require('fs').readFileSync('/etc/hosts')"
# openat(AT_FDCWD, "/etc/hosts", O_RDONLY) = 3
# read(3, "127.0.0.1 localhost\n...", 8192) = 234
# close(3) = 0

-f は子プロセスとスレッドも追跡する。呼び出し回数と所要時間の内訳だけ見たいときは -c、対象を絞るときは -e trace=openat,read のように指定する。strace は 1 回ごとに呼び出しへ割り込む方式なので実行速度が大きく落ちる。本番プロセスへの常用は避け、再現環境で使う。

コンテナとシステムコール

コンテナ はホスト OS のカーネルを共有するため、コンテナ内のプロセスもホストのシステムコールを使う。裏を返せば、コンテナの分離の強さは「どのシステムコールを通すか」でほぼ決まる。seccomp はこの絞り込みを担う仕組みである。

// 特定の syscall だけを拒否する最小例 (既定は許可)
{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [
    { "names": ["reboot", "mount"], "action": "SCMP_ACT_ERRNO" }
  ]
}

Docker が既定で適用するプロファイルはこの逆で、defaultAction に SCMP_ACT_ERRNO を置いた許可リスト方式を採り、300 以上あるシステムコールのうち約 44 個を無効化する (2026 年 8 月時点)。許可リスト方式には、カーネルに後から追加されたシステムコールも自動的に拒否側へ入るという利点がある。逆に拒否リスト方式は、新しい呼び出しが素通りする。権限そのものの分離は 名前空間と cgroup が担い、seccomp はその上で入口の数を削る役割になる。

Lambda とシステムコール

Lambda の関数は分離された実行環境で動き、カーネルの設定変更やネットワークインターフェースの直接操作といった特権的な操作はできない。書き込める領域は /tmp で、容量は既定 512 MB、最大 10,240 MB まで 1 MB 刻みで設定できる (2026 年 8 月時点)。ここは同じ実行環境が再利用される間だけ残る一時領域なので、恒久的な保存先にはならない。

パフォーマンスへの影響

システムコールは呼び出しごとに CPU のモード遷移を伴う。これはプロセスの切り替え (プロセスとスレッド) とは別で、同じスレッドのまま特権レベルだけが上下する動きである。それでも引数の検証やキャッシュの汚れが積み上がるため、1 回あたりのコストは通常の関数呼び出しより桁違いに大きい。細かい read/write を大量に出すコードは、書き込みをまとめるだけで速くなることがある。

呼び出し回数そのものを減らす仕組みもカーネル側に用意されている。Linux は vDSO (virtual dynamic shared object) と呼ぶ小さな共有ライブラリを各プロセスのアドレス空間へ貼り付けており、gettimeofday のように頻繁で副作用のない呼び出しはカーネルへ入らずユーザー空間で完結する。I/O では io_uring が、ユーザー空間とカーネルで共有するリングバッファーに要求と完了を並べる方式を採り、要求 1 件ごとの呼び出しとバッファーのコピーを避ける (Linux 固有の仕組み)。

システムコールを意識すると、性能とセキュリティの両方が同じ視点で見えてくる。呼び出し回数を減らせば I/O の待ちが縮み、越えられる境界を絞れば侵害時の被害が縮む。どちらも「ユーザー空間とカーネル空間の境界を何回、どれだけ広く越えるか」という一つの問いに帰着する。

共有:

この記事は役に立ちましたか?

関連用語

関連する記事