ERR04-C. プログラムの適切な終了方法を選択する
範囲外エラーなど、いくつかのエラーはユーザの誤入力が原因で生じる場合がある。通常、対話型プログラムはこのようなエラーに対処するため、入力を拒否し、受け入れ可能な値の入力をユーザに促す。サーバはクライアントにエラーを示すことにより無効なユーザ入力を拒否し、同時にそれ例外のクライアントの有効な要求に対するサービスは続行する。揮発性ストレージに保存されたユーザデータの損失を防止することにより、少なくとも、メモリ残量低下(ディスクスペースの状態)などのリソースの不足に適切に対処するには、堅牢なプログラムを準備する必要がある。対話型プログラムを使用すると、ユーザはデータを代替メディアに保存でき、ネットワークサーバはスループットを落としたりサービス品質を低下させたりして対応できる。ただし、不確実な状態での実行を続行することによるリスクデータの破壊ではなく、回復不能なロジックエラーなどのエラーが検出された場合の適切な対処法は、すばやくシステムのシャットダウンを行い、オペレータが確実な状態で再起動できるようにすることである。
ISO/IEC TR 24772:2013のセクション6.39、「Termination Strategy [REU]」[ISO/IEC TR 24772:2013]には次のように記載されている。
障害が検出された場合にシステムが取り得る対処方法は何通りもある。最もてっとりばやいかつ代表的な方法は、機能を完全に停止させる方法であり、これはフェイルファーストまたはフェイルストップとも呼ばれる。これは、検出された障害に対して即座にシステムを停止するという対応である。別の対処方法として、機能の一部を停止させる方法がある(フェイルソフト方式)。システムは障害のある状態で動作し続けるが、システムの性能は低下する可能性がある。電話交換局や電子商取引、その他の「常時利用可能な」アプリケーションなど、高可用性環境で使われているシステムは、フェイルソフト方式を採用することが多い。しかし、実際にどのように動作するかは、対象のシステムが安全性重視であるかセキュリティ重視であるかによって異なる。飛行機の操縦装置、交通信号、医療監視システムのようなフェイルセーフシステムであれば、正常稼動に必要な要件を満たすための対応ではなく、障害による被害や危険をいかに食い止めるかという対応がとられるだろう。また、暗号化システムのような最上級のセキュリティが要求されるシステムでは、障害が検出されたらサービスを停止することで、セキュリティを最大限に維持しようとするだろう。
また、以下のようにも記載されている。
システムの障害への対処方法は、障害が起きている箇所の重要度により異なる。プログラムが複数のタスクで構成されているときは、重要度の高いタスクとそうでないタスクがあるだろう。重要度の高いタスクは、プログラムのほかの部分から再起動できるものとできないものがあると考えられる。障害を検出したタスクは、以下のいずれかを行えるべきだろう。 ・他のタスクが自分のリソースを使える状態にしたまま停止する ・リソースを完全に解放して停止する ・プログラム全体を停止させる。タスクが終了するまでにかけられる時間や、ほかのタスクが終了シグナルを無視してよいかどうかは明確に定めるべきである。障害に対する対処に一貫性がない場合、脆弱性につながる可能性がある。
Cは、exit()
、main()
からの復帰、_Exit()
、および abort()
など、プログラムの終了に関していくつかの選択肢を提供している。
exit()
exit()
を呼び出すとプログラムの正常終了が行われる。main()
からの復帰以外では、exit()
を呼び出すのがプログラムを終了させるための一般的な方法である。この関数は int
型の引数を1つ持つ。この引数は、成功終了を意味する EXIT_SUCCESS
、または失敗終了を意味する EXIT_FAILURE
でなければならない。EXIT_SUCCESS
の値は0となる。C言語仕様のセクション7.22.4.4 [ISO/IEC 9899:2011] には、「status の値が0または EXIT_SUCCESS
の場合、成功終了(successful termination)状態を処理系定義の形式で返す」と記載されている。exit()
関数は、呼び出し元に復帰しない。
#include <stdlib.h> /* ... */ if (/* 実際に何か障害が起きた */) { exit(EXIT_FAILURE); }
exit()
の呼び出しは
- 書き出されていないバッファリングされたデータをフラッシュする。
- すべてのオープンしているストリームをクローズする。
- 一時ファイルを削除する。
- 終了状態を示す整数をオペレーティングシステムに返す。
Cの標準関数 atexit()
を使い、プログラムの終了時に追加の処理を実行するために exit()
をカスタマイズできる。
たとえば、
atexit(turn_gizmo_off);
上記の呼び出しは turn_gizmo_off()
関数を登録し、その後 exit()
が呼び出されると、プログラムの終了時に turn_gizmo_off()
が呼び出される。Cは、atexit()
関数が少なくとも32個の関数を登録できることを要求している。
atexit()
関数で登録した関数は exit()
によって、または main()
の正常終了時に呼び出される。
atexit
ハンドラから exit()
を呼び出すプログラムの動作は定義されていない(C言語仕様の付属書Jに記載されている未定義の動作182を参照。「ENV32-C. atexit で登録したハンドラ関数は必ず return する」も参照)。
main()
からの復帰
main()
からの復帰ではプログラムの正常終了が行われる。これはプログラムの望ましい終了方法である。return
文を評価することは、同じ引数を使用して exit()
を呼び出すのと同じ効果を持つ。
int main(int argc, char **argv) { /* ... */ if (/* 実際に何か障害が起きた */) { return EXIT_FAILURE; } /* ... */ return EXIT_SUCCESS; }
C言語仕様のセクション5.1.2.2.3 [ISO/IEC 9899:2011]は、main()
からの復帰について次のように述べている。
main
関数の返却値の型がint
と適合する場合、main
関数の最初の呼び出しからの復帰は、main
関数が返す値を実引数として持つexit
関数の呼び出しと等価とする。main
関数を終了する}
に到達した場合、main
関数は値0を返す。main
関数の返却値の型がint
と適合しない場合、ホスト環境に戻される終了状態は、未規定とする。
したがって、main()
からの復帰は、exit()
を呼び出すことと同じである。多くのコンパイラは、この動作を以下のような方法で実装している。
void _start(void) { /* ... */ exit(main(argc, argv)); }
しかし、main
の最後にexit関数が実行されるのは、main
関数が途中で強制終了されずにきちんと最後まで実行された場合である(「ERR00-C. エラー処理には一貫性のある方針を採用する」および「ERR05-C. アプリケーション非依存なコードではエラー検知のみ行ない、エラー処理は行わない」を参照)。
_Exit()
_Exit()
を呼び出すとプログラムの正常終了が行われる。exit()
関数と同様、_Exit()
も int
型の1つの引数を持ち、復帰はしない。ただし、exit()
と異なり、_Exit()
はオープンしているストリームをクローズするかどうか、ストリームバッファをフラッシュするかどうか、[1] 一時ファイルを削除するかどうかは、処理系定義である。atexit()
によって登録された関数は実行されない。
[1] 関数でのストリームバッファのフラッシュを禁止することにより、 POSIXは _Exit()
の仕様を強化している。The Open Group Base Specifications Issue 7, IEEE Std 1003.1, 2008 Edition [IEEE Std 1003.1-2008] のこの関数のマニュアル
を参照。
#include <stdlib.h> /* ... */ if (/* 実際に何か障害が起きた */) { _Exit(EXIT_FAILURE); }
_exit()
関数は _Exit()
の別名である。
abort()
abort()
関数を呼び出すと、プログラムを異常終了させることができる。ただし、シグナル SIGABRT
が捕捉されていて、かつシグナルハンドラによって exit()
または _Exit()
が呼び出される場合を除く。
#include <stdlib.h> /* ... */ if (/* 実際に何か障害が起きた */) { abort(); }
_Exit()
の場合と同様に、バッファにデータが残っているオープンストリームをフラッシュするかどうか、[2] オープンしているストリームをクローズするかどうか、一時ファイルを削除するかどうかは、処理系定義である。atexit()
によって登録された関数は実行されない。(「ERR06-C. assert() と abort() の終了動作を理解する」を参照)。
[2] _Exit()
の場合と異なり、POSIXは処理系でのストリームバッファのフラッシュを明示的に許可しているが、必須とはしていない。The Open Group Base Specifications Issue 7, IEEE Std 1003.1, 2008 Edition [IEEE Std 1003.1-2008] のこの関数のマニュアル
を参照。
まとめ
次の表に、プログラム終了関数の終了動作をまとめる。
関数 |
オープンしている |
ストリーム |
一時 |
|
プログラム |
---|---|---|---|---|---|
|
(info) |
(info) [2] |
(info) |
(error) |
異常 |
|
(info) |
(info) [1] |
(info) |
(error) |
正常 |
|
(tick) |
(tick) |
(tick) |
(tick) |
正常 |
|
(tick) |
(tick) |
(tick) |
(tick) |
正常 |
表の凡例:
- (tick) – 正常。指定のアクションを実行する。
- (error) – 異常。指定のアクションを実行しない。
- (info) – 処理系定義。指定のアクションを実行するかどうかは処理系によって決まる。
違反コード
プログラム終了前にアプリケーション固有の後処理を実行することが重要な場合、abort()
関数を呼び出さないこと。以下のコード例では、データがオープンファイル記述子に送信されたあとで abort()
が呼び出されている。データは、ファイルに書き出される場合と書き出されない場合がある。
#include <stdlib.h> #include <stdio.h> int write_data(void) { const char *filename = "hello.txt"; FILE *f = fopen(filename, "w"); if (f == NULL) { /* エラー処理 */ } fprintf(f, "Hello, World\n"); /* ... */ abort(); /* データは書き出されない可能性がある */ /* ... */ return 0; } int main(void) { write_data(); return 0; }
適合コード
以下の解決法では、abort()
の呼び出しを exit()
で置き換え、バッファリングされた入出力データをファイル記述子へフラッシュし、ファイル記述子を適切にクローズしている。
#include <stdlib.h> #include <stdio.h> int write_data(void) { const char *filename = "hello.txt"; FILE *f = fopen(filename, "w"); if (f == NULL) { /* エラー処理 */ } fprintf(f, "Hello, World\n"); /* ... */ exit(EXIT_FAILURE); /* データを書き出し、f をクローズ */ /* ... */ return 0; } int main(void) { write_data(); return 0; }
この例は、abort()
よりも exit()
を呼び出したほうがよい例であるが、abort()
のほうが好ましい状況もある。たとえばプログラムを終了する速度が重要な場合で、ファイル記述子のクローズや atexit()
で登録された処理ルーチンの呼び出しが必要ないときなどである。
abort()
の適切な使用方法の詳細については、「ERR06-C. assert() と abort() の終了動作を理解する」を参照のこと。
リスク評価
たとえば、exit()
の代わりに abort()
または _Exit()
を使用すると、書き出されたファイルが一貫性のない状態のまま残ったり、機密性の高い一時ファイルがファイルシステム上に残される可能性がある。
レコメンデーション |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
---|---|---|---|---|---|
ERR04-C |
中 |
中 |
高 |
P4 |
L3 |
関連するガイドライン
参考資料
7.20.4, "Communication with the Environment"
翻訳元
これは以下のページを翻訳したものです。
ERR04-C. Choose an appropriate termination strategy (revision 67)