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

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

このコミットは、Goランタイムにおけるシグナルハンドリングの初期化タイミングに関するバグ修正です。プログラムの起動時に、シグナルハンドラが完全に設定される前にシグナルが到着する可能性のある短い期間が存在し、これが問題を引き起こす可能性がありました。このコミットは、シグナルハンドラのインストールを、ランタイムがシグナルを適切に処理できる状態になるまで遅延させることで、この競合状態を解消します。

コミット

commit 2e4a035995e6d22871a27b7ad7c4b688a982b835
Author: Russ Cox <rsc@golang.org>
Date:   Thu Mar 15 22:17:54 2012 -0400

    runtime: do not handle signals before configuring handler
    
    There was a small window during program initialization
    where a signal could come in before the handling mechanisms
    were set up to handle it.  Delay the signal-handler installation
    until we're ready for the signals.
    
    Fixes #3314.
    
    R=golang-dev, dsymonds, mikioh.mikioh
    CC=golang-dev
    https://golang.org/cl/5833049

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

https://github.com/golang/go/commit/2e4a035995e6d22871a27b7ad7c4b688a982b835

元コミット内容

    runtime: do not handle signals before configuring handler
    
    There was a small window during program initialization
    where a signal could come in before the handling mechanisms
    were set up to handle it.  Delay the signal-handler installation
    until we're ready for the signals.
    
    Fixes #3314.
    
    R=golang-dev, dsymonds, mikioh.mikioh
    CC=golang-dev
    https://golang.org/cl/5833049

変更の背景

このコミットは、Goランタイムの初期化プロセスにおける潜在的な競合状態を修正するために行われました。具体的には、Goプログラムが起動する際、オペレーティングシステムからのシグナル(例えば、Ctrl+CによるSIGINTや、不正なメモリアクセスによるSIGSEGVなど)を処理するためのハンドラが完全に設定される前に、これらのシグナルが到着する可能性がありました。

このような状況が発生すると、ランタイムはシグナルを適切に処理できず、プログラムが予期せずクラッシュしたり、デッドロックに陥ったりする可能性がありました。これは、特にプログラムの起動直後に外部からのシグナルや、内部的なエラーシグナルが発生した場合に顕著な問題となります。

コミットメッセージに記載されている Fixes #3314 は、この問題がGoのIssueトラッカーで報告されていたことを示しています。このIssueは、プログラムの起動時にシグナルが処理されない、または誤って処理されることによるクラッシュや異常終了を報告していたと考えられます。このコミットは、その報告された問題を解決することを目的としています。

前提知識の解説

Goランタイム (Go Runtime)

Goランタイムは、Goプログラムの実行を管理する非常に重要な部分です。これには、ガベージコレクション、スケジューラ(ゴルーチンの管理)、メモリ管理、そしてシステムコールやシグナルハンドリングなどの低レベルなOSとのインタラクションが含まれます。Goプログラムは、コンパイル時にランタイムとリンクされ、独立したバイナリとして実行されます。

シグナル (Signals)

シグナルは、Unix系OSにおいてプロセス間通信やイベント通知のために使用されるソフトウェア割り込みの一種です。OSは、特定のイベント(例:ユーザーがCtrl+Cを押す、子プロセスが終了する、不正なメモリアクセスが発生する)が発生した際に、プロセスにシグナルを送信します。プロセスは、これらのシグナルに対してデフォルトの動作(例:終了、コアダンプ)を行うか、またはカスタムのシグナルハンドラ関数を登録して、シグナルを受信した際の動作を定義することができます。

シグナルハンドラ (Signal Handler)

シグナルハンドラは、特定のシグナルがプロセスに送信されたときに実行される関数です。プログラムは signal()sigaction() といったシステムコールを使用して、シグナルハンドラを登録します。シグナルハンドラは非同期に実行されるため、その実装には細心の注意が必要です。特に、シグナルハンドラ内で実行できる操作は限られており、再入可能な関数のみを呼び出すべきです。

競合状態 (Race Condition)

競合状態とは、複数のプロセスやスレッドが共有リソースにアクセスする際に、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。このコミットの背景にある問題は、シグナルハンドラの初期化とシグナルの到着という2つの非同期イベントが、特定のタイミングで重なることで発生する競合状態の一例です。

m0g0 (Goランタイムの内部構造)

Goランタイムの内部では、m (machine) と g (goroutine) という概念があります。

  • m はOSのスレッドを表します。Goランタイムは、OSスレッドを抽象化して m と呼びます。
  • g はGoの軽量スレッドであるゴルーチンを表します。
  • m0 は、Goプログラムが起動した際に最初に作成されるOSスレッド(メインスレッド)に対応する m です。
  • g0 は、各 m に関連付けられた特別なゴルーチンで、ランタイムの内部処理(スケジューリング、スタックの拡張、システムコールなど)を実行するために使用されます。ユーザーコードは g0 上では実行されません。

このコミットでは、m == &runtime·m0 という条件が使われており、これはメインのOSスレッド上でのみシグナルハンドラの初期化を行うことを意味します。

技術的詳細

このコミットの核心は、Goランタイムがシグナルハンドラをインストールするタイミングの変更です。

変更前は、runtime.c 内の runtime·check() 関数内で runtime·initsig() が呼び出されていました。runtime·check() はランタイムの初期化の比較的早い段階で実行される関数であり、様々な初期チェックを行います。この時点では、ランタイムの他の重要なコンポーネント(特にスレッドがシグナルを処理できる状態になるための準備)がまだ完全に整っていない可能性がありました。

