主要なシステムコール(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番になります。これは、プロセスが開始した時点で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で閉じます。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の誤りなのかを切り分けられ、調査の強力な武器になります。