errno とエラー処理の考え方
多くのシステムコールは、成功すると 0 以上の値を、失敗すると -1 を返します。「なぜ失敗したか」は戻り値そのものではなく、グローバル変数 errno に番号として格納される約束です。例えば ENOENT はファイルが無い、EACCES は権限が無い、を表します。errno は成功時に勝手に 0 へ戻されないため、戻り値で失敗を確認してから errno を読むのが鉄則です。番号を人が読める文に直すには perror や strerror を使います。エラーを毎回きちんと確認することが、堅牢な低レイヤプログラムの基本姿勢です。
システムコール(syscall)やライブラリ関数を呼ぶとき、常につきまとうのが「失敗したらどうするか」という問題です。ファイルが見つからない、権限が足りない、メモリが確保できない――こうした失敗は決して例外的な出来事ではなく、現場では日常的に起こります。Linuxのプログラミングでは、この失敗をどう検知し、その原因をどう知るかについて、明確な約束ごとが定められています。その中心にあるのがerrno(errno)という仕組みです。エラー処理をいい加減にしたプログラムは、いざ問題が起きたときに何の手がかりも残さず、原因究明を著しく難しくします。逆に、エラーを毎回きちんと確認するコードは、それだけで信頼性が大きく変わります。地味ですが、低レイヤを扱う者にとって最も大切な作法の一つです。ここでは、その正しい使い方を順に押さえていきます。
戻り値とerrnoの役割分担
多くのシステムコールやライブラリ関数は、二段構えで結果を伝えます。まず戻り値で「成功したか、失敗したか」というおおまかな結果を示します。典型的には、成功すると0以上の値(ファイルディスクリプタや、処理したバイト数など)を返し、失敗すると-1を返します。関数によっては、失敗の合図としてNULLポインタを返すものもあります。そして、失敗したときの「では、なぜ失敗したのか」という具体的な理由は、戻り値そのものではなく、グローバル変数errnoに番号として格納されます。errnoを使うには、errno.hというヘッダファイルをインクルードします。代表的な値をいくつか挙げると、ENOENTは「そのファイルやディレクトリが存在しない」、EACCESは「アクセス権がない」、EMFILEは「開いているファイルが多すぎる」、ENOMEMは「メモリが足りない」、EINTRは「処理がシグナルで中断された」を表します。これらは単なる数値に分かりやすい名前を付けた定数で、人が読んでも意味が取れるようになっています。戻り値が「成功か失敗か」、errnoが「失敗の中身」という役割分担を、まず頭に入れてください。
必ず守るべき二つの作法
errnoを扱ううえで、初学者がほぼ必ず一度ははまる落とし穴が二つあります。一つめは、errnoは失敗したときにだけ設定され、成功してもゼロには戻されないという点です。つまり、前に呼んだ別の関数が残した古いエラー番号が、そのまま居座っていることがあります。したがって、errnoの値だけを見て成否を判断してはいけません。正しい手順は必ず「まず戻り値が-1(やNULL)かどうかで失敗を確認し、失敗していると分かってからerrnoを読む」です。この順序を守らないと、本当は成功しているのに、前回の古いerrnoを拾って『失敗した』と誤判定する、という見つけにくいバグを生みます。二つめは、errnoは別の関数を呼ぶと簡単に上書きされてしまう点です。たとえば失敗を検知したあと、エラーメッセージを出力する前にprintfなどを挟むと、その関数が内部でerrnoを書き換えてしまうことがあります。そうなると、いざ表示する段には、もう本来のエラー番号は失われています。これを防ぐには、エラーが起きたらメッセージを出す前に int saved = errno; のように、いったん別の変数へ値を退避しておくのが安全です。なお、複数のスレッドが同時に動くプログラムでも値の取り違えが起きないよう、errnoはスレッドごとに別々の値を持つ仕組みになっています。
番号を人間の言葉に直す
errnoに入っているのは、あくまで番号です。そのままログに出しても「errno=2」では意味が伝わりません。これを人が読める文章に変換する関数が用意されています。perrorは、引数で渡した文字列に続けて、現在のerrnoに対応する説明文をコロンで区切って標準エラー出力へ出します。たとえばopenが失敗した直後にperror("open")と書くと、open: No such file or directoryのように、どこで何が起きたかが一行できれいに出力されます。一方strerrorは、エラー番号を受け取って、それに対応する説明の文字列を返す関数です。こちらは戻り値を自分の好きな形に組み立てたいときに便利で、fprintfと組み合わせれば、タイムスタンプ付きの独自ログ形式などに埋め込めます。これらの関数を使えば、利用者にとって意味の分かるエラー表示ができます。
エラー処理を習慣にする意味
システムコールを呼ぶたびに戻り値を確認し、失敗ならerrnoを見て適切に対処する――この一手間を省きたくなる気持ちは分かりますが、低レイヤのプログラムではこれを徹底することが、堅牢さの土台そのものになります。失敗を握りつぶしたコードは、たとえばファイルが開けていないのに読み書きを続行して、まったく無関係に見える場所で突然破綻します。そうなると原因の特定に膨大な時間を取られます。逆に、各段階できちんとエラーを捕まえていれば、問題の発生箇所がその場で特定でき、調査が一気に楽になります。とくに気をつけたいエラーにEINTRがあります。これは時間のかかるreadなどが、処理の途中でシグナルに割り込まれて中断したことを表す失敗で、本当の異常ではありません。この場合は、エラーとして投げ出すのではなく、もう一度同じシステムコールを呼び直すのが正しい対処になります。このように、エラー番号ごとに適切な対応は異なるため、一律に「失敗したら終了」とせず中身を見て判断する姿勢が大切です。各エラー番号がどんなときに設定されるかの正確な一覧は、man 2で個々のシステムコールのERRORSの節を引けば確認できます。そして、実行中のプログラムが実際にどのerrnoで失敗しているかは、straceを使えば-1のうしろにエラー名付きで観察できます。仕様はman、実地の挙動はstrace、コードの中では戻り値とerrnoの確認――この三点をそろえて初めて、エラーに強いプログラムが書けるようになります。