[インデックス 18312] ファイルの概要
このコミットは、Goランタイムにおけるパニック処理の堅牢性を向上させるためのものです。特に、「パニック中のパニック」が発生し、その際のスタックトレースの生成すら失敗した場合の挙動を改善することを目的としています。
コミット
commit c8c18614af2cc09f21458fb3a0e9281d54b508e6
Author: Keith Randall <khr@golang.org>
Date: Tue Jan 21 14:34:37 2014 -0800
runtime: if "panic during panic"'s stacktrace fails, don't recurse.
R=golang-codereviews, iant, khr, dvyukov
CC=golang-codereviews
https://golang.org/cl/54160043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c8c18614af2cc09f21458fb3a0e9281d54b508e6
元コミット内容
--- a/src/pkg/runtime/panic.c
+++ b/src/pkg/runtime/panic.c
@@ -355,19 +355,34 @@ runtime·startpanic(void)\
m->mallocing = 1; // tell rest of panic not to try to malloc
} else if(m->mcache == nil) // can happen if called from signal handler or throw
m->mcache = runtime·allocmcache();
- if(m->dying) {
+ switch(m->dying) {
+ case 0:
+ m->dying = 1;
+ if(g != nil)
+ g->writebuf = nil;
+ runtime·xadd(&runtime·panicking, 1);
+ runtime·lock(&paniclk);
+ if(runtime·debug.schedtrace > 0 || runtime·debug.scheddetail > 0)
+ runtime·schedtrace(true);
+ runtime·freezetheworld();
+ return;
+ case 1:
+ // Something failed while panicing, probably the print of the
+ // argument to panic(). Just print a stack trace and exit.
+ m->dying = 2;
runtime·printf("panic during panic\n");
runtime·dopanic(0);
- runtime·exit(3); // not reached
+ runtime·exit(3);
+ case 2:
+ // This is a genuine bug in the runtime, we couldn't even
+ // print the stack trace successfully.
+ m->dying = 3;
+ runtime·printf("stack trace unavailable\n");
+ runtime·exit(4);
+ default:
+ // Can't even print! Just exit.
+ runtime·exit(5);
}
- m->dying = 1;
- if(g != nil)
- g->writebuf = nil;
- runtime·xadd(&runtime·panicking, 1);
- runtime·lock(&paniclk);
- if(runtime·debug.schedtrace > 0 || runtime·debug.scheddetail > 0)
- runtime·schedtrace(true);
- runtime·freezetheworld();
}
void
変更の背景
Go言語のランタイムは、プログラムの異常終了時に「パニック」というメカニズムを提供します。これは、予期せぬエラーが発生した際に、プログラムを安全に停止させ、スタックトレースなどのデバッグ情報を出力するためのものです。しかし、ごく稀に、このパニック処理自体が失敗する、あるいはパニック処理中に別のパニックが発生する、という極めて深刻な状況が発生することがあります。
このような「パニック中のパニック」は、ランタイムが不安定な状態にあることを示しており、通常はスタックトレースを出力してプログラムを終了させようとします。しかし、さらにそのスタックトレースの生成処理すら失敗するような、より深刻な状況も考えられます。このコミット以前のランタイムでは、このような多重パニックの状況で無限ループに陥る可能性や、適切なエラーメッセージを出力せずに終了してしまう可能性がありました。
この変更の背景には、ランタイムの堅牢性を高め、どのような状況下でも可能な限り有用なデバッグ情報を出力し、最終的にはプログラムを確実に終了させるという設計思想があります。特に、スタックトレースの生成が失敗するような極端なケースにおいても、ランタイムがフリーズしたり、無意味な再帰を繰り返したりするのを防ぐことが重要でした。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念とC言語の知識が必要です。
- Goランタイム: Goプログラムは、Goランタイムと呼ばれるC言語とGo言語で書かれた低レベルなシステムによって実行されます。ガベージコレクション、スケジューリング、パニック処理などがランタイムの主要な機能です。
- パニック (Panic): Goにおけるパニックは、回復不能なエラーが発生した際にプログラムを異常終了させるメカニズムです。
panic
関数が呼び出されるか、ランタイムエラー(例: nilポインタ参照、配列の範囲外アクセス)が発生するとパニックが引き起こされます。パニックが発生すると、通常の実行フローは中断され、遅延関数(defer
)が実行され、最終的にスタックトレースが出力されてプログラムが終了します。 m
(M) とg
(G): Goランタイムの内部では、M(Machine)とG(Goroutine)という抽象化された概念が使われます。- M (Machine): オペレーティングシステムのスレッドを表します。Goランタイムは、複数のMを管理し、その上でGを実行します。
- G (Goroutine): Go言語の軽量な並行処理単位です。Goプログラムの関数呼び出しは、Gの上で実行されます。
m->dying
:m
構造体(M、つまりOSスレッド)のメンバーであり、そのスレッドがパニック処理中であるか、あるいはパニック処理中にさらに問題が発生しているかを示す状態変数です。この変数の値によって、ランタイムが現在のパニックの状態を判断し、適切な処理を行います。0
: 通常の状態。パニック処理中ではない。1
: 最初のパニックが発生し、その処理中。2
: 「パニック中のパニック」が発生し、その処理中。3
: 「パニック中のパニック」のスタックトレース生成すら失敗した状態。
runtime·printf
: Goランタイム内部で使用される、C言語のprintf
に似た関数で、デバッグ情報などを出力するために使われます。runtime·dopanic(0)
: パニック処理のメインロジックを呼び出す関数です。引数0
は、スタックトレースを生成することを示唆しています。runtime·exit(code)
: プログラムを終了させる関数です。引数code
は終了コードです。switch
文: C言語の制御構造の一つで、変数の値に基づいて異なるコードブロックを実行します。このコミットでは、m->dying
の値に応じてパニック処理の段階を分岐させるために使用されています。
技術的詳細
このコミットの核心は、runtime·startpanic
関数におけるm->dying
変数の利用方法の変更です。以前は、m->dying
が非ゼロであれば「パニック中のパニック」とみなし、スタックトレースを出力して終了するという単純なif
文でした。しかし、この変更により、m->dying
が多段階の状態を示すswitch
文に置き換えられ、より詳細なパニック処理の段階を管理できるようになりました。
新しいロジックは以下のようになります。
-
m->dying == 0
(最初のパニック):m->dying
を1
に設定します。これは、最初のパニック処理が開始されたことを示します。g->writebuf
をnil
に設定します。これは、ゴルーチンの書き込みバッファをクリアし、パニック処理中に余計な出力が発生しないようにするためと考えられます。runtime·xadd(&runtime·panicking, 1)
: グローバルなパニックカウンタをインクリメントします。runtime·lock(&paniclk)
: パニック処理中に他のスレッドがパニック処理に干渉しないように、パニックロックを取得します。- スケジューラートレースが有効な場合、
runtime·schedtrace(true)
を呼び出します。 runtime·freezetheworld()
: 全てのゴルーチンを停止させ、ランタイムの状態を固定します。これは、スタックトレースの生成やデバッグ情報の収集を安全に行うために不可欠なステップです。- このケースでは、関数は
return
し、通常のパニック処理フローに進みます。
-
m->dying == 1
(パニック中のパニック):- この状態は、最初のパニック処理中にさらに別のパニックが発生したことを示します。コミットメッセージによると、「おそらくパニック引数の出力中に何か失敗した」場合がこれに該当します。
m->dying
を2
に設定します。これは、二重パニックの状態であることを示します。runtime·printf("panic during panic\n")
: 「panic during panic」というメッセージを出力します。runtime·dopanic(0)
: 再度パニック処理を試みますが、この場合はスタックトレースの生成を試みるのみで、これ以上の再帰的なパニック処理の試行は行いません。runtime·exit(3)
: プログラムを終了コード3で終了させます。
-
m->dying == 2
(パニック中のパニックのスタックトレース生成失敗):- この状態は、二重パニックが発生し、さらにそのスタックトレースの生成すら失敗したという、極めて深刻な状況を示します。コミットメッセージでは、「これはランタイムの真のバグであり、スタックトレースすら正常に出力できなかった」と説明されています。
m->dying
を3
に設定します。runtime·printf("stack trace unavailable\n")
: 「stack trace unavailable」というメッセージを出力します。runtime·exit(4)
: プログラムを終了コード4で終了させます。
-
default
(それ以外のm->dying
の値):- これは、上記以外の予期せぬ
m->dying
の値、つまりランタイムの内部状態が非常に壊れていることを示します。 runtime·exit(5)
: 「出力すらできない!」という状況とみなし、プログラムを終了コード5で即座に終了させます。
- これは、上記以外の予期せぬ
この多段階のswitch
文により、ランタイムはパニックの深刻度に応じて段階的に対応を変え、無限再帰を防ぎつつ、可能な限り有用な情報を出力して終了するようになりました。これにより、デバッグが困難な極端なケースでのランタイムの挙動が改善されています。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/panic.c
ファイルのruntime·startpanic
関数内で行われています。
具体的には、以下の部分が変更されました。
- if(m->dying) {
- runtime·printf("panic during panic\n");
- runtime·dopanic(0);
- runtime·exit(3); // not reached
- }
- m->dying = 1;
- if(g != nil)
- g->writebuf = nil;
- runtime·xadd(&runtime·panicking, 1);
- runtime·lock(&paniclk);
- if(runtime·debug.schedtrace > 0 || runtime·debug.scheddetail > 0)
- runtime·schedtrace(true);
- runtime·freezetheworld();
+ switch(m->dying) {
+ case 0:
+ m->dying = 1;
+ if(g != nil)
+ g->writebuf = nil;
+ runtime·xadd(&runtime·panicking, 1);
+ runtime·lock(&paniclk);
+ if(runtime·debug.schedtrace > 0 || runtime·debug.scheddetail > 0)
+ runtime·schedtrace(true);
+ runtime·freezetheworld();
+ return;
+ case 1:
+ // Something failed while panicing, probably the print of the
+ // argument to panic(). Just print a stack trace and exit.
+ m->dying = 2;
+ runtime·printf("panic during panic\n");
+ runtime·dopanic(0);
+ runtime·exit(3);
+ case 2:
+ // This is a genuine bug in the runtime, we couldn't even
+ // print the stack trace successfully.
+ m->dying = 3;
+ runtime·printf("stack trace unavailable\n");
+ runtime·exit(4);
+ default:
+ // Can't even print! Just exit.
+ runtime·exit(5);
+ }
コアとなるコードの解説
変更前は、m->dying
が0
でない場合に「パニック中のパニック」として処理し、runtime·dopanic(0)
を呼び出して終了していました。これは、m->dying
が単に「パニック中である」という真偽値として扱われていたことを意味します。
変更後は、m->dying
がパニック処理の段階を示す状態変数として機能するように拡張されました。
case 0
: これは、runtime·startpanic
が最初に呼び出された、つまり最初のパニックが発生した通常のケースです。ここでm->dying
が1
に設定され、通常のパニック処理(freezetheworld
など)が開始されます。そしてreturn
することで、runtime·startpanic
の呼び出し元に戻り、パニック処理の残りの部分が実行されます。case 1
:m->dying
が1
の状態でruntime·startpanic
が再度呼び出された場合、これは「パニック中のパニック」が発生したことを意味します。この場合、m->dying
は2
に更新され、「panic during panic」というメッセージが出力された後、runtime·dopanic(0)
が呼び出されます。ここで重要なのは、runtime·dopanic(0)
が呼び出された後、runtime·exit(3)
でプログラムが終了することです。これにより、さらなる再帰的なパニック処理の試行が停止されます。case 2
:m->dying
が2
の状態でruntime·startpanic
が呼び出された場合、これは「パニック中のパニック」のスタックトレース生成すら失敗したことを意味します。m->dying
は3
に更新され、「stack trace unavailable」というメッセージが出力された後、runtime·exit(4)
でプログラムが終了します。default
: これまでのどのケースにも当てはまらない、予期せぬm->dying
の値の場合です。これはランタイムの深刻なバグを示唆しており、もはや何も出力できないと判断し、runtime·exit(5)
で即座にプログラムを終了させます。
この変更により、m->dying
の状態遷移が明確になり、パニック処理の堅牢性が大幅に向上しました。特に、多重パニックの状況で無限ループに陥る可能性が排除され、どのような状況でもプログラムが確実に終了し、可能な限り有用なデバッグ情報が出力されるようになりました。
関連リンク
- Go言語のパニックとリカバリーに関する公式ドキュメント(Go言語のバージョンによって内容が異なる場合がありますが、基本的な概念は共通です):
- Goランタイムのソースコード(
src/runtime
ディレクトリ):
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(特に
src/pkg/runtime/panic.c
) - Go言語のコミット履歴とコードレビューコメント
- C言語の基本的な構文と制御構造に関する知識
- オペレーティングシステムのスレッドとプロセスに関する一般的な知識
- デバッグとエラーハンドリングに関する一般的なソフトウェア工学の原則