エラー: UNIX プログラムでの errno

標準的なエラー機構に対処する

UNIX® の標準エラー・レポート機構、errno グローバル変数について、詳しく学びましょう。また、関連するグローバル変数 (sys_nerr や sys_errlist) や、エラーをユーザーに知らせるために役立つ標準関数についても学びます。

Chris Herborth (chrish@pobox.com), Freelance Writer, Author

Photo of Chris HerborthChris Herborth は、いくつかの賞を受賞したシニア・テクニカル・ライターであり、オペレーティング・システムやプログラミングに関して 10 年以上執筆活動を行っています。彼は息子の Alex と遊んだり妻の Lynette と時を過ごしたりしている以外は、ビデオ・ゲームの設計やプログラミング、そして研究(つまりゲームで遊ぶこと)を行っています。



2006年 9月 05日

はじめに

UNIX® 開発者は、適切なエラー検出やエラー回復を無視しがちです。その原因には、C 言語に例外がないことや、標準の C ライブラリーには初歩的なエラー機構しかないことがあげられます。この記事の目標は、標準 C ライブラリーによる UNIX のエラー・レポート機構に慣れ、そして (できれば) ユーザーにわかりやすい方法でエラーをレポートし、処理できるようになることです。

では、早速始めましょう。


始める前に

この記事のコード・サンプルを実際に試したい場合には、ソースコード・アーカイブをダウンロードする必要があります (自分でタイプ入力したい場合は除きます)。ここでは Eclipse で CDT (C/C++ Development Tooling) を使います。「参考文献」のセクションには、これまで Eclipse を使ったことがない人のために、Eclipse を使い始める上で役立つ資料へのリンクを用意してあります。

ここであげるコード例は、ごく簡単なものですが、Eclipse のような IDE (integrated development environment) を使うことによって、システム・ヘッダーを開いたり、特定のシンボルを見つけたり、などが容易になります。最新バージョンの Eclipse (3.2) と CDT プラグイン (2.0) には、素晴らしい、便利な機能が一杯詰まっています。


C プログラムでのエラー・レポート

C は、UNIX プラットフォームで最も一般的に使われているプログラミング言語です。UNIX では、他の言語 (Java™や C++、Python、Perl など) も一般的ですが、システムの API (application programming interface) は、すべて C 用に作られています。標準 C ライブラリーは、どのような C コンパイラー・パッケージにも入っており、POSIX (Portable Operating System Interface) や Single UNIX Specification などの UNIX 標準を構築するための基礎となっています。

1970 年代の初期に C と UNIX が開発された頃は、例外 (ある条件が発生した場合にアプリケーションのフローに割り込む) という概念は、かなり新しいものであるか、あるいは存在していませんでした。そのため、ライブラリーがエラーをレポートするためには、他の方法を使わざるを得ませんでした。

C ライブラリーの場合、あるいは他の UNIX ライブラリーの場合でも、失敗をレポートする方法として、次のような 2 つの一般的な方法があることに誰もが気付きます。

  • 関数は、エラー・コードまたは成功コードを返します。もしエラー・コードであれば、何が悪かったのかを、そのコード自体を使って判断できます。
  • 関数は特定の値 (あるいは、ある範囲の値) を返すことでエラーを示します。そして問題の原因を示すためにグローバル変数 errno が設定されます。

errno グローバル変数は、標準定数として定義される、すべての値と共に、<errno.h> システム・ヘッダーで定義されます (スレッド・セーフな C ライブラリーを持つシステムでは、errno グローバル変数は実際には各スレッドにそれぞれ独自の errno を持たせる関数、あるいはマクロなので、正確に言えばシンボルです)。

最初のカテゴリーの関数の多くは、実際には標準 errno コードのうちの 1 つを返します。しかし、その関数がどのように動作し、何を返すのかは、マニュアルのページの Returns セクションを調べない限りわかりません。運が良ければ、その関数の man ページには、戻り値として取り得る値と、この特定の関数の役割から見た各値の意味がリストアップされています。サードパーティーのライブラリーは、多くの場合は 1 つの規則しかなく、すべての関数がそれに従うものですが、何かを想定する前に、やはりそのライブラリーの資料をよく調べる必要があります。

ここで、実際の errno を示すコードと、そうしたエラー・コードを人に読みやすいものに変換するために使える関数をいくつか見てみましょう。


失敗をレポートする

リスト 1 に示す短いプログラムは、存在しないファイルを開こうとし、そのエラーを、このプログラムを実行している人に対して 2 つの方法でレポートします。

