このエントリーをはてなブックマークに追加
スポンサーリンク

エラー処理をしよう

エラーについて

エラーはコンピューターの痛み
人間、ケガをすると「痛い」と感じます。細菌に侵食され化膿すると、その間中ずっと痛みが続きます。内臓のどこかが痛んでいる場合、お腹のどこかが痛くなります。人間は痛みを感じている間はその箇所をいたわり、薬を飲んだり体を休めたりします。もし人間が痛みを感じなければ、痛んだ箇所を放置し、なおも酷使しつづけ、やがて臓器は壊れていくでしょう。

コンピューターには痛みがありません。故障箇所があっても、そこが完全に機能を停止するまでずっと動き続けます。ですから、コンピューターの痛みは人間が事前に察知してあげる必要があります。

コンピューターは、ディスクの読み取りや、通信に失敗すると「エラー」を出します。エラーを無視して動作し続けるプログラムは、人間が痛みを我慢してなお仕事を続ける状態と同じく、最後には症状を悪化させ死に至る事になります。幸い人間には自己修復機能があり、痛みに耐える事でより強靭な状態で再生する事が可能ですが、コンピューターは自己再生ができません。コンピューターにとっての破損は部品交換でしか回復はできません。ですので、部品交換のタイミングは人間が察してあげなければなりません。

ディスクエラー
ハードディスクは磁気でデーターを記録するメディアです。(近い将来別の手法ができるかもしれませんが)磁性体は次第に劣化しデーターを記録できなくなります。その際に、ディスドライバーは「エラー」を発します。プログラマーは「エラー」を受け取ったら「エラーが発生した」という旨の表示をし、プログラムを停止させるようなプログラムを作ります。

桁あふれ
2,147,483,647は32ビット系の多くのコンパイラで、int型で表すことのできる最大値です。ほとんどはintの桁あふれは気にしなくても良いのですが、稀に21億以上の数値を扱うケースがあります。

例えば、元々は普通の卸業のために作られた販売管理ソフトを、多少のカスタマイズを加えた上でゼネコンに販売したとします。その際に、卸業で使っている時には気にしなくて済んでいた21億という限界値が、ゼネコンなどの最大値が高額になる業種で扱う際に問題になってきます。が、卸業用からゼネコン用にカスタマイズしている時には、その点に気づきにくいです。そもそも21億という数値をオーバーする事自体稀ですから、その限界値はしばしば忘れられてしまうからです。

請求には締め日がありますから、不具合が発覚してからプログラムを作り直していたのでは間に合わない事がありますし、手計算で行うにはあまりにも桁が多すぎてヘタな電卓でもオーバーフローしてしまうでしょう。

このように、桁あふれというものはめったに起こらないからこそ、プログラムで対応してないケースが多く、発生した時の影響は大きくなります。

ネガティブになろう

C系の言語(C、C++、PHP、Perl)は、基本的にif文の連続です。もしも…、もしも…、もしも…、もしも…、もしも…………、この幾百、幾千もの「もしも」の集合体、これがプログラムなのかもしれません。しかし、もしも もしも もしもと、常に色々な障害の可能性(できれば最悪の事態)を常に想定する事こそ、プログラミングにとって最も大切な事なのです。

夢のクレヨン王国というアニメにおいて、主人公シルバー王女には悪い癖がいくつもあります。その中に、「疑い癖」というのがあります。何事においても人を信用できず、すぐに疑ってかかるという癖です。

しかし、癖というのは全てが「悪い」と決めることはできません。プログラムにおいては、ありとあらゆる事を疑わないといけないのです。中島みゆきの歌で「何につけても一応は絶望的観測をするのが癖です 」という歌がありました。しかし、絶望的観測は必要です。いや、むしろプログラムにおいては絶望的な観測が必要になる場面がいくつも出てきます。

筆者はよく他人から「ネガティブだ」とか「マイナス思考だ」と言われます。野球chにおいては「今日はもう負けだ」「ああ、もうこれは逆転されるわ」「ああ、もう今年は優勝は無理だわ」などと書き込んでは「じゃ見るな」とか「ネガ厨氏ね」などと言われたりします。このページをご覧になっている方で、普段から「マイナス思考だ」「ネガティブだ」と言われていて、それを気にしている方もいるかもしれません。

