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

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

このコミットは、GoランタイムにおけるPlan 9オペレーティングシステム上でのパニック(panic)とリカバリー(recover)のサポートを追加し、同時にノートハンドリング(note handling)に関する既存の問題を解決することを目的としています。具体的には、ゴルーチン(goroutine)のスタック下部にノートハンドラを実行するための十分な領域を確保し、exitstatusがグローバルエンティティではなくなるように変更することで、競合状態(race conditions)を解消しています。

コミット

commit c74f3c457613638311e3cb2a57a9fca2df849e7a
Author: Akshat Kumar <seed@mail.nanosouffle.net>
Date:   Wed Jan 30 02:53:56 2013 -0800

    runtime: add support for panic/recover in Plan 9 note handler
    
    This change also resolves some issues with note handling: we now make
    sure that there is enough room at the bottom of every goroutine to
    execute the note handler, and the `exitstatus' is no longer a global
    entity, which resolves some race conditions.
    
    R=rminnich, npe, rsc, ality
    CC=golang-dev
    https://golang.org/cl/6569068

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

https://github.com/golang/go/commit/c74f3c457613638311e3cb2a57a9fca2df849e7a

元コミット内容

runtime: add support for panic/recover in Plan 9 note handler

This change also resolves some issues with note handling: we now make
sure that there is enough room at the bottom of every goroutine to
execute the note handler, and the `exitstatus' is no longer a global
entity, which resolves some race conditions.

R=rminnich, npe, rsc, ality
CC=golang-dev
https://golang.org/cl/6569068

変更の背景

この変更の背景には、Go言語のランタイムがPlan 9オペレーティングシステム上で動作する際の、パニックとリカバリーのメカニズムに関する課題がありました。

  1. Plan 9のノートハンドリングとGoのパニック/リカバリーの統合不足: Plan 9は、Unix系のシグナルに似た「ノート(notes)」という非同期イベント通知メカニズムを持っています。Goのパニック/リカバリーは、プログラムの異常終了やエラーからの回復を扱うための重要な機能ですが、Plan 9のノートシステムと適切に連携していませんでした。特に、ノートハンドラ内でパニックが発生した場合の挙動が未定義または不適切であったと考えられます。
  2. ノートハンドラ実行のためのスタック領域不足: ノートハンドラは非同期に呼び出されるため、現在のゴルーチンのスタック上で実行される必要があります。しかし、既存の実装では、ノートハンドラが実行されるための十分なスタック領域が確保されておらず、スタックオーバーフローや予期せぬクラッシュの原因となる可能性がありました。
  3. exitstatusのグローバル変数としての問題: exitstatusは、プログラムの終了ステータスを保持するための変数ですが、これがグローバル変数として扱われていたため、複数のゴルーチンが同時に終了処理を行う際に競合状態が発生し、不正な終了ステータスが設定されたり、デッドロックが発生したりするリスクがありました。

これらの問題を解決し、Plan 9上でのGoプログラムの堅牢性と信頼性を向上させるために、本コミットの変更が導入されました。

前提知識の解説

Plan 9オペレーティングシステム

Plan 9 from Bell Labsは、ベル研究所で開発された分散オペレーティングシステムです。Unixの設計思想をさらに推し進め、すべてのリソース(ファイル、デバイス、ネットワーク接続など)をファイルシステムとして表現するという「すべてはファイルである」という原則を徹底しています。特徴としては、以下が挙げられます。

  • ファイルシステム中心主義: すべてのリソースがファイルとしてアクセス可能。
  • 名前空間: 各プロセスが独自のファイルシステムビュー(名前空間)を持つ。
  • ノート(Notes): Unixのシグナルに相当する非同期イベント通知メカニズム。プロセスは特定のノートを捕捉し、ハンドラを実行できる。ノートは文字列として表現され、より柔軟な情報伝達が可能。
  • rforkシステムコール: プロセス作成とリソース共有を細かく制御できる強力なシステムコール。

Go言語のパニックとリカバリー (panic/recover)

Go言語には、プログラムの異常終了を扱うための組み込みメカニズムとしてpanicrecoverがあります。

  • panic: 実行時エラーやプログラマが意図的に発生させる例外的な状況を示すために使用されます。panicが呼び出されると、通常の実行フローは中断され、現在のゴルーチンの遅延関数(defer文で登録された関数)が順に実行されます。遅延関数がすべて実行されると、呼び出し元の関数にパニックが伝播し、最終的にプログラムがクラッシュするか、recoverによって捕捉されます。
  • recover: defer関数内で呼び出されると、そのゴルーチンで発生したパニックを捕捉し、パニックの値を返します。recoverがパニックを捕捉すると、パニックの伝播は停止し、プログラムの実行はrecoverが呼び出されたdefer関数の次の行から再開されます。これにより、クラッシュを回避し、エラー回復処理を行うことができます。