リスト 1. errno 変数が失敗を記録する
// errno for fun and profit

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

const char *FILE_NAME = "/tmp/this_file_does_not_exist.yarly";

int main( int argc, char **argv )
{
      int fd = 0;

      printf( "Opening %s...\n", FILE_NAME );	
      fd = open( FILE_NAME, O_RDONLY, 0644 );
      if( fd < 0 ) {
            // Error, as expected.
            perror( "Error opening file" );
            printf( "Error opening file: %s\n", strerror( errno ) );
      }

      return EXIT_SUCCESS;
}

このプログラムを実行すると、リスト 2 のようなものが見えるはずです。

リスト 2. リスト 1 の出力
chrish@dhcp2 [507]$ ./Debug/errnoDemo 
Opening /tmp/this_file_does_not_exist.yarly...
Error opening file: No such file or directory
Error opening file: No such file or directory

出力 (リスト 2) を見るとわかるように、perror() 関数は、渡されたストリングと、それに続いてコロン、空白、そして現在の errno の値のテキスト表現を表示します。printf() コールと strerror() 関数 (現在の errno の値のテキスト表現へのポインターを返します) を使えば、皆さんも自分でこれをシミュレーションすることができます。

この出力からは判断できない詳細の 1 つが、perror() がメッセージを標準エラー・チャネル (stderr) に書き込んでいることです。リスト 1 の printf() コールは、標準出力チャネル (stdout) に書き込んでいます。

strerror() 関数は、必ずしもスレッド・セーフではありません。未知の値に対してはエラー・メッセージを静的バッファーの中でフォーマットし、そのバッファーに対するポインターを返します。何度も strerror() を呼ぶと、strerror() はそのバッファーの内容を上書きします。

POSIX 1003.1 標準は、strerror_r() を定義しています。strerror_r() は、エラーの値の他に、バッファーへのポインターとバッファー・サイズを受け付けます。リスト 3 は、このスレッド・セーフ版の使い方を示しています。

リスト 3. スレッド・セーフな strerror_r() 関数の動作
// Thread-safe usage of strerror_r().
void thread_safe( int err )
{
    char buff[256];
    
    if( strerror_r( err, buff, 256 ) == 0 ) {
        printf( "Error: %s\n", buff );
    }
}

perror() 関数とstrerror()/strerror_r() 関数は、標準的な errno の値を処理する際に恐らく最も一般的に使われているエラー・レポート方法です。次に、エラー関連の他のグローバル変数と、POSIX-1003.1 の errno 値で定義される標準を見てみることにしましょう。


エラー・グローバル変数と標準の値

さて、グローバルな errno 変数は、標準 C ライブラリー関数によって設定されます (別の何かによって設定される場合もあります。自分が使おうとしている関数が errno を設定するのかどうか、よくマニュアルを読んでください)。それによって、エラーの種類を示します。エラーとしては、引数として渡された値が不適切であったかもしれず、その関数の実行中に失敗したのかもしれません。

標準エラー記述を使う perror() 関数と strerror() 関数は、元はグローバル変数、sys_errlist です。

標準 C ライブラリーは、エラー関連のグローバル変数として、さらに 2 つ、sys_nerr (int) と sys_errlist (char へのポインターの配列) を定義しています。前者は、sys_errlist に保存されている標準エラー・メッセージの数です。歴史的なアプリケーション (つまり恐ろしく古いレガシー・コード) は、これらを直接参照することもありますが、これらは一貫して宣言されていないため、コンパイル中にエラーが発生します。

POSIX 標準は errno の取り得る値として、非常に多くの値を定義しています。当然ながら、そうした値のすべてがどの関数にも適用できるわけではありませんが、開発者が独自の関数を書く場合には、そうした値によって選択肢が広がります。

ここで Eclipse のヒントを 1 つ。コードの中で errno を選び、次に F3 キーを押します (あるいは errno の上で右クリックしてから、コンテキスト・メニューから Open Declaration を選びます)。そうすると、Eclipse は errno.h システム・ヘッダーを開き、errno の宣言をハイライトします (図 1)。

図 1. errno の宣言を示す
errno の宣言を示す

私のタブ設定が、このファイルを書いた人のものと一致していないことの他に、標準のエラー値やそれらのシンボル名、それぞれを説明する簡単なコメントなどに皆さんは気が付くと思います。大部分のシステム・ヘッダーは、標準の errno 値に対して、少なくともこれだけの情報を含んでいます。ですから、恐れずに調べてみましょう。また、システム・ヘッダーとマニュアル・ページは、そのシステムがサポートする可能性のある非標準の値に関して、唯一の情報源でもあります。