変更後は、runtime·initsig() の呼び出しが proc.c 内の runtime·mstart() 関数内に移動されました。runtime·mstart() は、各 m (OSスレッド) が起動する際に実行される関数です。特に、m0 (メインのOSスレッド) の runtime·mstart() が呼び出された際に、runtime·initsig() が実行されるようになりました。

この変更のポイントは以下の通りです。

  1. runtime·minit() の後: runtime·mstart() 内で runtime·minit() が呼び出された後に runtime·initsig() が呼び出されるようになりました。runtime·minit() は、現在の m (OSスレッド) を初期化し、シグナルを処理するために必要な準備(例えば、スタックの設定など)を行います。これにより、シグナルハンドラがインストールされる時点で、そのスレッドがシグナルを適切に処理できる状態になっていることが保証されます。
  2. m0 のみ: if(m == &runtime·m0) という条件が追加されました。これは、シグナルハンドラのインストールが、プログラムのメインスレッド(m0)上でのみ行われることを意味します。シグナルハンドラは通常、プロセス全体に対して設定されるため、複数のスレッドで重複して設定する必要はありません。メインスレッドで一度設定すれば十分です。

この修正により、シグナルハンドラが設定される前にシグナルが到着するという「短いウィンドウ」が閉じられ、Goプログラムの起動時の堅牢性が向上しました。

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

src/pkg/runtime/proc.c

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -734,6 +734,12 @@ runtime·mstart(void)
 	m->g0->sched.pc = (void*)-1;  // make sure it is never used
 	runtime·asminit();
 	runtime·minit();
+
+	// Install signal handlers; after minit so that minit can
+	// prepare the thread to be able to handle the signals.
+	if(m == &runtime·m0)
+		runtime·initsig();
+
 	schedule(nil);
 }
 
@@ -1161,7 +1167,7 @@ runtime·malg(int32 stacksize)
 {
 	G *newg;
 	byte *stk;
-	
+
 	if(StackTop < sizeof(Stktop)) {
 		runtime·printf("runtime: SizeofStktop=%d, should be >=%d\\n", (int32)StackTop, (int32)sizeof(Stktop));
 		runtime·throw("runtime: bad stack.h");

src/pkg/runtime/runtime.c

--- a/src/pkg/runtime/runtime.c
+++ b/src/pkg/runtime/runtime.c
@@ -119,7 +119,7 @@ void
 runtime·panicstring(int8 *s)
 {
 	Eface err;
-	
+
 	if(m->gcing) {
 		runtime·printf("panic: %s\\n", s);
 		runtime·throw("panic during gc\");
@@ -189,7 +189,7 @@ runtime·goargs(void)
 {
 	String *s;
 	int32 i;
-	
+
 	// for windows implementation see "os" package
 	if(Windows)
 		return;
@@ -207,7 +207,7 @@ runtime·goenvs_unix(void)
 {
 	String *s;
 	int32 i, n;
-	
+
 	for(n=0; argv[argc+1+n] != 0; n++)
 		;
 
@@ -342,8 +342,6 @@ runtime·check(void)
 		runtime·throw("float32nan2\");
 	if(!(i != i1))
 		runtime·throw("float32nan3\");
-\n-\truntime·initsig();
 }
 
 void

コアとなるコードの解説

src/pkg/runtime/proc.c の変更

  • runtime·mstart() 関数内に以下のコードが追加されました。
    	// Install signal handlers; after minit so that minit can
    	// prepare the thread to be able to handle the signals.
    	if(m == &runtime·m0)
    		runtime·initsig();
    
    • runtime·mstart() は、GoランタイムがOSスレッド(m)を起動する際に呼び出される関数です。
    • runtime·minit() の呼び出しの後にこのコードが配置されています。runtime·minit() は、現在の m の初期化を行い、シグナル処理に必要な準備を整えます。
    • if(m == &runtime·m0) は、現在の m がメインのOSスレッド(m0)である場合にのみ、runtime·initsig() を呼び出すことを保証します。これにより、シグナルハンドラのインストールが一度だけ、かつ適切なタイミングで行われます。
    • runtime·initsig() は、GoランタイムがOSのシグナルハンドラを設定するための内部関数です。

src/pkg/runtime/runtime.c の変更

  • runtime·check() 関数から以下の行が削除されました。
    	runtime·initsig();
    
    • runtime·check() はランタイムの初期化フェーズで実行される関数ですが、シグナルハンドラのインストールには早すぎるタイミングでした。この行の削除により、シグナルハンドラのインストールがより適切な runtime·mstart() に移管されました。

これらの変更により、シグナルハンドラが設定される前にシグナルが到着するという競合状態が解消され、Goプログラムの起動時の安定性が向上しました。

関連リンク

参考にした情報源リンク

  • Goのソースコード (特に src/pkg/runtime/proc.csrc/pkg/runtime/runtime.c の該当バージョン)
  • GoのIssueトラッカー (Issue #3314 の詳細)
  • Unix/Linuxのシグナルハンドリングに関する一般的なドキュメント
  • Goランタイムの内部構造に関する解説記事 (m, g, schedなど)
    • 例: The Go scheduler (Goスケジューラに関する公式ブログ記事など)
    • 例: Go's work-stealing scheduler (Goのワークスティーリングスケジューラに関する解説)
  • 競合状態に関する一般的なプログラミングの概念