Goランタイム

Goランタイムは、Goプログラムの実行を管理する非常に重要な部分です。これには、ガベージコレクション、スケジューラ(ゴルーチンの管理)、メモリ割り当て、システムコールインターフェース、そしてシグナル(Plan 9ではノート)ハンドリングなどが含まれます。ランタイムはGo言語で書かれたコードと、アセンブリ言語やC言語で書かれた低レベルのコード(OSとのインターフェース部分など)で構成されています。

ゴルーチン (Goroutine)

ゴルーチンはGo言語における軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。GoランタイムのスケジューラがゴルーチンをOSスレッドにマッピングし、効率的に実行を切り替えます。各ゴルーチンは独自のスタックを持っています。

スタック (Stack)

プログラムの実行中に、関数呼び出しの引数、ローカル変数、戻りアドレスなどが格納されるメモリ領域です。Goのゴルーチンは可変サイズのスタックを持っており、必要に応じて自動的に拡張・縮小されます。

シグナルハンドリング(Plan 9のノートハンドリング)

オペレーティングシステムは、プログラムに対して非同期イベント(シグナルまたはノート)を通知するメカニズムを提供します。プログラムはこれらのイベントを捕捉し、特定のハンドラ関数を実行することで対応できます。Plan 9では、このメカニズムが「ノート」と呼ばれ、notifyシステムコールによってハンドラを設定します。

技術的詳細

このコミットは、Plan 9におけるGoランタイムのノートハンドリングとパニック/リカバリーの統合を強化するために、いくつかの重要な技術的変更を導入しています。

  1. sigtrampsighandlerの導入:

    • runtime·sigtrampは、Plan 9のノートハンドラとして登録されるアセンブリ言語の関数です。これは、OSからノートが通知された際に最初に実行されるエントリポイントとなります。
    • sigtrampは、現在のゴルーチン(g)とマシン(m)の状態を保存し、ノートハンドリング専用のゴルーチン(m->gsignal)のスタックに切り替えます。これにより、ノートハンドラが安全に実行できる独立したスタック空間を確保します。
    • その後、sigtrampはC言語で実装されたruntime·sighandlerを呼び出します。
    • runtime·sighandlerは、受け取ったノート文字列(s)を解析し、Goランタイムが定義するSigTab(シグナルテーブル)と比較します。
    • ノートがSigPanicフラグを持つ(例: メモリ読み書きエラーなど)場合、sighandlerはパニック処理を開始します。具体的には、エラー文字列をm->notesigに保存し、現在のPC(プログラムカウンタ)をスタックにプッシュした後、PCをruntime·sigpanicに設定して、パニックがGoの通常のパニックメカニズムによって処理されるようにします。
    • ノートがSigThrowフラグを持つ(例: 一般的なトラップなど)場合、sighandlerは即座にプログラムを終了させます。
    • それ以外のノートは、デフォルトの動作(NDFLT)または継続(NCONT)を返します。
  2. m->notesigの導入とexitstatusの非グローバル化:

    • M構造体(マシンを表すランタイムの内部構造体)にnotesigフィールド(int8*型)が追加されました。これは、ノートハンドラが受け取ったエラー文字列を一時的に保存するためのバッファとして機能します。これにより、パニック処理中にエラー文字列に安全にアクセスできるようになります。
    • 以前はグローバル変数であったruntime·exitstatusが削除され、runtime·goexitsallおよびruntime·exit関数内でローカル変数として終了ステータスが管理されるようになりました。これにより、複数のゴルーチンが同時に終了処理を行う際の競合状態が解消され、より堅牢な終了処理が実現されます。
  3. StackSystemの増加:

    • src/pkg/runtime/stack.hにおいて、StackSystem定数がPlan 9向けに512バイトに設定されました。StackSystemは、OS固有の目的(シグナルハンドリングなど)のために、通常のガード領域の下に各スタックに追加されるバイト数です。これにより、ノートハンドラが実行されるための十分なスタック領域が、すべてのゴルーチンで確保されるようになります。
  4. Ureg構造体の定義:

    • src/pkg/runtime/defs_plan9_386.hsrc/pkg/runtime/defs_plan9_amd64.hに、Plan 9のレジスタ情報を含むUreg構造体が定義されました。これは、ノートハンドラが呼び出された時点のCPUレジスタの状態を捕捉し、パニック発生時のトレースバックなどに利用されます。
  5. procidの取得方法の変更:

    • src/cmd/dist/buildruntime.cとアセンブリコードにおいて、procid(プロセスID)の取得方法が変更されました。Plan 9ではスレッドローカルストレージ(TLS)の概念が異なるため、_tos(Thread-local OS structure)からのオフセットでprocidを取得するように修正されました。

