- ブラウザURL入力
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理今ここ
- DB保存
第1期の最終回です。ここまで見てきたCPU・メモリ・ストレージを、アプリが実際に使うときの入口を扱います。第2期からはこの機械の外、ネットワークへ出ていきます。
まず結論
ファイルを読む、通信する、メモリを確保する。こうした操作はアプリが自分で行うことができず、必ずカーネルに依頼します。その依頼の入口がシステムコールです。
システムコールは関数呼び出しに似て見えますが、CPUの特権レベルを切り替える別物です。カーネルに入って戻るだけの最小限のシステムコールでも、実測で数百ナノ秒かかります。関数呼び出しは数ナノ秒程度とされるので(出典を確かめていない目安)、おおよそ100倍の開きです。
通常の関数呼び出しとシステムコールでは、かかる時間はどれくらい違うでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
アプリには、できないことがある
前回、OSが1つのCPUを複数のアプリに分け与えていることを見ました。ここで重要な疑問があります。OSはどうやって秩序を保っているのでしょうか。
あるアプリが「ディスクの全内容を消す」と決めたら、止められるのでしょうか。他のアプリのメモリを覗こうとしたら?
答えは、アプリにはそもそもそれができないです。
CPUには特権レベルという仕組みがあります。アプリはユーザーモードという制限された状態で動いており、この状態では次のことが禁止されています。
- ハードウェア(ディスク・ネットワーク機器)への直接アクセス
- 他のプロセスのメモリ参照(第9週で学んだページテーブルの話)
- 特権的な命令の実行
では、どうやってファイルを読むのでしょうか。カーネルに頼むのです。その依頼の窓口がシステムコールです。
ファイルを1つ読むだけで、何回も依頼している
実際にどれだけシステムコールが使われているか、ls コマンドで見てみます。strace -c は、発行されたシステムコールを種類別に集計します。
% time seconds usecs/call calls errors syscall------ ----------- ----------- --------- --------- ----------------0.00 0.000000 0 11 read0.00 0.000000 0 1 write0.00 0.000000 0 10 close0.00 0.000000 0 20 mmap0.00 0.000000 0 8 8 access0.00 0.000000 0 1 execve0.00 0.000000 0 2 getdents640.00 0.000000 0 12 openat0.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) と書けば済むからです。
しかし中で起きていることは、関数呼び出しとまったく違います。
- ・飛び先はカーネルがあらかじめ決めた1箇所に固定されている
- ・だから任意のカーネル関数を呼び出すことはできない
決定的に違うのは、CPUの状態そのものが切り替わることです。ユーザーモードからカーネルモードへ移り、処理が終われば戻ります。この切り替えに費用がかかります。
通常の関数呼び出し 数ナノ秒程度(出典未確認の目安)システムコール(最小限の処理) 76〜773 ナノ秒(15台)同じCPUで脆弱性対策なし/あり 76 / 233 ナノ秒(Xeon Gold 6256)─────────────────────────────────────vDSO経由(clock_gettime) 13〜31 ナノ秒
4番目の引数だけ、レジスタが違う
システムコールが関数呼び出しと別物である証拠が、引数の渡し方に現れています。
引数 通常のC関数 システムコール1番目 rdi rdi2番目 rsi rsi3番目 rdx rdx4番目 rcx r10 ← ここだけ違う5番目 r8 r86番目 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倍)。何を見ている道具かを理解して使う。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
アプリがファイルを読むとき、なぜ自分で直接ディスクを操作できないのですか。
用語集(5語)
- システムコール
- アプリがカーネルに処理を依頼する仕組み。ファイル操作・通信・メモリ確保など、ハードウェアや他プロセスに関わる操作はすべてこれを通る。
- ユーザーモード / カーネルモード
- CPUの特権レベル。ユーザーモードではハードウェアへの直接アクセスが禁止される。カーネルモードでは全権限を持つ。
- syscall 命令
- x86-64でシステムコールを発行する命令。カーネルがあらかじめ設定した固定の入口へ飛ぶ。任意の場所へは飛べない。
- vDSO
- カーネルがユーザー空間に読み取り専用でマップする領域。時刻取得などをカーネルに入らずに済ませ、大幅に高速化する。
- strace
- プロセスが発行するシステムコールを記録するツール。ただしvDSO経由の呼び出しは捕捉できない。