しかし、努力次第によっては欠点は長所になり得るのです。ネガティブな思考はむしろプログラマーの適正があると言えます。ここで、ネガティブな思想をむしろ己の長所にしてしまいましょう。

ファイル入出力

ファイル入出力のエラー処理の大切さ
先にディスクエラーについて解説しましたように、ファイルのopenでエラーが発生する場合で気をつけなければならないのは、ディスクエラーの可能性があるという事です。

大抵はパーミッション違反だったり、ファイル自体が存在してなかったりする場合がほとんどですが、ディスクエラーには最も注意しなければなりません。なぜなら、ディスクエラーが発生した場合の対処は早期発見&早期交換しかないからです。そうでないと、大切なデーターを全て失ってしまうという最悪の事態にもなりかねません。

Perlのopen
open (FILE, "> $file") || die("Cannot open.");
は、Perlでファイルをオープンする命令です。 ここで、最後の || dieって何でしょう? || は、or(または)という意味です。この場合、|| の左の部分が真ならば、右側は実行されません。つまり、ファイルのオープンに成功した場合は、戻り値が真となるため、右側は実行されないわけです。ファイルのオープンに失敗した場合のみ die()という関数が実行されます。

dieは「メッセージを標準エラーに出力してスクリプトを終了させよ」という意味です。標準エラー出力に対してprintしてexit()する場合との違いは、dieを使うとエラーの行番号とスクリプト名を付加してくれるという点です。また、dieという文字(=死)からして、この処理がエラー処理である事がわかりやすくなります。これにより、ファイルのオープンに失敗したら、Cannot open in 行番号 スクリプト名 というエラーが表示されて、スクリプトが終了します。

PHPのfopen
PHPでファイルを開くために、
$handle = fopen($file, "r");
という命令がよく使われます。ここで、ファイルのオープンに失敗すると画面にワーニングが表示されます。ところが、これはあくまでワーニングであるため、プログラムは続行されます。その後に、
while (!feof($handle)) {
  $buff.=fgets($handle, 4096);
}
fclose($handle);
というコードがあたとします。(本当はfgetsにもエラー処理が欲しい所ですが。)ここで、!feofは、ファイルがEOFに達しているかどうか判断するためのものですが、ファイルがEOFに達していない場合だけでなく、feofという関数自体が失敗した場合もfalseを返すため、オープンされてないファイルハンドルを指定すると、ずっとfalseのまま無限ループになります。

これを防ぐために、ファイルを開く時にfopenを単独で書かずにfopen命令をif文で囲ってあげます。
if (!($handle=fopen($file, "r"))){
  die("file cannot open");
}
PHPでは、dieはエラーの行番号やスクリプト名を表示したりはしませんで、exit("コメント")と同じ意味になりますが、dieという言葉を使うことで、エラー処理をしているという事が見てわかりやすくなります。

Cのエラー処理
次に、LinuxでCでファイルのエラー処理を作っていきましょう。以下のサンプル1を見てください。

サンプル1)ファイルリード
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

FILE *fp;
char *filename;
char *open_mode;
int ret;
char *buff;

int main() {

        /*バッファ確保*/
        if (NULL == ( buff=malloc(256))) {
                fprintf(stderr,"Out of memory\n");
                return -1;
        }
        bzero(buff,256);

        /*ファイルオープン*/
        filename="test.dat";
        open_mode="r";
        if (NULL == (fp=fopen(filename,open_mode))) {
                fprintf(stderr,"File cannot open.\n");
                return -1;
        }

        /*EOFに達するまでループ*/
        while (1) {

                /*1行ファイルリード*/
                if (NULL == (fgets(buff,256,fp))) {

                        /*ファイルがEOF*/
                        if (feof(fp)) {
                                break;
                        }

                        /*ファイルリードエラー*/
                        fprintf(stderr,"File read error.\n");
                        return -1;

                }

                printf("%s",buff);
        }

        /*ファイルクローズ*/
        if (EOF == (ret=fclose(fp))) {
                fprintf(stderr,"File cannot close.\n");
                return -1;
        }

        return 0;
}
ファイルの入出力はすべてif文で囲む
ファイルのオープンに失敗する場合、まずは普通にファイルがないパーミッションが不正などが考えられます。しかし、あまり考えたくない事ですが、ファイルを読もうとして、ハードディスクが「ガリガリガリガリ…」という嫌な音をたてディスクの読み込み自体が失敗している場合があります。