これらの変更により、Plan 9上でのGoプログラムは、より信頼性の高いパニック/リカバリーメカニズムと、競合状態のない終了処理を実現できるようになりました。

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

このコミットで変更された主要なファイルと、それぞれの変更の概要は以下の通りです。

  • src/cmd/dist/buildruntime.c:
    • Plan 9 (386) 向けのprocidマクロが追加され、_tosからのオフセットでプロセスIDを取得するように変更されました。これは、Plan 9がスレッドローカルストレージを異なる方法で扱うためです。
  • src/pkg/runtime/defs_plan9_386.h, src/pkg/runtime/defs_plan9_amd64.h:
    • Plan 9のレジスタ状態を保持するUreg構造体が定義されました。これは、ノートハンドラが呼び出された際のCPUの状態を捕捉するために使用されます。
  • src/pkg/runtime/os_plan9.h:
    • runtime·notify関数のシグネチャが変更され、runtime·sigtramp, runtime·sighandler, runtime·sigpanic, runtime·goexitsallなどの新しい関数プロトタイプが追加されました。
    • NSIG(シグナル数)が1から5に増加し、ERRMAX(ノート文字列の最大長)が定義されました。
  • src/pkg/runtime/runtime.h:
    • M構造体(マシンを表すランタイムの内部構造体)にnotesigフィールドが追加されました。これは、ノートハンドラが受け取ったエラー文字列を保存するために使用されます。
  • src/pkg/runtime/signal_plan9_386.c, src/pkg/runtime/signal_plan9_amd64.c:
    • runtime·dumpregs関数が追加され、Ureg構造体の内容(レジスタ値)をダンプできるようになりました。
    • **runtime·sighandler関数が追加されました。**これがノートハンドリングの主要なロジックを実装しています。ノートの種類に応じてパニックを発生させるか、プログラムを終了させるかを決定します。
  • src/pkg/runtime/signals_plan9.h:
    • runtime·sigtab配列が定義されました。これは、Plan 9のノート文字列と、それに対応するGoのシグナル処理フラグ(SigPanic, SigThrow, SigNotify)をマッピングします。特に、メモリ読み書きエラーはパニックとして扱われるように設定されています。
  • src/pkg/runtime/stack.h:
    • StackSystem定数がPlan 9向けに512バイトに設定されました。これにより、ノートハンドラが実行されるための十分なスタック領域が確保されます。
  • src/pkg/runtime/sys_plan9_386.s, src/pkg/runtime/sys_plan9_amd64.s:
    • **runtime·sigtrampアセンブリ関数が追加されました。**これは、OSからノートが通知された際に最初に実行されるエントリポイントであり、スタックの切り替えやruntime·sighandlerの呼び出しを行います。
    • runtime·rfork内のprocidの取得ロジックが変更されました。
  • src/pkg/runtime/thread_plan9.c:
    • runtime·minit関数内で、ノートハンドリング用のゴルーチン(m->gsignal)とエラー文字列バッファ(m->notesig)が初期化されるようになりました。
    • runtime·osinit関数内で、runtime·notifyruntime·sigtrampが登録されるようになりました。
    • runtime·gonote関数が削除され、runtime·goexitsallruntime·exit関数がexitstatusをグローバル変数ではなくローカル変数として扱うように変更されました。
    • runtime·sigpanic関数が修正され、m->notesigに保存されたエラー文字列に基づいてパニックメッセージを生成するようになりました。
    • runtime·badsignal関数が修正され、引数sigが削除されました。

コアとなるコードの解説

src/pkg/runtime/signal_plan9_386.c および src/pkg/runtime/signal_plan9_amd64.c における runtime·sighandler

この関数は、Plan 9のノートハンドリングの中核をなすC言語のコードです。

