Dev Study
← コンピュータの仕組み
レッスン 13 / 13·約 13 分·第1期 コンピュータ単体の仕組み

システムコール — アプリとカーネルの境界線

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

第1期の最終回です。ここまで見てきたCPU・メモリ・ストレージを、アプリが実際に使うときの入口を扱います。第2期からはこの機械の外、ネットワークへ出ていきます。

まず結論

ファイルを読む、通信する、メモリを確保する。こうした操作はアプリが自分で行うことができず、必ずカーネルに依頼します。その依頼の入口がシステムコールです。

システムコールは関数呼び出しに似て見えますが、CPUの特権レベルを切り替える別物です。カーネルに入って戻るだけの最小限のシステムコールでも、実測で数百ナノ秒かかります。関数呼び出しは数ナノ秒程度とされるので(出典を確かめていない目安)、おおよそ100倍の開きです。

? 読む前に予測

通常の関数呼び出しとシステムコールでは、かかる時間はどれくらい違うでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

アプリには、できないことがある

前回、OSが1つのCPUを複数のアプリに分け与えていることを見ました。ここで重要な疑問があります。OSはどうやって秩序を保っているのでしょうか。

あるアプリが「ディスクの全内容を消す」と決めたら、止められるのでしょうか。他のアプリのメモリを覗こうとしたら?

答えは、アプリにはそもそもそれができないです。

CPUには特権レベルという仕組みがあります。アプリはユーザーモードという制限された状態で動いており、この状態では次のことが禁止されています。

  • ハードウェア(ディスク・ネットワーク機器)への直接アクセス
  • 他のプロセスのメモリ参照(第9週で学んだページテーブルの話)
  • 特権的な命令の実行

では、どうやってファイルを読むのでしょうか。カーネルに頼むのです。その依頼の窓口がシステムコールです。

具体実物

ファイルを1つ読むだけで、何回も依頼している

実際にどれだけシステムコールが使われているか、ls コマンドで見てみます。strace -c は、発行されたシステムコールを種類別に集計します。

$ strace -c ls(表示例・一部の行を省略。書式はstrace公式ソースに準拠)コマンド出力
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
0.00 0.000000 0 11 read
0.00 0.000000 0 1 write
0.00 0.000000 0 10 close
0.00 0.000000 0 20 mmap
0.00 0.000000 0 8 8 access
0.00 0.000000 0 1 execve
0.00 0.000000 0 2 getdents64
0.00 0.000000 0 12 openat
0.00 0.000000 0 14 newfstatat
------ ----------- ----------- --------- --------- ----------------
100.00 0.000000 108 8 total

ファイル一覧を表示するだけで、108回もカーネルに依頼しています。openat でファイルを開き、read で読み、close で閉じる。一覧の取得は getdents64 です。

errors 列の access が8回失敗していますが、これは異常ではありません。設定ファイルを探して「無ければ次の場所」と順に試している正常な動作です。

✓ ここまでの確認

アプリがハードウェアに直接アクセスできないようになっているのは、なぜですか。

抽象原理

「呼び出し」という言葉に騙されない

システムコールは、プログラムからは関数呼び出しのように見えます。read(fd, buf, size) と書けば済むからです。

しかし中で起きていることは、関数呼び出しとまったく違います。

システムコールで実際に起きること(x86-64)
番号をraxに設定readなら0番、writeなら1番○ どの依頼か指定
引数をレジスタにrdi, rsi, rdx, r10, r8, r9○ 最大6個まで
syscall命令を実行CPUがユーザーモード→カーネルモードへ○ 特権レベルの切り替え
カーネルが処理番号で処理を振り分け、正当性を確認○ 結果をraxに入れて戻る
↓
関数呼び出し = 数ns / システムコール = 数百ns(約100倍)
  • ・飛び先はカーネルがあらかじめ決めた1箇所に固定されている
  • ・だから任意のカーネル関数を呼び出すことはできない