あまり考えたくない事というよりは、思考から完全に除外したいケースです。もしサーバー機でこんな事になってしまったら、業務は止まり大損害が発生し、数多くの謝罪と始末書を書くハメになります。しかし、現実では必ず起こりえます。ここでは、心を鬼・・じゃなかった、心をネガティブにして「もしそうなったら・・」と考えます。
if (NULL == (fp=fopen(filename,open_mode))) {
    fprintf(stderr,"File cannot open.\n");
    return -1;
}
fopenはファイルを開く関数です。ファイルのオープンに成功すれば、fpにFILE型構造体へのポインタが返ります。もし、NULLが返った場合はエラーです。エラーメッセージを表示させて、プログラムを終了する必要があります。

ファイルの終わりを判別
/*1行ファイルリード*/
if (NULL == (fgets(buff,256,fp))) {

    /*ファイルがEOF*/
    if (feof(fp)) {
        break;
    }

    /*ファイルリードエラー*/
    fprintf(stderr,"File read error.\n");
    return -1;

}
fgets関数は、ファイルを読み込もうとしてエラーが発生するとNULLを返します。また、CではPHPとは違いファイルが終わりかどうかを、readやgetする前に判別する事ができません。読み込もうとして終わりだった場合でないと、eofフラグがセットされないためです。

掲示板や普通のWEBページなど、さほどエラーが発生しても金銭的損害の発生しないプログラムでは、NULLをファイルの終わりと判断し、そこでプログラムを正常終了させてしまっても問題はありません。しかし、業務ソフトではファイルの終わり以外でNULLコードが返ってきた場合も想定しなければなりません。前述の通りハードディスクのエラーだった場合だけでなく、途中で他のプロセスがファイルを消してしまったり、パーミッションを変えてしまったり、ファイルをロックしてたままデッドロックしている事があるかもしれません。

もちろん、大勢で同時に使うプログラム出ない限りそんな事はめったに起こりませんから、「ここではそんな事はあり得ない」と思うのが普通だと思いますが、起こらないから奇跡って言うんです あり得ない事が起こるのが、コンピューターというものです。いつ、いかなる場合においても、エラーが発生したときは「エラーである」ことを使用者にわからせねばなりません。

という事で、ここでは、ファイルの読み込みに失敗した時に、feofでファイルの終わりかどうかを判別し、eofだった場合にはじめて正常終了するようにしています。それ以外はエラーとして以上終了しています。もし、あり得ないエラーが起こった場合、他のプロセスが偶然同じ名前のファイルを使ってたり、ハードディスクに異常があったりと、色々な可能性を調べることで、データーの損失といった致命的なトラブルを事前に防ぐ事ができます。

ファイルを閉じる
/*ファイルクローズ*/
if (EOF == (ret=fclose(fp))) {
    fprintf(stderr,"File cannot close.\n");
    return -1;
}
ファイルを閉じる時にエラーが出る事はほとんどありません。あるとすれば、別のプロセスがファイルをロックしたり、消してしまったりした場合でしょうか。まったく可能性がゼロでない以上、エラー処理は加える習慣をつけましょう。

桁あふれの処理

エラー処理で困るのが桁あふれです。桁あふれは明確なエラーとならず、マイナスの数になってしまったり、一周してしまう場合があるからです。一周してしまう・・・・つまり、例えば符号なし1バイトで値を記録している場合なら、255に1を足すと0になり、さらに1を足すと2になってしまう事です。

