OS の役目とシステムコール

回 3-1 道 3 コンピュータの中

システムコールは、アプリが OS のカーネル (装置とメモリを直接扱う OS の中心) に仕事を頼むための窓口です。Python の open() でファイルを読む処理の裏では、open、read、close の呼び出しが順に起きます。開くと小さな整数の番号 (ファイルディスクリプタ) が返り、失敗すると C の窓口は -1 を返して、理由の番号を errno に置きます。

アプリが自分でしている仕事と、OS に頼んでいる仕事を切り分けられるようになります。

ファイルを開く 1 行は、誰が開いているのか

ファイルを実際に開いているのは Python ではなく、OS (オペレーティングシステム、コンピュータの装置をまとめて管理する土台のソフト) です。Python は OS に「開いてください」と頼んでいるだけです。

Python でファイルを読むときは、次のように書きます。

ファイルを読む 2 行

with open("memo.txt") as f:
    data = f.read()

この 2 行で、ファイルを探し、読む権限を確かめ、装置から中身を運びます。けれども Python のどこにも、装置を動かす命令はありません。その仕事は OS が受け持ち、アプリが OS に仕事を頼む窓口をシステムコールと呼びます。この回では、ファイルを扱う数行が OS への 4 つの窓口に分かれる様子を、Python を動かして確かめます。

OS は何をしていて、システムコールはどう呼ばれるのか

OS は装置とメモリをアプリに分け、アプリには窓口だけを見せます。その窓口がシステムコールです。

OS の役目は、資源を分けて、装置の違いを隠すこと

OS は、CPU の時間、メモリ、ストレージ、ネットワーク、入出力という資源をアプリに分け、装置ごとの違いを隠します。下の図でアプリの本数を変えると、OS が無いときは、同じ装置の係がアプリの数だけ重なって書かれます。

OS が無いと誰が装置の面倒を見るか

カーネル空間とユーザー空間

OS の中で、装置とメモリを直接触る中心をカーネルと呼びます。カーネルが動く場所をカーネル空間、ふつうのアプリが動く場所をユーザー空間と呼び、その間には CPU が守る線が 1 本引かれています。

ユーザー空間カーネル空間
住んでいる物ブラウザ、シェル、Pythonカーネル、装置を動かすドライバ
装置を触れるか触れない。カーネルに頼む直接触れる
落ちたときそのアプリだけが止まるOS ごと止まる

ユーザー空間のアプリは、カーネルのメモリを勝手に読めません。下の図で 1 つ選んで落としてみると、管理者の権限で動くプログラムもユーザー空間の側にいて、巻き添えが広がらないことが見えます。

越えられない線が 1 本ある

線を越える手順

線を越えてカーネルに頼む手順は、Linux の説明書 intro(2) によると次の 3 段です。

  1. システムコールの番号と引数を、カーネルが待っている場所 (CPU のレジスタ、計算に使う小さな置き場) に置く
  2. 専用の命令で CPU をカーネルモード (装置を触れる状態) に切り替え、カーネルに本当の仕事をさせる
  3. カーネルが CPU をユーザーモードに戻したとき、失敗なら理由の番号を errno (失敗の理由を入れておく変数) に置く

どの番号がどの仕事かは、CPU の種類ごとに表で決まっています。Linux の x86-64 (多くの PC とサーバーの CPU) の表では、次の通りです。

番号名前仕事
0read番号のファイルから読む
1write番号のファイルへ書く
2openファイルを開いて番号をもらう
3close番号札を OS に返す
257openat場所を指して開く (open の新しい形)

x86-64 では、専用の命令は syscall で、番号を rax に、1 つ目から 3 つ目の引数を rdi、rsi、rdx に置きます。結果は rax に戻ります。write なら、rdi に書き出す先の番号、rsi に字の置き場所、rdx に送る長さを置きます。下の図で、送る長さと書き出す先を変えてみてください。

システムコールの窓口に置く値

開いていない番号の 7 を選ぶと、C の窓口は -1 を返し、何も書かれません。

ラッパー関数と代表的なシステムコール

ふつうのプログラムは、システムコールを直接呼びません。C の標準ライブラリにあるラッパー関数 (手順をくるんだ関数) を呼び、その関数が番号を置いて線を越えます。Python の os.open、os.read、os.write、os.close も、このラッパー関数を通る薄い窓口です。

分野代表的なシステムコール
ファイルopen、openat、read、write、close
プロセス (動いているプログラム)fork、execve、wait4、kill
メモリmmap、munmap、brk
ネットワークsocket、connect、accept

ファイルディスクリプタは、開いたファイルの番号札

open が成功すると、小さな 0 以上の整数が返ります。これがファイルディスクリプタ (開いたファイルを指す番号札、略して fd) で、後の read、write、close にはファイルの名前ではなくこの番号を渡します。

open(2) によると、返る番号は、そのプログラムでいま開いていない番号のうち一番小さい物です。0、1、2 は最初から標準入力、標準出力、標準エラーに使われているので、プログラムが最初に開くファイルの番号は、ふつう 3 からになります。