決定的に違うのは、CPUの状態そのものが切り替わることです。ユーザーモードからカーネルモードへ移り、処理が終われば戻ります。この切り替えに費用がかかります。

呼び出しコストの比較(関数呼び出し以外は On the Costs of Syscalls の実測)表
通常の関数呼び出し 数ナノ秒程度(出典未確認の目安)
システムコール(最小限の処理) 76〜773 ナノ秒(15台)
同じCPUで脆弱性対策なし/あり 76 / 233 ナノ秒(Xeon Gold 6256)
─────────────────────────────────────
vDSO経由(clock_gettime) 13〜31 ナノ秒
具体実物

4番目の引数だけ、レジスタが違う

システムコールが関数呼び出しと別物である証拠が、引数の渡し方に現れています。

引数を渡すレジスタの違い(x86-64 Linux)表
引数 通常のC関数 システムコール
1番目 rdi rdi
2番目 rsi rsi
3番目 rdx rdx
4番目 rcx r10 ← ここだけ違う
5番目 r8 r8
6番目 r9 r9

なぜ4番目だけ違うのでしょうか。理由は明快です。syscall 命令は実行時に rcx を戻り先アドレスで上書きしてしまうからです(r11 にはフラグが入ります)。上書きされるレジスタに引数を置いても意味がないので、r10 で代替しています。

この一点だけでも、システムコールが「ちょっと重い関数呼び出し」ではなく、CPUの動作そのものが違う別の仕組みだと分かります。

✓ ここまでの確認

システムコールで4番目の引数にrcxが使えないのはなぜですか。

抽象原理

呼ばずに済ませる工夫 — vDSO

システムコールが高くつくなら、呼ばずに済ませればいい。そういう工夫が存在します。

時刻の取得を考えてください。gettimeofday は非常に高頻度で呼ばれます。しかし現在時刻は秘密の情報ではありません。誰が読んでも同じ答えですし、読まれて困るものでもありません。

そこでカーネルは、時刻データを読み取り専用でユーザー空間にマップしておきます。アプリはカーネルに入らず、普通のメモリを読むだけで時刻を取得できます。これが vDSO です。

結果、実測で 13〜31ナノ秒。通常のシステムコールの数百ナノ秒に比べて桁違いに速くなります。

この章のまとめ

  • アプリはユーザーモードで動き、ハードウェアや他プロセスに直接触れられない。必要な操作はカーネルに依頼する = システムコール。
  • ls を1回実行するだけで100回以上のシステムコールが発行される。
  • 関数呼び出しに見えるがCPUの特権レベルが切り替わる別物。関数呼び出し数ns(目安)に対し、システムコールは数百nsで約100倍。
  • 4番目の引数が rcx ではなく r10 なのは、syscall 命令が rcx を上書きするため。
  • vDSO は時刻取得などをカーネルに入らず済ませる仕組みで13〜31ns。ただしstraceには原理的に映らない。
  • straceは計測対象を大幅に遅くする(最悪ケースの実測で442倍)。何を見ている道具かを理解して使う。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 4正解: 0

アプリがファイルを読むとき、なぜ自分で直接ディスクを操作できないのですか。

用語集(5語)
システムコール
アプリがカーネルに処理を依頼する仕組み。ファイル操作・通信・メモリ確保など、ハードウェアや他プロセスに関わる操作はすべてこれを通る。
ユーザーモード / カーネルモード
CPUの特権レベル。ユーザーモードではハードウェアへの直接アクセスが禁止される。カーネルモードでは全権限を持つ。
syscall 命令
x86-64でシステムコールを発行する命令。カーネルがあらかじめ設定した固定の入口へ飛ぶ。任意の場所へは飛べない。
vDSO
カーネルがユーザー空間に読み取り専用でマップする領域。時刻取得などをカーネルに入らずに済ませ、大幅に高速化する。
strace
プロセスが発行するシステムコールを記録するツール。ただしvDSO経由の呼び出しは捕捉できない。