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

errno とエラー処理の考え方

多くのシステムコールは、成功すると 0 以上の値を、失敗すると -1 を返します。「なぜ失敗したか」は戻り値そのものではなく、グローバル変数 errno に番号として格納される約束です。例えば ENOENT はファイルが無い、EACCES は権限が無い、を表します。errno は成功時に勝手に 0 へ戻されないため、戻り値で失敗を確認してから errno を読むのが鉄則です。番号を人が読める文に直すには perror や strerror を使います。エラーを毎回きちんと確認することが、堅牢な低レイヤプログラムの基本姿勢です。

システムコール(syscall)やライブラリ関数を呼ぶとき、いつも頭の片隅に置いておくべき問いがある。「もし失敗したら、どうするのか」だ。

ファイルが見つからない、権限が足りない、メモリが確保できない。こうした失敗は決して例外的な出来事ではなく、現場では日常的に起こる。

Linuxのプログラミングでは、この失敗をどう検知し、その原因をどう知るかについて、明確な約束ごとが定められている。その中心にあるのがerrno(errno)という仕組みだ。

💡
ポイント失敗の検知と原因把握には明確な約束ごとがある。その中心にあるのがerrnoという仕組みだ。

エラー処理をいい加減にしたプログラムは、いざ問題が起きたときに何の手がかりも残さず、原因究明を著しく難しくする。逆に、エラーを毎回きちんと確認するコードは、それだけで信頼性が大きく変わる。

地味だが、低レイヤを扱う者にとって最も大切な作法の一つだ。ここでは、その正しい使い方を順に押さえていく。

🔀 「失敗したか」と「なぜ失敗したか」は別々に伝わる

多くのシステムコールやライブラリ関数は、二段構えで結果を伝える。

まず戻り値で「成功したか、失敗したか」というおおまかな結果を示す。典型的には、成功すると0以上の値(ファイルディスクリプタや、処理したバイト数など)を返し、失敗すると-1を返す。関数によっては、失敗の合図としてNULLポインタを返すものもある。

そして、失敗したときの「では、なぜ失敗したのか」という具体的な理由は、戻り値そのものではなく、グローバル変数errnoに番号として格納される。errnoを使うには、errno.hというヘッダファイルをインクルードする。

システムコールを呼ぶ戻り値成功=0以上 / 失敗=-1errno失敗の中身(理由の番号)
🔗
たとえ戻り値は「成功か失敗か」の合図灯、errnoは「失敗の中身」を書いたメモ。役割が分かれている。

代表的な値をいくつか挙げる。ENOENTは「そのファイルやディレクトリが存在しない」、EACCESは「アクセス権がない」、EMFILEは「開いているファイルが多すぎる」、ENOMEMは「メモリが足りない」、EINTRは「処理がシグナルで中断された」を表す。

ENOENT(ファイルやディレクトリが無い)・EACCES(アクセス権がない)・EMFILE(開きすぎ)・ENOMEM(メモリ不足)・EINTR(シグナルで中断)。

これらは単なる数値に分かりやすい名前を付けた定数で、人が読んでも意味が取れるようになっている。

戻り値が「成功か失敗か」、errnoが「失敗の中身」という役割分担を、まず頭に入れてほしい。

🕳 errno の二つの落とし穴

errnoを扱ううえで、初学者がほぼ必ず一度ははまる落とし穴が二つある。

一つめは、errnoは失敗したときにだけ設定され、成功してもゼロには戻されないという点だ。つまり、前に呼んだ別の関数が残した古いエラー番号が、そのまま居座っていることがある。

つまずきerrnoは失敗時だけ設定され、成功してもゼロに戻らない。errnoの値だけで成否を判断してはいけない。まず戻り値で失敗を確認してからerrnoを読む。

したがって、errnoの値だけを見て成否を判断してはいけない。正しい手順は、必ず「まず戻り値が-1(やNULL)かどうかで失敗を確認し、失敗していると分かってからerrnoを読む」だ。

errno を読む正しい順序(1) 戻り値を見る-1 や NULL か?(2) 失敗と分かったらerrno を読む(3) すぐ退避int saved = errno;順序を守らないと、古いerrnoを拾って誤判定する