昔、ワールドゴルフ2というゲームがありました。このゲーム(※FM-77AV版)では、スコアを大たたきして128オーバーになると127アンダーになるという裏技がありました。つまり、桁がオーバーフローして+128となるはずが、−127になってしまったわけです。ワールドゴルフ2自体があまり有名ではないので大した問題にはならなかったのですが、もしもドラクエでお金を65535円以上ためたら0円になってしまっていたら、大騒ぎだったでしょう。

サンプル2)C言語による桁あふれ処理
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

FILE *fp;
char *filename;
char *open_mode;
int ret;
char *buff;
int cnt;
char *str1;
int len;
int shurui;

char *apple_buff;
char *orange_buff;
char *grape_buff;

char *apple_c;
char *orange_c;
char *grape_c;

int apple;
int orange;
int grape;

int apple_total;
int orange_total;
int grape_total;

char *src;
char *month;

int main() {

        /*行読み込みバッファ確保*/
        if (NULL == ( buff=malloc(257))) {
                fprintf(stderr,"Out of memory\n");
                return -1;
        }
        bzero(buff,257);

        /*個別読み込みバッファ確保*/
        if (NULL == ( month=malloc(257))) {
                fprintf(stderr,"Out of memory\n");
                return -1;
        }
        bzero(month,257);
        if (NULL == ( apple_buff=malloc(257))) {
                fprintf(stderr,"Out of memory\n");
                return -1;
        }
        bzero(apple_buff,257);
        if (NULL == ( orange_buff=malloc(257))) {
                fprintf(stderr,"Out of memory\n");
                return -1;
        }
        bzero(orange_buff,257);
        if (NULL == ( grape_buff=malloc(257))) {
                fprintf(stderr,"Out of memory\n");
                return -1;
        }
        bzero(grape_buff,257);

        /*表示バッファ確保*/
        if (NULL == ( apple_c=malloc(15))) {
                fprintf(stderr,"Out of memory\n");
                return -1;
        }
        bzero(apple_c,15);
        if (NULL == ( orange_c=malloc(15))) {
                fprintf(stderr,"Out of memory\n");
                return -1;
        }
        bzero(orange_c,15);
        if (NULL == ( grape_c=malloc(15))) {
                fprintf(stderr,"Out of memory\n");
                return -1;
        }
        bzero(grape_c,15);

        /*ファイルオープン*/
        filename="test.dat";
        open_mode="r";
        if (NULL == (fp=fopen(filename,open_mode))) {
                fprintf(stderr,"File cannot open.\n");
                return -1;
        }

        /*合計クリア*/
        apple_total=0;
        orange_total=0;
        grape_total=0;

        /*ヘッダー表示*/
        printf("      orange apple grape\n");

        /*EOFに達するまでループ*/
        while (1) {

                /*1行ファイルリード*/
                if (NULL == (fgets(buff,256,fp))) {
                        /*ファイルがEOF*/
                        if (feof(fp)) {
                                break;
                        }
                        /*ファイルリードエラー*/
                        fprintf(stderr,"File read error.\n");
                        return -1;
                }

                /*最後が改行コードだったら消す*/
                if (strlen(buff)>0) {
                        if (*(buff+strlen(buff)-1)==0x0a) {
                                *(buff+strlen(buff)-1)=0;
                        }
                }

                /*空行は飛ばす*/
                if (strlen(buff)==0){
                        continue;
                }

                /*buffをカンマで区切る*/
                len=0;
                shurui=0;
                src=buff;
                for (cnt=0;cnt<256;cnt++){

                        if (*(src+len)==0||*(src+len)==0x2c) {

                                switch (shurui) {
                                        case 0:
                                                bcopy(src,month,len);
                                                break;
                                        case 1:
                                                bcopy(src,apple_buff,len);
                                                break;
                                        case 2:
                                                bcopy(src,orange_buff,len);
                                                break;
                                        case 3:
                                                bcopy(src,grape_buff,len);
                                                break;
                                }
                                src=src+len+1;
                                shurui++;
                                len=0;
                        } else {
                                len++;
                        }

                        if (shurui>3) {
                                break;
                        }

                }

                apple=atoi(apple_buff);
                orange=atoi(orange_buff);
                grape=atoi(grape_buff);

                if (apple>99999) {
                        strcpy(apple_c, " *****");
                } else {
                        sprintf(apple_c,"%6d",apple);
                }

                if (orange>99999) {
                        strcpy(orange_c, " *****");
                } else {
                        sprintf(orange_c,"%6d",orange);
                }

                if (grape>99999) {
                        strcpy(grape_c, " *****");
                } else {
                        sprintf(grape_c,"%6d",grape);
                }

                printf("%s   %s%s%s\n",month,apple_c,orange_c,grape_c);

                /*合計計算*/
                apple_total+=apple;
                orange_total+=orange;
                grape_total+=grape;

                /*バッファクリアー*/
                bzero(buff,256);
                bzero(month,256);
                bzero(apple_buff,256);
                bzero(orange_buff,256);
                bzero(grape_buff,256);
                bzero(apple_c,14);
                bzero(orange_c,14);
                bzero(grape_c,14);

        }

        /*ファイルクローズ*/
        if (EOF == (ret=fclose(fp))) {
                fprintf(stderr,"File cannot close.\n");
                return -1;
        }

        bzero(apple_c,14);
        bzero(orange_c,14);
        bzero(grape_c,14);

        if (apple_total>99999) {
                strcpy(apple_c, " *****");
        } else {
                sprintf(apple_c,"%6d",apple_total);
        }

        if (orange_total>99999) {
                strcpy(orange_c, " *****");
        } else {
                sprintf(orange_c,"%6d",orange_total);
        }

        if (grape_total>99999) {
                strcpy(grape_c, " *****");
        } else {
                sprintf(grape_c,"%6d",grape_total);
        }

        printf("------------------------\n");
        printf("total %s%s%s\n",apple_c,orange_c,grape_c);

        return 0;
}
test.dat
Jun,100,234,3212
Feb,100,23,1333
Mar,120,32,123456
これは、CSVファイルで格納された、リンゴ、みかん、ブドウの出荷数を一覧表示し、最後に合計を出すプログラムです。実際には、桁数はもっと多く必要かもしれませんし、3桁ごとにカンマで区切る必要があるかもしれませんが、サンプルという事でご容赦ください。

