基礎●●●○○

システムコール

System Call

アプリがカーネルの機能を借りるための唯一の正式な窓口。ファイルも通信もこの呼び出しの先にある。

概要

システムコールは、アプリケーションがカーネルの機能を借りるための正式な窓口です。ファイルを開く、データを読む、ネットワークに接続する、新しいプロセスを生む——ユーザー空間で動くプログラムは、こうしたハードウェアに関わる操作を自力ではできず、そのたびにカーネルへ「これをやってください」と依頼します。この依頼の呼び出しがシステムコールです。

普段書くコードでシステムコールが直接見えることは多くありませんが、実はほとんどの重要な操作はその先にあります。fopenprint も、掘り下げていけば openwrite といったシステムコールに行き着きます。プログラムがハードウェアに影響を及ぼす経路は、原理的にすべてシステムコールを通っている、と言ってよいほどです。だからこそ「このプログラムは実際に何をしているのか」を知りたいとき、システムコールの列を眺めるのが最も確実な観察方法になります。

システムコールがカーネルへの唯一の入口であることには、はっきりした理由があります。アプリケーションにハードウェアを直接触らせない設計を貫く以上、カーネルの力を借りる経路を一本に絞り、そこで必ず権限を検査する必要があるからです。この一本道が、OSの安定性と安全性を支えています。

なぜ生まれたか

カーネルは、ハードウェアに触れることを許された唯一の存在として作られました。しかしそうすると、アプリケーションが「ファイルを読みたい」「メモリが欲しい」と思っても、自分ではどうにもできません。かといって、ユーザープログラムが好きなときにカーネルの関数を直接呼び出せてしまうと、権限の壁は意味をなくします。悪意あるプログラムがカーネルの内部処理に勝手に飛び込めるなら、特権モードを分けた意味がありません。

そこで必要になったのが、「制御された入口」です。ユーザープログラムは、あらかじめ決められた番号の機能だけを、決められた作法で呼び出せる。呼び出しの瞬間にCPUの特権レベルがユーザーモードからカーネルモードへ切り替わり、カーネルは「誰が・何を・どんな引数で」要求したかを検査してから処理を実行する。この切り替えと検査を伴う正式な入口が、システムコールです。ハードウェアを守るために触れる者を一人に絞ったなら、その一人にお願いする窓口もまた、厳しく管理された一箇所でなければならなかったのです。

詳細

代表的なシステムコール

システムコールの多くは、ファイルと入出力に関わるものです。open でファイルを開いてファイルディスクリプタ(開いた対象を指す整数の番号)を得て、read で読み、write で書き、close で閉じる。プロセスに関わるものとしては、fork で自分の複製を作り、exec でそのプロセスを別のプログラムに置き換え、exit で終了し、wait で子の終了を回収します。メモリでは mmapbrk で領域を確保し、通信では socketconnectsendrecv を使います。Linux には数百のシステムコールがありますが、日常的に登場するのはこの程度の顔ぶれです。

面白いのは、ここに一貫した思想が貫かれていることです。UNIX 系OSでは、通常のファイルもデバイスも、そしてソケットによるネットワーク通信さえも、同じファイルディスクリプタで表され、同じ readwrite で扱えます。「すべてはファイルであり、すべてはシステムコールの先にある」——この統一観のおかげで、ローカルファイルへの書き込みとネットワークへの送信を、ほとんど同じコードで書けるのです。

呼び出しの往復を追う

システムコールが実際にどう動くかを、一往復で追ってみます。アプリケーションはたいてい、システムコールを直接書くのではなく、標準ライブラリ(C言語なら libc)の薄いラッパー関数を呼びます。そのラッパーが、決められたレジスタにシステムコール番号と引数を並べ、syscall のような「トラップ命令」を実行します。トラップ命令はCPUに特権レベルの切り替えを起こさせ、制御はカーネルの入口へ移ります。

ハードウェアカーネルライブラリアプリハードウェアカーネルライブラリアプリ引数と権限を検査read を呼ぶトラップ命令で特権モードへ切替ドライバ経由でディスクを操作データが届く結果を書き戻しユーザーモードへ復帰読めたバイト数を返す

カーネルは受け取った要求の引数と権限を検査し、問題なければデバイスドライバを通じてハードウェアを操作します。結果をアプリケーションのメモリに書き戻したうえで特権レベルをユーザーモードに戻し、ラッパー関数へ復帰します。アプリから見れば、ただの関数呼び出しが返ってきただけに見えますが、その内側ではモードの往復が起きています。

このモード切り替えにはコストがかかります。特権レベルの切り替え、レジスタの退避と復元、キャッシュへの影響などが積み重なるため、システムコールは通常の関数呼び出しよりずっと重い操作です。処理そのものが軽くても、呼び出しの回数が多いと切り替えのコストが無視できなくなります。

観察する — strace

システムコールは、プログラムの挙動を外から観察する最良の窓でもあります。Linux の strace コマンドを使うと、対象プロセスが発行したシステムコールを引数と戻り値つきで時系列に並べて見られます。「設定ファイルが読めずに落ちる」バグなら、open が目当てのパスに対して失敗(No such file や Permission denied)を返している行がそのまま原因を指し示します。ドキュメントを読むよりも、プログラムが実際にカーネルへ何を要求しているかを見るほうが早い場面は少なくありません。

コストを減らす工夫

システムコールが重いという事実は、性能設計に直結します。基本の対策は「回数を減らす」ことです。1バイトずつ write を呼ぶのではなく、バッファに溜めてまとめて書き出す(バッファリング)だけで、切り替え回数が桁違いに減ります。標準ライブラリの入出力がバッファを持つのはこのためです。

大量の入出力を捌くサーバでは、さらに踏み込んだ工夫が使われます。多数の接続を1回のシステムコールでまとめて監視する epoll、あるいは近年の io_uring は、アプリとカーネルが共有するリングバッファに要求と結果を積むことで、システムコールの往復そのものを大幅に減らします。なお、システムコールに伴うモード切り替えは、CPUを別プロセスに明け渡す割り込みやコンテキストスイッチと仕組みの一部を共有しますが、両者は別物です。前者は「同じプロセスがカーネルの助けを借りて戻ってくる」だけで、必ずしもプロセスの切り替えを伴いません。この区別を押さえておくと、性能の議論で混乱せずに済みます。