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

[インデックス 18857] ファイルの概要

このコミットは、Goランタイムにおけるクラッシュ時のパニック処理を強化することを目的としています。特にSetPanicOnCrashが有効な場合や、mallocgcにおけるnilポインタ参照のような状況で、ランタイムがパニックを起こす条件をより厳密に定義しています。これにより、デッドロック、ハング、または不明瞭なクラッシュにつながる可能性のある、有用でないパニックの発生を防ぎます。

コミット

commit 156962872575382697a0487030cd5777312d6d0c
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Mar 13 13:25:59 2014 +0400

    runtime: harden conditions when runtime panics on crash
    This is especially important for SetPanicOnCrash,
    but also useful for e.g. nil deref in mallocgc.
    Panics on such crashes can't lead to anything useful,
    only to deadlocks, hangs and obscure crashes.
    This is a copy of broken but already LGTMed
    https://golang.org/cl/68540043/
    
    TBR=rsc
    R=rsc
    CC=golang-codereviews
    https://golang.org/cl/75320043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/156962872575382697a0487030cd5777312d6d0c

元コミット内容

このコミットの元々の内容は、Goランタイムがクラッシュ時にパニックを発生させる条件をより厳しくするというものです。特にSetPanicOnCrashが設定されている場合や、メモリ割り当て(mallocgc)中のnilポインタ参照のような状況で、ランタイムがパニックを起こすことが、デッドロックやハング、あるいは原因不明のクラッシュを引き起こす可能性があるため、これを避けるための変更です。この変更は、以前に承認されたものの問題があったhttps://golang.org/cl/68540043/のコピーであり、その修正版としてhttps://golang.org/cl/75320043が提案されました。

変更の背景

Goランタイムは、プログラムの異常終了(クラッシュ)を検知した際に、通常はプロセスを終了させます。しかし、SetPanicOnCrashのような設定が有効になっている場合、クラッシュを検知した際にGoのパニック機構を介してエラーを報告しようと試みることがあります。

このコミットの背景には、以下のような問題意識があります。

  1. デッドロックやハングの回避: クラッシュが発生している状況は、ランタイムの内部状態が既に破損している可能性が高いです。このような状況で無理にパニックを発生させようとすると、パニック処理自体がさらに別のエラーを引き起こしたり、ランタイムのロックが適切に解放されずにデッドロックやハング状態に陥ったりするリスクがあります。
  2. 不明瞭なクラッシュの防止: パニック処理が失敗することで、本来のクラッシュ原因とは異なる、より理解しにくいクラッシュが発生する可能性があります。これはデバッグを非常に困難にします。
  3. mallocgcにおけるnil参照: ガベージコレクタ(GC)やメモリ割り当て処理(mallocgc)中にnilポインタ参照のような致命的なエラーが発生した場合、その時点でランタイムは非常に不安定な状態にあります。このような状況でパニックを試みることは、さらなる問題を引き起こすだけで、有用な情報を提供しないと判断されました。

したがって、このコミットは、ランタイムが「安全に」パニックを発生させることができる条件を厳密にすることで、システムの安定性とデバッグのしやすさを向上させることを目指しています。つまり、パニックを発生させることがかえって有害であると判断される状況では、パニックではなく直接プロセスを終了させるべきであるという思想に基づいています。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念とC言語の知識が必要です。

  1. Goroutine (G), Machine (M), Processor (P):

    • G (Goroutine): Goの軽量スレッドです。Goプログラムの実行単位であり、Go関数が実行されるコンテキストです。
    • M (Machine): OSのスレッド(カーネルスレッド)を表します。GoランタイムはMをOSに要求し、その上でGを実行します。
    • P (Processor): MがGを実行するために必要な論理プロセッサです。PはMに割り当てられ、Gの実行に必要なリソース(スケジューラキューなど)を提供します。
    • m->curg: 現在Mが実行しているGoroutine(G)を指します。
    • m->g0: 各Mに紐付けられた特別なGoroutineで、ランタイムコード(C言語で書かれた部分)を実行するために使用されます。Goコードは実行しません。
  2. シグナルハンドリング:

    • OSは、プログラムに異常が発生した場合(例: セグメンテーション違反、不正なメモリアクセス)にシグナルを送信します。
    • Goランタイムは、これらのシグナルを捕捉し、適切に処理するためのシグナルハンドラを設定しています。
    • SigPanicフラグ: シグナルテーブル(runtime·sigtab)内のフラグで、特定のシグナルがGoのパニックを引き起こすべきかどうかを示します。
    • SI_USER: シグナルがユーザープロセスによって送信されたことを示すコードです。この場合、通常はパニックを引き起こしません。
  3. Goランタイムの状態変数:

    • m->locks: ランタイムがロックを保持しているかどうかを示すカウンタです。0でない場合、ランタイムはロックを保持しており、デッドロックのリスクがあります。
    • m->mallocing: ランタイムがメモリ割り当て中であるかどうかを示すフラグです。メモリ割り当て中にクラッシュした場合、ヒープの状態が不安定である可能性があります。
    • m->throwing: ランタイムがパニック処理中であるかどうかを示すフラグです。既にパニック処理中であれば、再帰的なパニックは避けるべきです。
    • m->gcing: ランタイムがガベージコレクション中であるかどうかを示すフラグです。GC中にクラッシュした場合、GCの状態が不安定である可能性があります。
    • m->dying: ランタイムが終了処理中であるかどうかを示すフラグです。
    • gp->status: Goroutineの現在の状態(例: GrunningはGoroutineが実行中であることを示す)。
    • gp->syscallsp: Goroutineがシステムコール中であるかどうかを示すスタックポインタです。システムコール中にクラッシュした場合、OSのコンテキストにいるため、Goのパニック処理を安全に行えない可能性があります。
    • m->libcallsp (Windows固有): 外部ライブラリ呼び出し中であるかどうかを示すスタックポインタです。
  4. runtime·throw: Goランタイム内部で、回復不可能なエラーが発生した際に呼び出される関数です。通常、プログラムを終了させます。

  5. C言語のポインタと構造体: Goランタイムの低レベル部分はC言語で書かれており、ポインタや構造体(G, M, Siginfoなど)を多用します。