int32
runtime·sighandler(void *v, int8 *s, G *gp)
{
	Ureg *ureg;
	uintptr *sp;
	SigTab *sig, *nsig;
	int32 len, i;

	if(!s)
		return NCONT;

	len = runtime·findnull((byte*)s);
	if(len <= 4 || runtime·mcmp((byte*)s, (byte*)"sys:", 4) != 0)
		return NDFLT;

	nsig = nil;
	sig = runtime·sigtab;
	for(i=0; i < NSIG; i++) {
		if(runtime·strstr((byte*)s, (byte*)sig->name)) {
			nsig = sig;
			break;
		}
		sig++;
	}

	if(nsig == nil)
		return NDFLT;

	ureg = v;
	if(nsig->flags & SigPanic) {
		if(gp == nil || m->notesig == 0)
			goto Throw;

		// Save error string from sigtramp's stack,
		// into gsignal->sigcode0, so we can reliably
		// access it from the panic routines.
		if(len > ERRMAX)
			len = ERRMAX;
		runtime·memmove((void*)m->notesig, (void*)s, len);

		gp->sig = i;
		gp->sigpc = ureg->pc; // or ureg->ip for amd64

		// Only push runtime·sigpanic if ureg->pc != 0.
		// If ureg->pc == 0, probably panicked because of a
		// call to a nil func.  Not pushing that onto sp will
		// make the trace look like a call to runtime·sigpanic instead.
		// (Otherwise the trace will end at runtime·sigpanic and we
		// won't get to see who faulted.)
		if(ureg->pc != 0) { // or ureg->ip for amd64
			sp = (uintptr*)ureg->sp;
			*--sp = ureg->pc; // or ureg->ip for amd64
			ureg->sp = (uint32)sp; // or uint64 for amd64
		}
		ureg->pc = (uintptr)runtime·sigpanic; // or ureg->ip for amd64
		return NCONT;
	}

	if(!(nsig->flags & SigThrow))
		return NDFLT;

Throw:
	runtime·startpanic();

	runtime·printf("%s\\n", s);
	runtime·printf("PC=%X\\n", ureg->pc); // or ureg->ip for amd64
	runtime·printf("\\n");

	if(runtime·gotraceback()) {
		runtime·traceback((void*)ureg->pc, (void*)ureg->sp, 0, gp); // or ureg->ip for amd64
		runtime·tracebackothers(gp);
		runtime·dumpregs(ureg);
	}
	runtime·goexitsall("");
	runtime·exits(s);

	return 0;
}
  • 引数:
    • v: Ureg構造体へのポインタ。ノート発生時のCPUレジスタの状態が含まれます。
    • s: ノート文字列(例: "sys: trap: fault read addr")。
    • gp: 現在のゴルーチン(G構造体)へのポインタ。
  • ノートの解析: ノート文字列sを解析し、runtime·sigtabsignals_plan9.hで定義)と比較して、対応するSigTabエントリを見つけます。
  • SigPanic処理:
    • もしノートがSigPanicフラグを持つ場合(例: メモリ読み書きエラー)、Goのパニックメカニズムに引き渡します。
    • ノート文字列をm->notesigにコピーします。これは、パニック処理中にエラーメッセージにアクセスできるようにするためです。
    • 現在のプログラムカウンタ(ureg->pcまたはureg->ip)をスタックにプッシュし、ureg->pc(またはureg->ip)をruntime·sigpanic関数のアドレスに設定します。これにより、ノートハンドラから戻った際にruntime·sigpanicが実行され、Goのパニック処理が開始されます。
  • SigThrow処理:
    • もしノートがSigThrowフラグを持つ場合(例: 一般的なトラップ)、即座にプログラムを終了させます。これは、回復不能なエラーと見なされるためです。
    • runtime·startpanic()を呼び出し、エラーメッセージとレジスタ情報を出力し、トレースバックを生成した後、runtime·goexitsallruntime·exitsを呼び出してプログラムを終了します。
  • その他のノート: NDFLT(デフォルトの動作)またはNCONT(継続)を返します。

src/pkg/runtime/sys_plan9_386.s および src/pkg/runtime/sys_plan9_amd64.s における runtime·sigtramp

このアセンブリ関数は、Plan 9のnotifyシステムコールによって登録される実際のノートハンドラです。