標準の errno の値としては、次のようなものがあります。

  • E2BIG -- 関数に渡される引数リストが長すぎました。
  • EACCESS -- アクセスが拒否されました。このプログラムを実行しているユーザーは、ファイルやディレクトリーなどへのアクセス権を持っていません。
  • EAGAIN -- 要求されたリソースが一時的に利用不能です。同じ操作を後で再度行えば、成功するかもしれません。
  • EBADF -- 関数が、不適切なファイル記述子を使おうとしました (例えば、開いているファイルを参照していない、あるいは、読み取り専用として開かれているファイルに書き込むために使われた、など)。
  • EBUSY -- 要求されたリソースは利用できません。例えば、別のアプリケーションが読んでいる最中にディレクトリーを削除しようとした、など。EBUSY と EAGAIN との微妙な違いに注意してください。当然ですが、読み取りを行っているプログラムが終了した後なら、そのディレクトリーを削除することができます。
  • ECHILD -- wait() 関数、または waitpid() 関数が、子プロセスが終了するのを待とうとしましたが、すべての子プロセスは既に終了していました。
  • EDEADLK -- リクエストが継続されると、リソース・デッドロックが発生します。これは、マルチスレッドのコードで起こるようなデッドロックではないことに注意してください。errno とその仲間では、とてもそうしたデッドロックは追跡できません。
  • EDOM -- 入力引数が、その数学関数の対象外です。
  • EEXIST -- ファイルが既に存在し、それが問題です。例えば、パスを付けて mkdir() を呼ぶと、既存のファイルやディレクトリーに名前を付けることになってしまうような場合です。
  • EFAULT -- 関数の引数の 1 つが、無効なアドレスを参照しています。大部分の実装では、これは検出できません (プログラムは SIGSEGFAULT シグナルを受け取り、終了します)。
  • EFBIG -- リクエストが、実装で定義された最大のファイルサイズを超えてファイルを拡張しようとしました。これは通常は約 2 GB ですが、最近のほとんどのファイルシステムはずっと大きなファイルをサポートしており、場合によると read()/write() や lseek() などの関数の 64 ビット版が必要になります。
  • EINTR -- 関数がシグナルに割り込まれ、割り込みはプログラムのシグナル・ハンドラーによってキャッチされ、そしてシグナル・ハンドラーの戻りは正常でした。
  • EINVAL -- 関数に対して無効な引数を渡しました。
  • EIO -- I/O エラーが発生しました。これは通常、ハードウェアの問題への反応として生成されます。
  • EISDIR -- ファイル引数を要求する関数を、ディレクトリー引数を付けて呼びました。
  • ENFILE -- このプロセスで既に開かれているファイルが多すぎます。各プロセスは OPEN_MAX ファイル記述子を持っており、今ユーザーは (OPEN_MAX + 1) ファイルを開こうとしています。ファイル記述子には、ソケットのようなものもの含まれることに注意してください。
  • ENLINK -- この関数を呼ぶと、そのファイルは LINK_MAX 以上のリンクを持つことになります。
  • ENAMETOOLONG -- PATH_MAX よりも長いパス名を作ってしまいました。あるいは、NAME_MAX よりも長いファイル名やディレクトリー名を作ってしまいました。
  • ENFILE -- このシステムで同時に開いているファイルが多すぎます。これは一時的な状態のはずであり、最近のシステムでは、通常は起こらないはずです。
  • ENODEV -- そのようなデバイスはありません。あるいは、指定されたデバイスに対して不適切なことを試みようとしています (例えば、大昔のライン・プリンターから読み取ろうとしてはいけません)。
  • ENOENT -- そのようなファイルは見つかりません。あるいは、指定されたパス名が存在しません。
  • ENOEXEC -- 実行可能ではないファイルを実行しようとしました。
  • ENOLCK -- ロックが利用できません。システム全体でのファイル制限、あるいはレコード・ロックに達しました。
  • ENOMEM -- システムのメモリーが足りなくなりました。従来から、アプリケーションは (そして OS そのものも)、これをうまく扱えません。そのために、特にディスク上のスワップ空間のサイズを動的に増加できないシステムでは、使いそうな RAM 量よりも多くの RAM が必要なのです。
  • ENOSPC -- デバイス上に空間が残っていません。ユーザーが、既に一杯のデバイスに対して書き込もうとした、あるいはファイルを作成しようとしました。これも同じく、アプリケーションでも OS でもうまく扱えないエラーです。
  • ENOSYS -- システムは、この関数をサポートしません。例えば、ジョブ・コントロールを持たないシステムに対して setpgid() を呼ぶと、ENOSYS エラーが発生します。
  • ENOTDIR -- 指定されたパス名はディレクトリーである必要がありますが、そうなっていません。これは EISDIR エラーの逆です。
  • ENOTEMPTY -- 指定されたディレクトリーが空ではありませんが、空である必要があります。空のディレクトリーでも、「.」 や「..」 などが含まれていることに注意してください。
  • ENOTTY -- その操作をサポートしていないファイル、あるいは特別なファイルに対して I/O コントロール操作をしようとしました。例えば、ディレクトリーに対してボーレートを設定しようとしてはいけません。
  • ENXIO -- 存在しないデバイスの特別なファイルに対して I/O リクエストを試みました。
  • EPERM -- この操作は許可されていません。ユーザーは、指定されたリソースへのアクセス権を持っていません。
  • EPIPE -- もはや存在しないパイプから読み取ろうとしました、あるいはパイプに書き込もうとしました。パイプ・チェーンの中のプログラムの 1 つが、そのストリーム部分を閉じてしまいました (例えば終了によって)。
  • ERANGE -- 関数が呼ばれましたが、戻り値は戻り型で表現するには大きすぎます。例えば、ある関数が unsigned char の値を返したものの、結果を 256 以上 (あるいは -1 以下) と計算したとすると、errno は ERANGE に設定され、その関数は無関係な値を返します。こうした場合には、入力データが適切かどうかを調べるか、あるいは関数を呼んだら必ず errno を調べることが重要です。
  • EROFS -- 読み取り専用のファイルシステム (あるいは読み取り専用モードでマウントされたファイルシステム) に保存されたファイル、あるいはディレクトリーを変更しようとしました。
  • ESPIPE -- パイプ、あるいは FIFO (First In, First Out) に対してシークしようとしました。
  • ESRCH -- 無効なプロセス ID、あるいはプロセス・グループを指定しました。
  • EXDEV -- デバイスにまたがってリンクを移動する操作を行おうとしました。例えば、UNIX ファイルシステムでは、ファイルシステム間でファイルを移動することを許しません (ファイルをコピーし、その後でオリジナルを削除する必要があります)。