表示可能桁数を決める
表示用に文字列に変換するためのバッファは15バイト確保してあります。かなり多めですが、それでも桁あふれする可能性はあります。Cのsprintfは、PerlやPHPのように可変長の文字列を返すのではなく、あらかじめmallocで確保しておいた領域か配列のポインターをセットし、そこに文字列がセットされるようになっています。

mallocで確保した以上にsprintfしてしまうと、プログラムはcoreを吐くかセグメンテーションフォルトのエラーが出てしまいます。MS-DOSの時代ではセグメント違反をすると大抵暴走してしまいリセットをかけなければならなくなりました。

そのため、sprintfする前に、結果の桁があふれないかどうかチェックしましょう。
if (apple>99999) {
     strcpy(apple_c, " *****");
} else {
     sprintf(apple_c,"%6d",apple);
}
は、リンゴの各数量を調べて、5桁以上ならばsprintfせずに、*****を表示しています。同様に、合計値も5桁を越える場合は*****を表示しています。

実際には以下のようになります。
orange apple grape
Jun 100 234 3212
Feb 100 23 1333
Mar 120 32 *****
------------------------
total 320 289 *****
int型の桁あふれに対応
int型は前述の通り約21億まで計算できるため、大抵は桁あふれを気にする必要がありません。この例でいくと、リンゴやみかんの単価が21億なんてまずありえないと思います。(輸入業者が輸送にかかるコストを計算、なんていう場合は話が別ですが)

しかし、ここでもっと想定する必要があるのは、不正データーの混入です。このサンプルでは、CSVファイルからデーターを取り込むわけですが、データーが破損していて不正なデーターが混入している事も考えられます。したがって、上のサンプルでは処理をしてませんが、atoiで文字列を数値に変換する前に、文字列がすべて数字かどうか調べた方がいいでしょう。