// void sigtramp(void *ureg, int8 *note)
TEXT runtime·sigtramp(SB),7,$0
	get_tls(AX)

	// check that m exists
	MOVL	m(AX), BX
	CMPL	BX, $0
	JNE	3(PC)
	CALL	runtime·badsignal(SB) // will exit
	RET

	// save args
	MOVL	ureg+4(SP), CX
	MOVL	note+8(SP), DX

	// change stack
	MOVL	m_gsignal(BX), BP
	MOVL	g_stackbase(BP), BP
	MOVL	BP, SP

	// make room for args and g
	SUBL	$16, SP

	// save g
	MOVL	g(AX), BP
	MOVL	BP, 12(SP)

	// g = m->gsignal
	MOVL	m_gsignal(BX), DI
	MOVL	DI, g(AX)

	// load args and call sighandler
	MOVL	CX, 0(SP)
	MOVL	DX, 4(SP)
	MOVL	BP, 8(SP)

	CALL	runtime·sighandler(SB)

	// restore g
	get_tls(BX)
	MOVL	12(SP), BP
	MOVL	BP, g(BX)

	// call noted(AX)
	MOVL	AX, 0(SP)
	CALL	runtime·noted(SB)
	RET
  • TLS (Thread-Local Storage) の取得: get_tls(AX)マクロを使用して、現在のスレッドのTLS(GoランタイムではM構造体へのポインタ)を取得します。
  • mの存在チェック: m(現在のマシン)が存在するかどうかを確認し、存在しない場合はruntime·badsignalを呼び出して終了します。
  • 引数の保存: uregnote(ノート文字列)の引数をレジスタに保存します。
  • スタックの切り替え:
    • m->gsignal(ノートハンドリング専用のゴルーチン)のスタックベースアドレスを取得し、現在のスタックポインタ(SP)をそのアドレスに設定します。これにより、ノートハンドラは独立したスタックで実行されます。
    • 引数と現在のゴルーチン(g)を保存するための領域をスタックに確保します。
  • gの切り替え: 現在のゴルーチン(g)を保存し、m->gsignalを現在のゴルーチンとして設定します。
  • runtime·sighandlerの呼び出し: 保存した引数(ureg, note, 元のg)をスタックにプッシュし、runtime·sighandlerを呼び出します。
  • gの復元: runtime·sighandlerから戻った後、元のゴルーチン(g)を復元します。
  • notedの呼び出し: runtime·notedシステムコールを呼び出して、OSにノート処理が完了したことを通知します。

src/pkg/runtime/signals_plan9.h における runtime·sigtab

このヘッダファイルでは、Plan 9のノート文字列とGoランタイムのシグナル処理フラグのマッピングが定義されています。

#define N SigNotify
#define T SigThrow
#define P SigPanic

SigTab runtime·sigtab[] = {
	P, "sys: fp:",

	// Go libraries expect to be able
	// to recover from memory
	// read/write errors, so we flag
	// those as panics. All other traps
	// are generally more serious and
	// should immediately throw an
	// exception.
	P, "sys: trap: fault read addr",
	P, "sys: trap: fault write addr",
	T, "sys: trap:",

	N, "sys: bad sys call",
};

#undef N
#undef T
#undef P
  • SigTab構造体は、ノート文字列(name)と、それに対応するフラグ(flags)を持ちます。
  • SigPanic (P): このフラグが設定されたノートは、Goのパニックとして処理され、recoverによって捕捉される可能性があります。例として、浮動小数点例外(sys: fp:)やメモリ読み書きエラー(sys: trap: fault read addr, sys: trap: fault write addr)が挙げられます。これは、Goのライブラリがこれらのエラーから回復できることを期待しているためです。
  • SigThrow (T): このフラグが設定されたノートは、即座にプログラムを終了させます。一般的なトラップ(sys: trap:)などがこれに該当し、通常は回復不能な深刻なエラーと見なされます。
  • SigNotify (N): このフラグが設定されたノートは、単に通知として扱われ、特別なパニックや終了処理は行われません。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (特に src/pkg/runtime ディレクトリ)
  • Plan 9のドキュメント (特にノートハンドリングに関する部分)
  • Go言語の公式ドキュメントおよびブログ記事
  • Stack Overflowや技術ブログなど、Go言語のランタイムやPlan 9に関する一般的な情報源。
    • https://go.dev/blog/defer-panic-and-recover
    • https://9p.io/plan9/
    • https://github.com/golang/go/
    • https://golang.org/cl/6569068 (元のGerritチェンジリスト)
    • https://go.dev/src/runtime/ (Goランタイムのソースコード)
    • https://go.dev/src/cmd/dist/buildruntime.c
    • https://go.dev/src/pkg/runtime/defs_plan9_386.h
    • https://go.dev/src/pkg/runtime/defs_plan9_amd64.h
    • https://go.dev/src/pkg/runtime/os_plan9.h
    • https://go.dev/src/pkg/runtime/runtime.h
    • https://go.dev/src/pkg/runtime/signal_plan9_386.c
    • https://go.dev/src/pkg/runtime/signal_plan9_amd64.c
    • https://go.dev/src/pkg/runtime/signals_plan9.h
    • https://go.dev/src/pkg/runtime/stack.h
    • https://go.dev/src/pkg/runtime/sys_plan9_386.s
    • https://go.dev/src/pkg/runtime/sys_plan9_amd64.s
    • https://go.dev/src/pkg/runtime/thread_plan9.c