システムコールとは(ユーザ空間とカーネル空間の境界)
プログラムが画面表示・ファイル読み書き・通信などを行うとき、ハードウェアを直接触ることはできません。これらはカーネルだけが持つ特権で、アプリは「システムコール」という窓口を通じてカーネルに作業を依頼します。CPUにはユーザ空間(アプリが動く制限された世界)とカーネル空間(特権を持つOSの世界)の区別があり、システムコールはその境界を安全にまたぐ唯一の正式な入り口です。呼び出すとCPUは特権モードへ切り替わり、要求された処理を終えると元のアプリへ戻ります。この仕組みのおかげで、アプリのバグが直接ハードウェアやOS全体を壊すことを防げます。
プログラムが画面に文字を出したり、ファイルを保存したり、ネットワークで通信したりするとき、その仕事を実際にこなしているのはカーネルだ。アプリは自分でハードウェアを動かしているわけではない。
なぜカーネルなのか。ディスクやネットワークカード、ディスプレイといったハードウェアを直接操作する権限は、OSの中核であるカーネルだけが握っているからだ。
もしアプリが好き勝手にハードウェアを叩けてしまったら、一つのプログラムのバグや暴走が、ほかのプログラムやOS全体、さらには機器そのものまで巻き込んで壊しかねない。
そこでCPUとOSは協力して、アプリが動く世界とカーネルが動く世界をはっきり分け、両者の行き来を厳しく管理している。
この「アプリからカーネルへ仕事を頼むための、ただ一つの正式な窓口」がシステムコール(syscall)だ。
低レイヤを学ぶうえで、まずこの窓口の存在と意味を押さえておくと、これから扱うファイル入出力やプロセス生成の理解がぐっと楽になる。
🌗 世界は二つに分かれている
CPUには「いまどこまでの操作を許すか」を切り替える特権レベルの仕組みが備わっている。これを使って、システムが扱うメモリ空間は大きく二つの領域に分けられる。
一つはユーザ空間(userspace)だ。アプリやコマンド、サービス、シェルといった通常のプログラムが動く場所で、ここでは権限が制限されている。ハードウェアへの直接アクセスや、ほかのプロセスのメモリへの侵入はできない。
もう一つはカーネル空間(kernelspace)で、カーネル本体やデバイスドライバが動く。こちらはすべての権限を持つ特権的な世界だ。
アプリのコードは普段、ユーザ空間で、CPUの「ユーザモード」という制限された状態で実行されている。
二つを分けておく利点ははっきりしている。ユーザ空間で起きた問題がカーネル空間へ直接波及するのを防ぎ、システム全体の堅牢さと安全性を保てる。
Linuxの構造を上から眺めると、アプリ・コマンド・サービス・シェルといったユーザランドが上の層にあり、その下にカーネルとその一部であるデバイスドライバ、さらに下に実際のハードウェアがある。システムコールは、この上下の層を安全につなぐ橋だ。
🚪 境界をまたぐ一瞬に起きること
アプリがシステムコールを発行すると、CPUは制限されたユーザモードから、全権を持つカーネルモードへと切り替わる。この切り替えをモード遷移と呼ぶ。
具体的には、どの機能を呼びたいかを示す番号と引数を所定の場所に並べてカーネルへ制御を移す。カーネルは要求された処理(たとえばディスクからの読み出し)を肩代わりして実行する。
処理が終わると、結果を持ってふたたびユーザモードへ戻り、アプリは中断した続きから実行を再開する。
ここで大事なのは、この境界の出入りはアプリが自分の好きなアドレスへ飛び込めるわけではない、という点だ。カーネルがあらかじめ用意したただ一つの入口を必ず通る。
だからこそアプリは「お願い」はできても「乗っ取り」はできない。カーネルは渡された引数を一つずつ検査したうえで、安全に処理を進められる。
この一方向の関所があるおかげで、信頼できないアプリと、信頼すべきカーネルとが、同じ機械の上で安全に同居できる。
🔄 モード遷移とコンテキストスイッチの違い
よく似た用語にコンテキストスイッチ(contextswitch)があり、システムコールによるモード遷移と混同しがちだ。ここで区別しておく。
モード遷移は、同じプロセスのまま、CPUの権限レベルがユーザモードとカーネルモードのあいだで切り替わることを指す。実行しているプログラムそのものは変わらない。
一方コンテキストスイッチは、CPUがいま動かしているプロセスを、いったん脇に置いて別のプロセスに切り替える動作を指す。
コンテキストスイッチでは、レジスタの内容やメモリの対応づけといった「そのプロセスの実行状態(コンテキスト)」をまるごと保存し、次のプロセスのものを読み込み直す。そのぶんモード遷移よりも重い処理になる。
両者の絡み方はこうだ。システムコールは必ずモード遷移を伴うが、必ずしもコンテキストスイッチを伴うわけではない。
たとえばディスク待ちのように時間のかかる処理では、待っているあいだにカーネルが別のプロセスへコンテキストスイッチして、CPUを遊ばせないようにする、という連携が起こる。
⏱ 境界を越えるコスト
ユーザモードとカーネルモードの行き来には、わずかとはいえ処理コストがかかる。レジスタの保存や引数の検査といった手続きが毎回挟まるためだ。
このため、損をする書き方がある。たとえば1バイトずつ何百万回も書き込みのシステムコールを呼ぶと、その小さなコストが塵も積もって全体が目に見えて遅くなる。
C標準ライブラリのprintfなどが、出力をいったん内部にため込んでからまとめて書き出す(バッファリングする)のは、システムコールの呼び出し回数そのものを減らして、この境界越えのコストを節約するためでもある。
ここから一つの勘所が出てくる。「カーネルに頼む仕事はできるだけまとめて、回数を減らす」という発想は、性能を意識した低レイヤプログラミングの基本になる。
逆に言えば、性能が出ないプログラムを調べるときは、まず無駄に多くのシステムコールを呼んでいないかを疑うのが定石だ。
🔭 見えない裏方を覗く道具
システムコールは目に見えにくい裏方だが、観察する道具はちゃんと用意されている。
一つはstraceだ。あるプログラムがどんなシステムコールをどんな引数で呼んでいるかを順番に表示してくれる。もう一つは、各システムコールの正確な仕様を引くためのマニュアル、man 2だ。
「このアプリは結局カーネルに何を頼んでいるのか」をstraceで一度自分の目で確かめると、ユーザ空間とカーネル空間の境界が、実際に発行される一行一行の呼び出しとして体感できる。
たとえば文字を一つ表示するだけのプログラムでも、その裏ではwriteというシステムコールが発行されているのが見て取れる。
この境界を意識できるようになることが、この先に登場するopen・read・write・fork・execといった具体的なシステムコールを、丸暗記ではなく仕組みとして理解するための土台になる。
つまずいたら、まずstraceで一行ずつ眺めてみる。それが、抽象を実感へ変える第一歩だ。