この順序を守らないと、本当は成功しているのに、前回の古いerrnoを拾って『失敗した』と誤判定する、という見つけにくいバグを生む。

二つめの落とし穴は、errnoは別の関数を呼ぶと簡単に上書きされてしまう点だ。

たとえば失敗を検知したあと、エラーメッセージを出力する前にprintfなどを挟むと、その関数が内部でerrnoを書き換えてしまうことがある。そうなると、いざ表示する段には、もう本来のエラー番号は失われている。

コツerrnoは別の関数呼び出しで簡単に上書きされる。エラーを検知したらメッセージを出す前に int saved = errno; のように別変数へ退避しておく。

これを防ぐには、エラーが起きたらメッセージを出す前に int saved = errno; のように、いったん別の変数へ値を退避しておくのが安全だ。

なお、複数のスレッドが同時に動くプログラムでも値の取り違えが起きないよう、errnoはスレッドごとに別々の値を持つ仕組みになっている。

🗣 ただの番号を人の言葉に直す

errnoに入っているのは、あくまで番号だ。そのままログに出しても「errno=2」では意味が伝わらない。これを人が読める文章に変換する関数が用意されている。

perrorは、引数で渡した文字列に続けて、現在のerrnoに対応する説明文をコロンで区切って標準エラー出力へ出す。

perror("open") ……openが失敗した直後に書くと、open: No such file or directory のように一行できれいに出力される。

たとえばopenが失敗した直後にperror("open")と書くと、open: No such file or directoryのように、どこで何が起きたかが一行できれいに出力される。

もう一つ、自分で表示を組み立てたいときはstrerrorを使う。strerrorは、エラー番号を受け取って、それに対応する説明の文字列を返す関数だ。

こちらは戻り値を自分の好きな形に組み立てたいときに便利で、fprintfと組み合わせれば、タイムスタンプ付きの独自ログ形式などに埋め込める。

これらの関数を使えば、利用者にとって意味の分かるエラー表示ができる。

🧱 毎回エラーを確認することが堅牢さの土台になる

システムコールを呼ぶたびに戻り値を確認し、失敗ならerrnoを見て適切に対処する。この一手間を省きたくなる気持ちは分かる。

つまずきエラーを握りつぶすと、ファイルが開けていないのに読み書きを続行し、無関係に見える場所で突然破綻する。原因特定に膨大な時間を取られる。

だが省くと、失敗を握りつぶしたコードは、たとえばファイルが開けていないのに読み書きを続行して、まったく無関係に見える場所で突然破綻する。そうなると原因の特定に膨大な時間を取られる。

逆に、各段階できちんとエラーを捕まえていれば、問題の発生箇所がその場で特定でき、調査が一気に楽になる。

とくに気をつけたいエラーにEINTRがある。これは、時間のかかるreadなどが、処理の途中でシグナルに割り込まれて中断したことを表す失敗で、本当の異常ではない。

コツEINTR(シグナルで中断)は本当の異常ではない。エラーとして投げ出さず、もう一度同じシステムコールを呼び直すのが正しい対処になる。

この場合は、エラーとして投げ出すのではなく、もう一度同じシステムコールを呼び直すのが正しい対処になる。

このように、エラー番号ごとに適切な対応は異なるため、一律に「失敗したら終了」とせず中身を見て判断する姿勢が大切だ。

各エラー番号がどんなときに設定されるかの正確な一覧は、man 2で個々のシステムコールのERRORSの節を引けば確認できる。

そして、実行中のプログラムが実際にどのerrnoで失敗しているかは、straceを使えば-1のうしろにエラー名付きで観察できる。

💡
ポイント仕様はman、実地の挙動はstrace、コードの中では戻り値とerrnoの確認。この三点をそろえて初めて、エラーに強いプログラムが書ける。

仕様はman、実地の挙動はstrace、コードの中では戻り値とerrnoの確認。この三点をそろえて初めて、エラーに強いプログラムが書けるようになる。

この項目に出てくる用語

errnoえらーなんばー
直近のシステムコール失敗の原因を表す番号。
システムコールしすてむこーる
アプリがカーネルに処理を依頼する公式の窓口。

関連コマンド

man 3 printfstracegcc

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