🐧 Linux 総合学習プラットフォーム
システムコール ・ 上級

システムコールとは(ユーザ空間とカーネル空間の境界)

プログラムが画面表示・ファイル読み書き・通信などを行うとき、ハードウェアを直接触ることはできません。これらはカーネルだけが持つ特権で、アプリは「システムコール」という窓口を通じてカーネルに作業を依頼します。CPUにはユーザ空間(アプリが動く制限された世界)とカーネル空間(特権を持つOSの世界)の区別があり、システムコールはその境界を安全にまたぐ唯一の正式な入り口です。呼び出すとCPUは特権モードへ切り替わり、要求された処理を終えると元のアプリへ戻ります。この仕組みのおかげで、アプリのバグが直接ハードウェアやOS全体を壊すことを防げます。

プログラムが画面に文字を出したり、ファイルを保存したり、ネットワークで通信したりするとき、その仕事を実際にこなしているのはカーネルだ。アプリは自分でハードウェアを動かしているわけではない。

なぜカーネルなのか。ディスクやネットワークカード、ディスプレイといったハードウェアを直接操作する権限は、OSの中核であるカーネルだけが握っているからだ。

もしアプリが好き勝手にハードウェアを叩けてしまったら、一つのプログラムのバグや暴走が、ほかのプログラムやOS全体、さらには機器そのものまで巻き込んで壊しかねない。

そこでCPUとOSは協力して、アプリが動く世界とカーネルが動く世界をはっきり分け、両者の行き来を厳しく管理している。

💡
ポイントアプリからカーネルへ仕事を頼むための、ただ一つの正式な窓口がシステムコール(syscall)だ。

この「アプリからカーネルへ仕事を頼むための、ただ一つの正式な窓口」がシステムコール(syscall)だ。

低レイヤを学ぶうえで、まずこの窓口の存在と意味を押さえておくと、これから扱うファイル入出力やプロセス生成の理解がぐっと楽になる。

🌗 世界は二つに分かれている

CPUには「いまどこまでの操作を許すか」を切り替える特権レベルの仕組みが備わっている。これを使って、システムが扱うメモリ空間は大きく二つの領域に分けられる。

一つはユーザ空間(userspace)だ。アプリやコマンド、サービス、シェルといった通常のプログラムが動く場所で、ここでは権限が制限されている。ハードウェアへの直接アクセスや、ほかのプロセスのメモリへの侵入はできない。

もう一つはカーネル空間(kernelspace)で、カーネル本体やデバイスドライバが動く。こちらはすべての権限を持つ特権的な世界だ。

アプリのコードは普段、ユーザ空間で、CPUの「ユーザモード」という制限された状態で実行されている。

ユーザ空間(ユーザモード・権限が制限)アプリ / コマンド / サービス / シェルシステムコールただ一つの正式な窓口カーネル空間(カーネルモード・全権)カーネル本体 / デバイスドライバ / ハードウェア操作

二つを分けておく利点ははっきりしている。ユーザ空間で起きた問題がカーネル空間へ直接波及するのを防ぎ、システム全体の堅牢さと安全性を保てる。

🔗
たとえユーザ空間は受付ロビー、カーネル空間は機械室。客は受付の窓口(システムコール)越しにしか頼めず、勝手に機械室へは入れない。

Linuxの構造を上から眺めると、アプリ・コマンド・サービス・シェルといったユーザランドが上の層にあり、その下にカーネルとその一部であるデバイスドライバ、さらに下に実際のハードウェアがある。システムコールは、この上下の層を安全につなぐ橋だ。

🚪 境界をまたぐ一瞬に起きること

アプリがシステムコールを発行すると、CPUは制限されたユーザモードから、全権を持つカーネルモードへと切り替わる。この切り替えをモード遷移と呼ぶ。

具体的には、どの機能を呼びたいかを示す番号と引数を所定の場所に並べてカーネルへ制御を移す。カーネルは要求された処理(たとえばディスクからの読み出し)を肩代わりして実行する。

処理が終わると、結果を持ってふたたびユーザモードへ戻り、アプリは中断した続きから実行を再開する。

アプリユーザモード(1)呼ぶ(4)続きを再開カーネルカーネルモード(2)肩代わり実行番号+引数を渡して制御を移す(3)結果を持ってユーザモードへ戻る

ここで大事なのは、この境界の出入りはアプリが自分の好きなアドレスへ飛び込めるわけではない、という点だ。カーネルがあらかじめ用意したただ一つの入口を必ず通る。