また、カンマ(,)が来たらデーターの区切りという事にしていますが、もしバッファの最後までカンマが来なかったら、という事も考えて、バッファを257バイトと、実際に読み込む最大値よりも1バイト多めに確保しています。もし、256バイトまで読んで、すべて数字で詰まっている場合は257バイト目は必ず0(bzeroで257バイトクリアしているため)なので、ここで読み込みが終了します。

ここでは、CSVファイルの場合だけを考えましたが、不正なデーターが混入するのは、こればかりではありません。手入力をするにしても、担当者が眠くてキーボードに手を置きっぱなしにして、オートリピートしてしまったら。数量、1111111111111111111111111というとんもない数値が入力されるかもしれません。

コンピューターは、1111111111111111111111という、人間が誰が見ても「ヘン」な数でも、真面目に数値として受け入れてしまうため、1111111111111111111111を足して桁あふれを起こしてしまいます。このようなデーターが不正なデーターかどうかは、プログラム側で判別する必要があるのです。

ログを残す

技術者が24時間365日コンピューターの前に張り付いているわけにはいきません。人間は寝なければならないし、休日も必要です。なので、エラーが発生する場合は大抵は技術者が見てない場合です。

こうなった場合に備えて「いつ」「どの装置が」「どういう理由で」エラーになったのかを、遡って検証できる状態にしておくと良いと思います。先の例ではエラーである事を伝える表示だけでしたが、これに発生した日時を入れると、さらに良いでしょう。

エラーは「ありえない」や「あってはいけない」のではなく、あった場合にどうするかを想定して処理をします。万一エラーが起こった場合どうするかを常に想定し、サーバー破損などの最悪の事態が最悪の結果を招かないように事前に準備をしておきましょう

動かないコンピュータ
「日経コンピュータ」という雑誌に「動かないコンピュータ」というコラムがありました(今でも続いてるかも)。これは、コンピューターがバグ等によって永遠に不具合が解消されずに、永遠に稼動しない状態をドキュメンタリー形式に綴ったコラムです。

これらのコラムを読んでいて一番思った事は、不具合に対して「あってはならない」とか「ありえない」で片付けてしまう事が多い点です。そのような事を言っているうちは永遠に問題は解決しません。不具合は、あってはならないのではなく、あるんです。問題は不具合が発生した時に、いつ、どの関数が、どんな値が入って、どういうエラーが発生したのかを後からトレースできるようにする事です。

PC内にログを残す
サーバーにしても端末にしても、業務で稼動しているソフトではエラーが発生した際に、いつどこでどんなエラーが出たかを記録することにします。また、このログとその時作業をしていたオペレーターの話を総合する事で、エラーの原因を特定しやすくなります。これにより、稼動した直後は不具合が連続していたシステムも、次第に安定するようになります。つまり永遠に動かないコンピューターとはならないわけです。

メールを飛ばす
ハードディスクの破損によるトラブルの場合、いくらPCにログを残していてもそのログが消失してしまう可能性があります。そこで、重大なエラーが発生した場合に、メールを飛ばすようにするのも手です。PHPではerror_logというエラーの原因をメールで飛ばす関数が用意されています。また、set_error_handlerという割り込みをかける関数も用意されています。

PHP以外では残念ながらエラーが出たらメールを飛ばす処理は自作しなければなりませんが、Linuxベースのシステムではsendmailコマンドを呼び出す事で、さほど難しくなくメールを送信する事ができます。

メールを飛ばすにしても、ログを残すにしても、注意しなければならないのは無限ループにならないようにする事です。無限ループ内でエラーが発生した場合や、エラー処理内でさらにエラーが発生した場合など、気をつけないと無限ループになりがちです。その場合、ログのせいでハードディスクがパンクしたり、異常な数のメールが送信されたりします。ログに書き込んだりメールを送信したりした後は、プログラムを強制終了させるなどして、ログの処理が無限ループに陥らないようにします。
スポンサーリンク