技術的詳細

このコミットの主要な変更点は、runtime·canpanicという新しい関数の導入と、既存のシグナルハンドラにおけるその利用です。

runtime·canpanic関数の導入

src/pkg/runtime/panic.cruntime·canpanic(G *gp)関数が追加されました。この関数は、与えられたGoroutine gpがクラッシュ時にパニックを発生させることが安全であるかどうかを判断します。

この関数は以下の条件をチェックします。

  1. gp == nil || gp != m->curg:
    • gpがnilである場合、またはgpが現在のMが実行しているGoroutine(m->curg)ではない場合、パニックは安全ではありません。これは、パニック処理が特定のGoroutineのコンテキストに依存するためです。特に、m->g0(ランタイムGoroutine)上で発生したクラッシュは、Goコードのコンテキストではないため、パニックを発生させるべきではありません。
  2. m->locks != 0 || m->mallocing != 0 || m->throwing != 0 || m->gcing != 0 || m->dying != 0:
    • ランタイムがロックを保持している、メモリ割り当て中である、既にパニック処理中である、ガベージコレクション中である、または終了処理中である場合、パニックは安全ではありません。これらの状態はランタイムの内部状態が不安定であるか、デッドロックのリスクがあることを示します。
  3. gp->status != Grunning || gp->syscallsp != 0:
    • Goroutine gpGrunning状態ではない場合、またはシステムコール中である場合、パニックは安全ではありません。GrunningでないGoroutineは実行コンテキストが不完全である可能性があり、システムコール中のGoroutineはOSのコンテキストにいるため、Goのパニック処理を安全に実行できません。
  4. m->libcallsp != 0 (Windows固有):
    • Windowsの場合、外部ライブラリ呼び出し中である場合もパニックは安全ではありません。これは、外部ライブラリのコードがGoランタイムの制御外で実行されており、そのコンテキストでパニック処理を行うことが困難であるためです。

これらの条件のいずれかが真であれば、runtime·canpanicfalseを返し、パニックは安全ではないと判断されます。

シグナルハンドラでの利用

src/pkg/runtime/signal_386.c, src/pkg/runtime/signal_amd64x.c, src/pkg/runtime/signal_arm.cの各シグナルハンドラ(runtime·sighandler)において、パニックを発生させるかどうかの条件が変更されました。

変更前は、シグナルがユーザープロセスからのものでなく、かつSigPanicフラグが立っている場合に、gp == nil || gp == m->g0という条件でパニックを避けていました。これは、Goroutineが存在しないか、ランタイムGoroutine(g0)上でクラッシュが発生した場合に、直接runtime·throw(プログラム終了)にジャンプするという意味です。

変更後は、この条件が!runtime·canpanic(gp)に置き換えられました。これにより、パニックを発生させるべきでない状況がより広範に、かつ厳密にチェックされるようになりました。つまり、runtime·canpanicfalseを返す(パニックが安全でない)場合にのみ、goto Throw(プログラム終了)が実行されます。

runtime.hの変更

src/pkg/runtime/runtime.hruntime·canpanic関数のプロトタイプ宣言が追加されました。

bool    runtime·canpanic(G*);

これは、runtime·canpanic関数が他のファイルから呼び出されるために必要です。

コアとなるコードの変更箇所

