主要なシステムコール(open/read/write/close)
ファイル操作の土台は open・read・write・close の4つのシステムコールです。open はファイルを開き、成功すると「ファイルディスクリプタ」と呼ばれる小さな整数(0以上)を返します。以後の read や write はこの番号を使って対象を指定し、最後に close で後始末をします。標準入力(0)・標準出力(1)・標準エラー出力(2)はプログラム開始時から開かれている特別なディスクリプタです。read と write は実際に処理したバイト数を返し、要求した分より少ないこともある点が重要です。失敗時は -1 を返し、原因は errno に格納されます。
Linuxでファイルを扱うプログラムを書くとき、覚えるべきシステムコールはたった四つでいい。open・read・write・closeという四つのシステムコール(syscall)だ。
日本語にすれば「開く・読む・書く・閉じる」で、紙のノートを開いて読み書きし、最後に閉じる、という日常の動作にそのまま対応する。
面白いのは、Linuxではこの四つが通常のファイルだけでなく、デバイスやパイプ、ソケットなど「ファイルとして見えるもの」全般に共通して使える点だ。
つまりこの四つを理解することは、Linuxの入出力という仕組み全体の土台を理解することに直結する。
ここでは、これらがどんな約束ごとで動くのかを順に見ていく。仕様を正確に確認したいときは、man 2 openのようにマニュアルのセクション2(システムコール)を引くのが最も確実だ。
🔢 開いたファイルに付く「背番号」
openは、対象のファイルを開いて読み書きの準備を整えるシステムコールだ。成功すると、そのファイルを指し示すための小さな非負整数を返す。
これがファイルディスクリプタ(fd)で、いわば「いま開いているファイルにつけられた背番号」だ。以後のreadやwriteは、長いファイル名ではなく、この手短な番号を使って対象を指定する。
番号は、使われていない一番小さいものから順に割り当てられる。そのため最初に開いたファイルが多くの場合3番になる。
なぜ3番からなのか。プロセスが開始した時点で0番(標準入力)・1番(標準出力)・2番(標準エラー出力)があらかじめ開かれて予約されているためだ。
openの第一引数にはパス名、第二引数には開き方を表すフラグを渡す。
フラグはO_RDONLY(読み込み専用)・O_WRONLY(書き込み専用)・O_RDWR(読み書き両用)のいずれか一つを必ず指定し、必要に応じてO_CREAT(なければ新規作成)やO_TRUNC(中身を空にする)、O_APPEND(末尾に追記)などを縦棒で組み合わせる。
たとえばopen("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0644)と書けば、ログファイルがなければ作り、あれば末尾に追記する形で開ける。
新規作成を伴うときだけ、第三引数に作成するファイルの権限(この例なら0644)を渡す。
📏 read と write は頼んだ分だけ働くとは限らない
readはfdが指すファイルからデータを読み込み、writeは逆に書き込む。
どちらも操作対象のfd、データを置く(あるいは取り出す)バッファ、そして扱いたいバイト数を引数に取り、実際に処理できたバイト数を戻り値として返す。
man 2 readで宣言を引くと ssize_t read(int fd, void *buf, size_t count); という形が確認できる。
ここに、初学者がとてもつまずきやすい大切な性質がある。要求したバイト数より少ない数しか処理されないことがあるのだ。
たとえば1000バイトの書き込みを頼んでも、戻り値が600であれば、まだ400バイトが書き残されている。
だから堅牢なプログラムでは、戻り値を見て処理済みのバイト数を足し込み、残りがゼロになるまでwriteを繰り返すのが定石だ。
readの戻り値が0のときは特別な意味を持つ。「これ以上読むデータがない」、つまりファイルの終端(EOF)に達したことを表す。
読み込みループは、この0を「もう終わり」の合図として回し、最後に読み込んだ内容の末尾へ終端文字を付けて文字列として扱う、という書き方がよく使われる。
📍 「次に読み書きする場所」を覚えているもの
openからcloseまでのあいだ、ファイルには「次にどこから読み書きするか」を示すオフセットという内部的な位置が保たれている。
最初はファイルの先頭(0)を指していて、readやwriteを呼ぶたびに、処理したバイト数だけ自動で前へ進む。
だから何も特別なことをしなくても、繰り返し読めば先頭から順に最後まで読める。
この位置を好きなところへ動かしたいときは、lseekというシステムコールを使う。
lseekには起点を選ぶ引数があり、SEEK_SET(先頭から)・SEEK_CUR(現在位置から)・SEEK_END(末尾から)を指定できる。
たとえばファイルの途中だけを読み出したい、ヘッダを読み飛ばして本体から処理したい、といった場合に、lseekで狙った位置へ飛んでからreadする。
この「順番に進むオフセット」という考え方を知っておくと、ファイルの一部分だけを扱うプログラムを書くときに迷わない。
🧹 閉じ忘れると静かに溜まっていくもの
使い終わったファイルはcloseで閉じる。closeはファイルディスクリプタを解放し、カーネル側で確保していた資源を返す後始末の役割を担う。
開いたまま閉じ忘れると、未使用のfdが少しずつ溜まっていき、やがて新たなopenが「開きすぎ」を意味するEMFILEエラーで失敗する。
これをファイルディスクリプタリークと呼び、長時間動き続けるサーバ系のプログラムでとりわけ深刻な問題になる。
一つのプロセスが同時に開けるfdの数には上限があり、シェルのulimit -nで確認できる。「開いたら必ず閉じる」を習慣にしてほしい。
なお、これら四つのシステムコールはいずれも、失敗すると戻り値として-1を返し、失敗の具体的な理由をグローバル変数errno(errno)に格納する。
たとえば指定したファイルが存在しなければENOENT、権限が足りなければEACCESが入る。
「成功なら0以上、失敗なら-1、理由はerrnoを見る」という約束は、ファイル入出力に限らずシステムコール全般に共通する基本作法だ。
🔎 書いたコードと実際の呼び出しを照らし合わせる
open・read・write・closeは、設定ファイルの読み込み、ログの書き出し、デバイスファイル経由でのハードウェア制御など、低レイヤのあらゆる場面で顔を出す。
これらを直接使うコードを書いたら、gccでコンパイルして動かし、期待どおりに読み書きできているかをstraceで確かめると理解が一気に深まる。
straceの出力には、openat("設定ファイル", O_RDONLY) = 3 や write(1, "...", 14) = 14 のように、呼び出しと戻り値が一行ずつ並ぶ。
自分のプログラムが内部でどんな順序でこれらを呼んでいるかを目で追えば、書いたコードと、実際に発行されるシステムコールとの対応がはっきり結びつく。
とくに「読めているはずなのに中身が空」「書いたはずなのにファイルが変わらない」といった不具合のときに効く。
straceで戻り値とerrnoを確認すれば、原因がopenの失敗なのかreadの誤りなのかを切り分けられ、調査の強力な武器になる。
つまずいたら、まずstraceでその四つの呼び出しを一行ずつ追ってみる。