動かして確かめる

Python の os の 4 つの窓口を、自分の手で順に呼んでみます。ファイルは頁の中の仮の置き場に作り、最後に消します。

動かす 4 つの窓口を順に呼ぶ

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

open True
write 6
close
open True
read b'hello\n'
close

open は番号札を返し、番号は 0、1、2 より後なので fd >= 3 は True です。write は書けたバイト数の 6 (hello の 5 字と改行) を返しました。2 回目の open は読むために開き直し、read は中身のバイト列を返しました。

ふだんの open() も、裏では同じ番号札を持っています。open() の返すファイルの物から、番号札を取り出してみます。

動かす open() の中の番号札

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

int
0
3

f.fileno() は整数の番号札を返しました。os.fstat で大きさを OS に聞くと、f.write の直後は 0 バイト、f.flush() の後は 3 バイトです。f.write の時点では字は Python の手元にためてあり、flush か close のときにまとめて write に渡ります。

read は、頼んだ数より少なく返すことがあり、それは失敗ではありません。4 バイトずつ頼んでみます。

動かす 4 バイトずつ読む

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

4 b'hell'
2 b'o\n'
0 b''

6 バイトのファイルを 4 バイトずつ読むと、4 バイト、2 バイトと返り、3 回目は 0 バイトです。read(2) は、0 はファイルの終わりを示すと書いています。f.read() の 1 行も、裏では 0 が返るまで read を呼んでいます。

ここまでをまとめると、最初の 2 行は次の窓口に分かれます。

Python の書き方裏で呼ぶシステムコール
open("memo.txt")open (いまの Linux では openat)
f.read()read を 0 が返るまで
f.write(...)ためてから write
with の終わりclose

どこで崩れるのか

システムコールは失敗すると理由を返し、呼ぶ回数が増えると遅くなります。返ってきた物を見ないと、どちらも気づけません。

閉じた番号で読むと EBADF

閉じた番号札で読もうとすると、カーネルは断ります。理由は errno の名前で分かります。

動かす 閉じた番号と無いファイル

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

EBADF
Bad file descriptor
ENOENT
No such file or directory

閉じた番号で read を呼ぶと EBADF (正しく開いた番号ではない)、消したファイルを開くと ENOENT (その名前のファイルが無い) です。Python は -1 と errno を受け取り、OSError という例外に変えて知らせます。close(2) は、閉じた番号は使い回されると書いています。後で別のファイルが同じ番号を得ると、古い番号で関係の無いファイルを読み書きしてしまいます。

開いたまま閉じ忘れる

番号札には、1 つのプログラムが同時に持てる数の上限があります。open(2) の説明では、上限に届くと open は EMFILE (開いている数がプロセスの上限に届いた) で失敗します。

要求のたびに 1 枚ずつ閉じ忘れると、ある時から全部の open が失敗し、番号札を使うネットワークの接続も受けられなくなります。with を使えば、例外が出ても close が呼ばれます。

呼ぶ回数が増えると遅くなる

システムコールは線を越える分、手間がかかります。だから open() は字をためてからまとめて write に渡します。write を呼んだ回数を、小さな模型で数えます。

動かす ためる大きさと呼ぶ回数

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

1 1000
64 32
4096 1
2000

2 バイトの行を 1000 行書いています。左がためる大きさ (バイト)、右が write を呼んだ回数で、ためないと 1000 回です。64 バイトためると、64 バイトずつの 31 回と、最後に残った 16 バイトの 1 回で 32 回です。4096 バイトなら 2000 バイトは一度もあふれず、最後の 1 回だけです。最後の行のとおり、書けた量はどれも 2000 バイトです。Python の文書は、open() のためる大きさを、多くの環境で 128 KB ほどと書いています。

ためている間は OS に渡っていないので、途中でプログラムが落ちると、その分は消えます。

崩れる前提をまとめると、次の通りです。

  • 開いたら必ず使える、という前提。名前が無い、権限が無い、番号札が尽きた、で open は失敗する
  • 頼んだ分だけ返る、という前提。read と write は、頼んだより少ない数を返すことがある
  • f.write を呼べばもう書けている、という前提。ためている間は、まだ OS に渡っていない
  • 番号札は自分だけの物、という前提。閉じた番号は、別のファイルに使い回される

仕事でどう確かめるか

仕事では、プログラムが呼んだシステムコールと、その戻り値を道具で見て確かめます。

1 つ目は、Linux の strace (システムコールを横から記録する道具) です。コマンドの前に付けると、呼んだシステムコールの名前、引数、戻り値が 1 行ずつ出ます。strace(1) の説明の例は次の通りです。

strace(1) の説明の例

open("/dev/null", O_RDONLY) = 3
open("/foo/bar", O_RDONLY)
  = -1 ENOENT (No such file
  or directory)

1 行目は開けて番号札の 3 をもらった記録、2 行目は開けずに -1 と理由の ENOENT が返った記録です。open(2) によると glibc 2.26 から open のラッパー関数は openat を呼ぶので、いまの Linux では openat と出ることが多くなっています。