このコミットで変更された主要なファイルとコード箇所は以下の通りです。

  1. src/pkg/runtime/panic.c:

    • runtime·canpanic関数の新規追加。この関数がパニックの安全性を判断するロジックをカプセル化しています。
  2. src/pkg/runtime/runtime.h:

    • runtime·canpanic関数のプロトタイプ宣言の追加。
  3. src/pkg/runtime/signal_386.c:

    • runtime·sighandler関数内で、パニックを発生させる条件が!runtime·canpanic(gp)に変更されました。
  4. src/pkg/runtime/signal_amd64x.c:

    • runtime·sighandler関数内で、パニックを発生させる条件が!runtime·canpanic(gp)に変更されました。
  5. src/pkg/runtime/signal_arm.c:

    • runtime·sighandler関数内で、パニックを発生させる条件が!runtime·canpanic(gp)に変更されました。

コアとなるコードの解説

src/pkg/runtime/panic.cにおけるruntime·canpanic関数

bool
runtime·canpanic(G *gp)
{
	byte g;

	USED(&g);  // don't use global g, it points to gsignal

	// Is it okay for gp to panic instead of crashing the program?
	// Yes, as long as it is running Go code, not runtime code,
	// and not stuck in a system call.
	if(gp == nil || gp != m->curg)
		return false;
	if(m->locks != 0 || m->mallocing != 0 || m->throwing != 0 || m->gcing != 0 || m->dying != 0)
		return false;
	if(gp->status != Grunning || gp->syscallsp != 0)
		return false;
#ifdef GOOS_windows
	if(m->libcallsp != 0)
		return false;
#endif
	return true;
}

この関数は、Goroutine gpがパニックを発生させるべきかどうかを決定する中心的なロジックです。

  • USED(&g);: これはコンパイラに対するヒントで、ローカル変数gが使用されていることを示し、最適化による削除を防ぎます。g自体はここでは直接的な意味を持ちません。コメントにあるように、グローバルなggsignalを指す)と混同しないようにするためのものです。
  • 最初のif文: gpnilであるか、または現在のMが実行しているGoroutine(m->curg)と異なる場合、falseを返します。これは、シグナルがg0(ランタイムGoroutine)上で発生した場合や、有効なGoルーチンコンテキストがない場合にパニックを避けるためです。
  • 2番目のif文: m(現在のOSスレッドのコンテキスト)が、ロックを保持している、メモリ割り当て中、パニック処理中、GC中、または終了処理中である場合、falseを返します。これらの状態はランタイムが不安定であるか、デッドロックのリスクがあることを示唆します。
  • 3番目のif文: gpのステータスがGrunningでない(つまり、実行中ではない)か、またはシステムコール中である場合(gp->syscallsp != 0)、falseを返します。実行中でないGoroutineやシステムコール中のGoroutineは、Goのパニック処理を安全に実行できる状態にありません。
  • #ifdef GOOS_windows: Windows固有の条件で、外部ライブラリ呼び出し中である場合(m->libcallsp != 0)、falseを返します。

これらの条件をすべて満たした場合にのみ、trueが返され、パニックが安全であると判断されます。

シグナルハンドラにおける変更例 (src/pkg/runtime/signal_amd64x.cより)

// 変更前
// if(gp == nil || gp == m->g0)
//     goto Throw;

// 変更後
if(!runtime·canpanic(gp))
    goto Throw;

この変更により、シグナルハンドラは、シグナルがユーザープロセスからのものでなく、かつSigPanicフラグが立っている場合に、まずruntime·canpanic(gp)を呼び出します。

  • もしruntime·canpanic(gp)falseを返した場合(つまり、パニックが安全でないと判断された場合)、goto Throwが実行され、プログラムは直接終了します。
  • runtime·canpanic(gp)trueを返した場合(パニックが安全であると判断された場合)、シグナルハンドラはGoのパニック機構を介してエラーを報告しようと試みます。

この変更は、Goランタイムの堅牢性を高め、特に予期せぬクラッシュが発生した際に、デッドロックやハングといった二次的な問題を防ぐ上で非常に重要です。

関連リンク

  • Goのパニックとリカバリに関する公式ドキュメント(一般的な概念): https://go.dev/blog/defer-panic-and-recover
  • Goランタイムのシグナルハンドリングに関する議論(より深い理解のため): Goのソースコードやメーリングリストのアーカイブが参考になります。

参考にした情報源リンク

  • Goのコミット履歴: https://github.com/golang/go/commits/master
  • Goのコードレビューシステム (Gerrit): https://go.dev/cl/ (特にコミットメッセージに記載されているCL番号)
    • https://golang.org/cl/68540043
    • https://golang.org/cl/75320043
  • Goランタイムのソースコード(特にsrc/runtimeディレクトリ)
  • Goのメーリングリストアーカイブ (golang-dev, golang-nuts)
  • Goの内部実装に関するブログ記事やドキュメント(例: Go scheduler, GC internals)
  • C言語のシグナルハンドリングに関する一般的な知識。
  • SetPanicOnCrashに関する情報(Goの古いバージョンや特定のデバッグツールで使われる可能性のある機能)。