POSIX 1003.1 仕様の特徴として気に入らない点は、ノー・エラーの値がないことです。errno が 0 に設定されても、これを標準のシンボリック定数で参照できないこと以外、何も問題が起きません。私は errno.h の中に E_OK や EOK、ENOERROR を持つプラットフォーム上でプログラムしたところ、リスト 4 のようなものを含むコードになってしまいました。こんなことにならないよう、仕様の中でカバーして欲しかったと思います。

リスト 4. no error というエラー値
#if !defined( EOK )
#  define EOK 0         /* no error */
#endif

sys_nerr グローバル変数と strerror() 関数を使うと、システムに組み込まれたエラー・メッセージをすべて出力するコードを簡単に作ることができます (リスト 5)。これによって、使われているシステムがサポートし、実装で定義される (つまり非標準の) 追加の errno 値がすべてダンプされることに注意してください。POSIX 1003.1 準拠のシステムで必要なエラーは、上記にリストしたエラーのみであり、それ以外はすべて「おまけ」です。

リスト 5. エラーのすべてを見せる
// Print out all known errors on the system.
void print_errs( void )
{
    int idx = 0;
    
    for( idx = 0; idx < sys_nerr; idx++ ) {
        printf( "Error #%3d: %s\n", idx, strerror( idx ) );
    }
}

私のシステム (この記事の執筆時では Mac OS X 10.4.7) がサポートする errno 値の完全なリストを見せて皆さんを退屈させるつもりはありませんが、下記は print_errs() 関数の出力のサンプルです (リスト 6)。

リスト 6. 標準のエラー値として取り得る値は大量にあります
Error #  0: Unknown error: 0
Error #  1: Operation not permitted
Error #  2: No such file or directory
Error #  3: No such process
Error #  4: Interrupted system call
Error #  5: Input/output error
Error #  6: Device not configured
Error #  7: Argument list too long
Error #  8: Exec format error
Error #  9: Bad file descriptor
Error # 10: No child processes
   
Error # 93: Attribute not found
Error # 94: Bad message
Error # 95: EMULTIHOP (Reserved)
Error # 96: No message available on STREAM
Error # 97: ENOLINK (Reserved)
Error # 98: No STREAM resources
Error # 99: Not a STREAM
Error #100: Protocol error
Error #101: STREAM ioctl timeout
Error #102: Operation not supported on socket