使い方分かること
strace -e trace=%fileファイルの名前を取る呼び出しだけを見る。どのファイルを探して、どれが無かったか
strace -cシステムコールごとの呼んだ回数、かかった時間、失敗の数をまとめる
strace -p と番号動いているプログラムに後から付いて見る

2 つ目は、OSError の errno の名前を記録に残すことです。ENOENT なら場所、EACCES なら権限、EMFILE なら閉じ忘れと、調べる先が変わります。

関わる実例

カーネルとユーザー空間の線が破られると、権限の無いアプリが、してはいけないことをできてしまいます。

Dirty COW (CVE-2016-5195、2016 年 10 月)

CVE の記録によると、Linux のカーネルの 4.8.3 より前の版に競合状態 (処理の順番しだいで結果が変わる欠陥) があり、権限の無い利用者が、コピーオンライト (書くときに初めて写しを作る仕組み) の誤りを突いて、読むだけのメモリの写しに書き込めました。2016 年 10 月には実際に悪用されていました。崩れた前提は「読むだけの物には、カーネルが書かせない」です。窓口の向こうのカーネルに欠陥があれば、権限の確かめが抜けます。

Meltdown (2018 年 1 月)

2018 年 1 月 3 日に公開された論文によると、カーネルのメモリはユーザー空間から触れない印が付いています。Meltdown は、CPU が命令を先回りして実行する仕組みの副作用で、その印を越えてカーネルのメモリを読めることを示しました。原因はハードウェアです。崩れた前提は「ユーザー空間からカーネルのメモリは読めない」で、論文は KAISER という守りが Meltdown を妨げると書いています。

よくある問い

システムコールとふつうの関数の呼び出しは何が違うのですか

ふつうの関数は、ユーザー空間の中で行って戻ります。システムコールは、専用の命令で CPU をカーネルモードに切り替え、カーネルに仕事をさせてから戻ります。書き方は同じに見えても、線を越える分の手間がかかります。

カーネル空間とユーザー空間は何が違うのですか

カーネル空間はカーネルが動く場所で、装置とすべてのメモリを直接触れます。ユーザー空間はふつうのアプリが動く場所で、装置を触るにはシステムコールでカーネルに頼みます。

Python の open と os.open は何が違うのですか

os.open は番号札の整数を返す薄い窓口で、Python の文書は低い層の入出力のための物と書いています。open() は番号札を中に持ち、字をためる、文字とバイトを変換する、といった仕事を足しています。

write システムコールの引数は何ですか

write の引数は、書き出す先の番号札、中身の置き場所、長さの 3 つです。x86-64 の Linux では rdi、rsi、rdx に置き、番号の 1 を rax に置いて syscall を実行します。戻り値は書けたバイト数です。

確認クイズ

答えだと思う選択肢を押すと、その場で正誤と理由が開きます。4 問すべてに正しく答えると、この回は修了で、石が緑に変わります (この端末のブラウザに残ります)。間違えた問いは、あとから解き直せます。

1 問目

システムコールの説明として正しいものはどれですか。

2 問目

open のシステムコールが成功したとき、返ってくるものはどれですか。

3 問目

このコードを動かすと何が出ますか。

「あ」を 1 字書く

import os
f = os.O_WRONLY | os.O_CREAT
fd = os.open("sc_q.txt", f)
b = "あ".encode()
print(os.write(fd, b))
os.close(fd)
os.remove("sc_q.txt")

4 問目

os.close(fd) の後で、同じ fd を os.read(fd, 1) に渡すと、何が起きますか。

説明課題

次の 2 行で、裏で OS が何をしているかを、自分の言葉で 4〜5 行にまとめてください。書いたら、下の模範解答を開いて比べてください。

説明する 2 行

with open("memo.txt") as f:
    data = f.read()
  • どのシステムコールが、どの順で呼ばれるか
  • それぞれに何を渡し、何が返るか
  • 失敗したら何が返るか

模範解答

自分の説明を書いてから開いて、比べてください。

この回の先の一歩

「ファイルが開けない」「接続が増えると落ちる」「書いたはずの記録が無い」といった障害は、どのシステムコールが何を返したかを見ると、原因の場所が絞れます。

もっと深く

この回の続きを、ゆめさくのコースの章や道具で深められます。頁の中の確認クイズに答えたあと、手を動かす量を増やしたいときに使ってください。

一次資料

どれも 2026年10月27日に開いて、題と中身を確かめました。

次へ

道 3 コンピュータの中の回

  1. 3-1OS の役目とシステムコールOS が間に立つ理由この回
  2. 3-2プロセスとスレッドスケジューラ、プロセス間通信頁で読む
  3. 3-3メモリの階層とキャッシュ局所性、当たりと外れ、L1 から主記憶まで頁で読む
  4. 3-4取り合いと排他レースコンディション、Mutex頁で読む
  5. 3-5CPU が命令を実行する命令、レジスタ、クロック頁で読む

道 3 の頁へ戻るほかの道を見る