Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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文に置き換えられ、より詳細なパニック処理の段階を管理できるようになりました。

新しいロジックは以下のようになります。

  1. m->dying == 0 (最初のパニック):

    • m->dying1に設定します。これは、最初のパニック処理が開始されたことを示します。
    • g->writebufnilに設定します。これは、ゴルーチンの書き込みバッファをクリアし、パニック処理中に余計な出力が発生しないようにするためと考えられます。
    • runtime·xadd(&runtime·panicking, 1): グローバルなパニックカウンタをインクリメントします。
    • runtime·lock(&paniclk): パニック処理中に他のスレッドがパニック処理に干渉しないように、パニックロックを取得します。
    • スケジューラートレースが有効な場合、runtime·schedtrace(true)を呼び出します。
    • runtime·freezetheworld(): 全てのゴルーチンを停止させ、ランタイムの状態を固定します。これは、スタックトレースの生成やデバッグ情報の収集を安全に行うために不可欠なステップです。
    • このケースでは、関数はreturnし、通常のパニック処理フローに進みます。
  2. m->dying == 1 (パニック中のパニック):

    • この状態は、最初のパニック処理中にさらに別のパニックが発生したことを示します。コミットメッセージによると、「おそらくパニック引数の出力中に何か失敗した」場合がこれに該当します。
    • m->dying2に設定します。これは、二重パニックの状態であることを示します。
    • runtime·printf("panic during panic\n"): 「panic during panic」というメッセージを出力します。
    • runtime·dopanic(0): 再度パニック処理を試みますが、この場合はスタックトレースの生成を試みるのみで、これ以上の再帰的なパニック処理の試行は行いません。
    • runtime·exit(3): プログラムを終了コード3で終了させます。
  3. m->dying == 2 (パニック中のパニックのスタックトレース生成失敗):

    • この状態は、二重パニックが発生し、さらにそのスタックトレースの生成すら失敗したという、極めて深刻な状況を示します。コミットメッセージでは、「これはランタイムの真のバグであり、スタックトレースすら正常に出力できなかった」と説明されています。
    • m->dying3に設定します。
    • runtime·printf("stack trace unavailable\n"): 「stack trace unavailable」というメッセージを出力します。
    • runtime·exit(4): プログラムを終了コード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->dying0でない場合に「パニック中のパニック」として処理し、runtime·dopanic(0)を呼び出して終了していました。これは、m->dyingが単に「パニック中である」という真偽値として扱われていたことを意味します。

変更後は、m->dyingがパニック処理の段階を示す状態変数として機能するように拡張されました。

  • case 0: これは、runtime·startpanicが最初に呼び出された、つまり最初のパニックが発生した通常のケースです。ここでm->dying1に設定され、通常のパニック処理(freezetheworldなど)が開始されます。そしてreturnすることで、runtime·startpanicの呼び出し元に戻り、パニック処理の残りの部分が実行されます。
  • case 1: m->dying1の状態でruntime·startpanicが再度呼び出された場合、これは「パニック中のパニック」が発生したことを意味します。この場合、m->dying2に更新され、「panic during panic」というメッセージが出力された後、runtime·dopanic(0)が呼び出されます。ここで重要なのは、runtime·dopanic(0)が呼び出された後、runtime·exit(3)でプログラムが終了することです。これにより、さらなる再帰的なパニック処理の試行が停止されます。
  • case 2: m->dying2の状態でruntime·startpanicが呼び出された場合、これは「パニック中のパニック」のスタックトレース生成すら失敗したことを意味します。m->dying3に更新され、「stack trace unavailable」というメッセージが出力された後、runtime·exit(4)でプログラムが終了します。
  • default: これまでのどのケースにも当てはまらない、予期せぬm->dyingの値の場合です。これはランタイムの深刻なバグを示唆しており、もはや何も出力できないと判断し、runtime·exit(5)で即座にプログラムを終了させます。

この変更により、m->dyingの状態遷移が明確になり、パニック処理の堅牢性が大幅に向上しました。特に、多重パニックの状況で無限ループに陥る可能性が排除され、どのような状況でもプログラムが確実に終了し、可能な限り有用なデバッグ情報が出力されるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(特にsrc/pkg/runtime/panic.c
  • Go言語のコミット履歴とコードレビューコメント
  • C言語の基本的な構文と制御構造に関する知識
  • オペレーティングシステムのスレッドとプロセスに関する一般的な知識
  • デバッグとエラーハンドリングに関する一般的なソフトウェア工学の原則