だからこそアプリは「お願い」はできても「乗っ取り」はできない。カーネルは渡された引数を一つずつ検査したうえで、安全に処理を進められる。

コツ入口は一つだけ、引数はカーネルが必ず検査する。この一方向の関所のおかげで、信頼できないアプリと信頼すべきカーネルが同じ機械に同居できる。

この一方向の関所があるおかげで、信頼できないアプリと、信頼すべきカーネルとが、同じ機械の上で安全に同居できる。

🔄 モード遷移とコンテキストスイッチの違い

よく似た用語にコンテキストスイッチ(contextswitch)があり、システムコールによるモード遷移と混同しがちだ。ここで区別しておく。

モード遷移は、同じプロセスのまま、CPUの権限レベルがユーザモードとカーネルモードのあいだで切り替わることを指す。実行しているプログラムそのものは変わらない。

一方コンテキストスイッチは、CPUがいま動かしているプロセスを、いったん脇に置いて別のプロセスに切り替える動作を指す。

コンテキストスイッチでは、レジスタの内容やメモリの対応づけといった「そのプロセスの実行状態(コンテキスト)」をまるごと保存し、次のプロセスのものを読み込み直す。そのぶんモード遷移よりも重い処理になる。

つまずきモード遷移は「同じプロセスのまま権限レベルだけ切り替え」、コンテキストスイッチは「動かすプロセスごと入れ替え」。後者のほうが重い。混同しないこと。

両者の絡み方はこうだ。システムコールは必ずモード遷移を伴うが、必ずしもコンテキストスイッチを伴うわけではない。

たとえばディスク待ちのように時間のかかる処理では、待っているあいだにカーネルが別のプロセスへコンテキストスイッチして、CPUを遊ばせないようにする、という連携が起こる。

⏱ 境界を越えるコスト

ユーザモードとカーネルモードの行き来には、わずかとはいえ処理コストがかかる。レジスタの保存や引数の検査といった手続きが毎回挟まるためだ。

このため、損をする書き方がある。たとえば1バイトずつ何百万回も書き込みのシステムコールを呼ぶと、その小さなコストが塵も積もって全体が目に見えて遅くなる。

C標準ライブラリのprintfなどが、出力をいったん内部にため込んでからまとめて書き出す(バッファリングする)のは、システムコールの呼び出し回数そのものを減らして、この境界越えのコストを節約するためでもある。

💡
ポイントカーネルに頼む仕事はできるだけまとめて、回数を減らす。これが性能を意識した低レイヤプログラミングの基本になる。

ここから一つの勘所が出てくる。「カーネルに頼む仕事はできるだけまとめて、回数を減らす」という発想は、性能を意識した低レイヤプログラミングの基本になる。

逆に言えば、性能が出ないプログラムを調べるときは、まず無駄に多くのシステムコールを呼んでいないかを疑うのが定石だ。

🔭 見えない裏方を覗く道具

システムコールは目に見えにくい裏方だが、観察する道具はちゃんと用意されている。

一つはstraceだ。あるプログラムがどんなシステムコールをどんな引数で呼んでいるかを順番に表示してくれる。もう一つは、各システムコールの正確な仕様を引くためのマニュアル、man 2だ。

strace ……プログラムが呼ぶシステムコールを順に表示。man 2 ……各システムコールの正確な仕様を引くマニュアル。

「このアプリは結局カーネルに何を頼んでいるのか」をstraceで一度自分の目で確かめると、ユーザ空間とカーネル空間の境界が、実際に発行される一行一行の呼び出しとして体感できる。

たとえば文字を一つ表示するだけのプログラムでも、その裏ではwriteというシステムコールが発行されているのが見て取れる。

この境界を意識できるようになることが、この先に登場するopen・read・write・fork・execといった具体的なシステムコールを、丸暗記ではなく仕組みとして理解するための土台になる。

つまずいたら、まずstraceで一行ずつ眺めてみる。それが、抽象を実感へ変える第一歩だ。

この項目に出てくる用語

システムコールしすてむこーる
アプリがカーネルに処理を依頼する公式の窓口。
ユーザ空間ゆーざくうかん
アプリが動く、権限を制限された領域。
カーネル空間かーねるくうかん
OSの中核が動く、特権を持つ領域。
コンテキストスイッチこんてきすとすいっち
CPUが実行する処理を別のものへ切り替える動作。

関連コマンド

man 2 openstrace

▶ 学習アプリでこの続きを学ぶ・演習する