これは大量のエラーです。幸い、大部分の関数は、レポートすべきエラーは数個しか持っていません。そのため、そうしたエラーを適切に処理することは、通常は難しいことではありません。


エラーに対応する

プログラムにエラー処理コードを追加するのは面倒で手間がかかり、しかも時間もかかります。整然としたコードが乱れ、考えられるエラーすべてに対してハンドラーを追加する作業で他の開発作業が止まってしまいます。そのため開発者は、こうした作業を嫌います。

しかしこの作業は、自分のためだけではありません。それは、実際にそのプログラムを使う人のためでもあるのです。もし何かが失敗すれば、プログラムのユーザーは失敗の原因を知る必要があり、もっと重要なこととして、どうしたら問題を修正できるかを知る必要があるのです。

この重要な部分を、開発者は見逃しがちです。ユーザーに対して単純に「ファイルが見つかりませんでした」と言うよりは、「SuperWidget コンフィギュレーション・ファイルを見つけることができませんでした」と伝え、そして見つからないファイルを選択するオプションを与え (例えばファイル選択ウィジェットのようなものを提供する、など)、見つからないファイルを検索し (例えばファイルがありそうな場所をプログラムで探す、など)、あるいは、デフォルト・データが入った新しいバージョンのファイルを作成する、といったことをした方が親切です。

確かに、そうしたことをすると、コードの流れが妨げられます。しかし、エラー処理やエラー回復が良くできていれば、そのアプリケーションはユーザーから高い評価を得られるのです。そして、他の開発者はエラー処理に関心を持たないことが多いため、皆さんのアプリケーションは、簡単に他の誰のものより優れたものになるのです。


まとめ

UNIX では、標準のエラー・レポート機構は最小限にとどめられています。しかしそれは、アプリケーションにランタイム・エラーが起きた場合にクラッシュで処理したり、何が起きたのかをユーザーに伝えずに終了したりしても構わない理由にはなりません。

標準の C ライブラリーと POSIX 1003.1 は、標準的なエラー値として取り得る数多くの値や、エラーをレポートし、人が読み取れる形にエラーを翻訳するためのいくつかの手軽な関数を定義しています。しかし、そうしたものだけでは十分ではありません。開発者は、ユーザーに対して何が起きているかを伝え、問題の修復方法や回避策を伝える努力をもっとすべきなのです。


ダウンロード

内容ファイル名サイズ
Demoau-errnoDemo.zip21KB

参考文献

学ぶために

  • C では使えないエラー処理構成体、例外を説明した記事として、Wikipedia の「例外処理 (Exception handling)」を読んでください。例外は、他の言語 (C++ や Objective-C、Python など) であれば、UNIX システムでもサポートされています。
  • Eclipse プラットフォームへの入門として、「What is Eclipse, and how do I use it?」(developerWorks、2001年11月) を読んでください。
  • Eclipse Platformを使用したデバッグ」(developerWorks、2003年5月) を読み、Eclipse Platform に組み込まれたデバッグ機能の使い方について学んでください。
  • Get started now with Eclipse には、Eclipse に関係する、あらゆるものについての情報やリンクが用意されています。
  • Eclipse Platformを使用したC/C++ 開発」(developerWorks、2003年4月) を読み、C/C++ 開発プロジェクトに Eclipse Platform を使うための概要を学んでください。
  • developerWorks の AIX and UNIX ゾーンを訪れ、皆さんの UNIX スキルを磨いてください。
  • New to AIX and UNIX ページを訪れ、AIX と UNIX についてさらに詳しく学んでください。
  • developerWorks の Open source ゾーンを訪れてください。オープンソース技術を使った開発や、IBM 製品でオープンソース技術を使用するためのハウ・ツー情報やツール、プロジェクトの更新情報など、豊富な情報が用意されています。
  • developerWorks technical events and webcasts で最新情報を入手してください。
  • AIX 5L Wiki は、AIX に関する技術情報のためのコラボレーション環境です。
  • Podcasts を利用して、IBM の技術エキスパートに追いついてください。

製品や技術を入手するために

  • 誰もが愛するオープンソースの IDE、Eclipse のホームページ、Eclipse.org を訪れてください。
  • Eclipse で C/C++ をサポートするために、Eclipse C/C++ Development Tooling -- CDT を訪れてください。
  • 皆さんの次期開発プロジェクトを、IBM trial software を使って構築してください。developerWorks から直接ダウンロードすることができます。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=AIX and UNIX, Open source
ArticleID=271564
ArticleTitle=エラー: UNIX プログラムでの errno
publish-date=09052006