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

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

このコミットは、GoランタイムにおけるFreeBSD環境でのスレッド作成時のシグナルハンドリングに関するバグ修正です。具体的には、新しいスレッドが生成される際にシグナルが到着すると発生する「二重フォルト」の問題を解決するために、スレッド生成中にシグナルを一時的に無視するように変更されています。

コミット

Author: Devon H. O'Dell devon.odell@gmail.com Date: Wed Feb 22 15:44:09 2012 +1100

runtime: fix FreeBSD signal handling around thread creation
Ignore signals while we are spawning a new thread. Previously, a
signal arriving just before runtime.minit setting up the signal
handler triggers a "double fault" in signal trampolining.
Fixes #3017.

R=rsc, mikioh.mikioh, minux.ma, adg
CC=golang-dev
https://golang.org/cl/5684060

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

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

元コミット内容

runtime: fix FreeBSD signal handling around thread creation
Ignore signals while we are spawning a new thread. Previously, a
signal arriving just before runtime.minit setting up the signal
handler triggers a "double fault" in signal trampolining.
Fixes #3017.

R=rsc, mikioh.mikioh, minux.ma, adg
CC=golang-dev
https://golang.org/cl/5684060

変更の背景

このコミットは、GoランタイムがFreeBSD上で新しいスレッド(OSスレッド)を作成する際に発生する特定の競合状態(race condition)によって引き起こされるバグを修正するために導入されました。

問題の核心は、Goランタイムが新しいOSスレッドを生成し、そのスレッド内でシグナルハンドラを設定するまでの短い期間にありました。この期間中にシグナル(例えば、タイマーシグナルやセグメンテーションフォルトなど)が到着すると、まだ適切にシグナルハンドリングの準備ができていない状態であるため、システムが予期せぬ動作を引き起こしていました。コミットメッセージではこれを「signal trampoliningにおける二重フォルト」と表現しています。

具体的には、Goランタイムはruntime.minit関数内でシグナルハンドラを初期化しますが、新しいスレッドが起動してからruntime.minitが実行されるまでの間にシグナルが配送されると、Goランタイムが期待するシグナル処理のメカニズムがまだ確立されていないため、シグナルが適切に処理されず、結果としてクラッシュや不正な動作を引き起こしていました。この問題はGoのIssue #3017として報告されていました。

この修正の目的は、新しいスレッドが完全に初期化され、シグナルハンドリングの準備が整うまでの間、シグナルが配送されないように一時的にブロックすることで、この競合状態を回避し、システムの安定性を向上させることです。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

  1. シグナル (Signals): Unix系OSにおけるシグナルは、プロセスに対して非同期的にイベントを通知するソフトウェア割り込みの一種です。例えば、Ctrl+Cによる割り込み(SIGINT)、不正なメモリアクセス(SIGSEGV)、子プロセスの終了(SIGCHLD)などがあります。プロセスはシグナルを受け取ると、デフォルトの動作(プロセス終了など)を実行するか、事前に登録されたシグナルハンドラ関数を実行します。

  2. シグナルハンドリング (Signal Handling): プロセスが特定のシグナルを受け取ったときにどのように振る舞うかを定義するメカニズムです。sigactionシステムコールなどを用いて、シグナルハンドラ関数を登録したり、シグナルのブロック(マスク)を設定したりできます。

  3. sigprocmaskシステムコール: プロセスのシグナルマスク(ブロックされているシグナルのセット)を検査または変更するために使用されるシステムコールです。

    • SIG_SETMASK: set引数で指定されたシグナルセットを新しいシグナルマスクとして設定します。
    • SIG_BLOCK: set引数で指定されたシグナルセットを現在のシグナルマスクに追加します。
    • SIG_UNBLOCK: set引数で指定されたシグナルセットを現在のシグナルマスクから削除します。 このコミットでは、SIG_SETMASKを使用して、スレッド作成中にすべてのシグナルをブロックし、その後元のシグナルマスクに戻すという操作を行っています。
  4. SIGFILLSETマクロ: sigset_t型のシグナルセットを、すべての標準シグナルを含むように初期化するマクロです。これにより、すべてのシグナルをブロックするためのマスクを簡単に作成できます。

  5. pthread_create関数: POSIXスレッド(pthreads)ライブラリの一部で、新しいスレッドを作成するために使用される関数です。この関数は、新しいスレッドの属性(スタックサイズなど)を指定し、新しいスレッドが実行を開始する関数ポインタを渡します。Goランタイムは、OSスレッドを生成する際に内部的にこれを使用します。

  6. シグナル・トランポリン (Signal Trampolining): これは、シグナルハンドラが呼び出される際の低レベルなメカニズムを指します。OSがシグナルをプロセスに配送する際、直接ユーザー定義のシグナルハンドラ関数を呼び出すのではなく、通常はカーネルが提供する小さなコード(トランポリン)を介して呼び出します。このトランポリンは、シグナルハンドラが実行されるためのコンテキスト(スタックフレーム、レジスタの状態など)を適切に設定し、ハンドラからの復帰を処理します。このプロセス中に問題が発生すると、「二重フォルト」のような深刻なエラーにつながる可能性があります。

  7. 二重フォルト (Double Fault): 通常、CPUの例外処理中に別の例外が発生した場合に用いられる用語ですが、ここではシグナルハンドリングの文脈で、シグナル処理中にさらに別のシグナル(または関連するエラー)が発生し、システムが回復不能な状態に陥ることを指しています。Goランタイムがシグナルハンドラをセットアップする前にシグナルが到着すると、そのシグナルを処理しようとして失敗し、その失敗がさらに別の問題を引き起こす、という連鎖的なエラーが発生していたと考えられます。

技術的詳細

この修正の技術的なアプローチは、新しいOSスレッドが作成され、Goランタイムがそのスレッドのシグナルハンドリングを完全に初期化するまでの短い期間、すべてのシグナルを一時的にブロックすることです。これにより、未初期化の状態でのシグナル配送による競合状態を回避します。

具体的な手順は以下の通りです。

  1. シグナルマスクの保存: pthread_createを呼び出す直前に、現在のスレッドのシグナルマスク(ブロックされているシグナルのセット)を保存します。これは、スレッド作成後に元のシグナルマスクに戻すために必要です。
  2. すべてのシグナルのブロック: SIGFILLSETマクロを使用して、すべてのシグナルを含む新しいシグナルセットを作成し、sigprocmask(SIG_SETMASK, &ign, &oset)を呼び出して、この新しいセットを現在のスレッドのシグナルマスクとして設定します。これにより、スレッド作成中にすべてのシグナルがブロックされます。
  3. 新しいスレッドの作成: pthread_createを呼び出して新しいOSスレッドを生成します。この間、シグナルはブロックされているため、予期せぬシグナル配送による問題は発生しません。
  4. 元のシグナルマスクへの復元: pthread_createが成功した後、保存しておいた元のシグナルマスクをsigprocmask(SIG_SETMASK, &oset, nil)を呼び出して復元します。これにより、スレッド作成後のシグナルハンドリングは通常の動作に戻ります。

このアプローチにより、Goランタイムが新しいスレッドのコンテキストでシグナルハンドラを安全に初期化する時間を確保し、FreeBSD環境での「二重フォルト」の問題を解決しています。

また、runtime.minit関数(Goランタイムの初期化の一部)においても、初期化の最後にruntime·sigprocmask(&sigset_none, nil)を呼び出して、すべてのシグナルのブロックを解除し、シグナルハンドリングが正常に機能するようにしています。これは、Goランタイムが起動する際に、シグナルがブロックされていないことを保証するためです。

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

このコミットでは、以下のファイルが変更されています。

  • src/pkg/runtime/cgo/gcc_freebsd_386.c: FreeBSD/386アーキテクチャ向けのCGOスレッド開始処理。
  • src/pkg/runtime/cgo/gcc_freebsd_amd64.c: FreeBSD/AMD64アーキテクチャ向けのCGOスレッド開始処理。
  • src/pkg/runtime/os_freebsd.h: FreeBSD固有のOS関連ヘッダファイル。runtime·sigprocmask関数のプロトタイプが追加されています。
  • src/pkg/runtime/sys_freebsd_386.s: FreeBSD/386アーキテクチャ向けのシステムコールラッパー(アセンブリ)。runtime·sigprocmaskシステムコールラッパーが追加されています。
  • src/pkg/runtime/sys_freebsd_amd64.s: FreeBSD/AMD64アーキテクチャ向けのシステムコールラッパー(アセンブリ)。runtime·sigprocmaskシステムコールラッパーが追加されています。
  • src/pkg/runtime/thread_freebsd.c: FreeBSD固有のスレッド関連処理。runtime·newosproc関数とruntime·minit関数が変更されています。

コアとなるコードの解説

src/pkg/runtime/cgo/gcc_freebsd_386.c および src/pkg/runtime/cgo/gcc_freebsd_amd64.c

これらのファイルは、CGO(C言語との相互運用)を介して新しいOSスレッドを開始する際の処理を定義しています。変更の核心は、libcgo_sys_thread_start関数内でpthread_createを呼び出す前後にシグナルマスクを操作する部分です。

// 変更前 (例: gcc_freebsd_386.c)
// ...
// err = pthread_create(&p, &attr, threadentry, ts);
// ...

// 変更後 (例: gcc_freebsd_386.c)
#include <sys/types.h>
#include <sys/signalvar.h> // 新規追加
#include <pthread.h>
#include <signal.h> // 新規追加
// ...
void
libcgo_sys_thread_start(ThreadStart *ts)
{
	pthread_attr_t attr;
	sigset_t ign, oset; // 新規追加: シグナルセット変数
	pthread_t p;
	size_t size;
	int err;

	SIGFILLSET(ign); // 新規追加: ignにすべてのシグナルを設定
	sigprocmask(SIG_SETMASK, &ign, &oset); // 新規追加: すべてのシグナルをブロックし、元のマスクをosetに保存

	pthread_attr_init(&attr);
	pthread_attr_getstacksize(&attr, &size);
	ts->g->stackguard = size;
	err = pthread_create(&p, &attr, threadentry, ts);

	sigprocmask(SIG_SETMASK, &oset, nil); // 新規追加: 元のシグナルマスクを復元

	if (err != 0) {
		fprintf(stderr, "runtime/cgo: pthread_create failed: %s\n", strerror(err));
		abort();
	}
}

この変更により、pthread_createが呼び出される前にすべてのシグナルがブロックされ、スレッドが正常に作成された後に元のシグナルマスクが復元されることで、スレッド作成中の競合状態が回避されます。

src/pkg/runtime/os_freebsd.h

このファイルには、FreeBSD固有のOS関連関数のプロトタイプが定義されています。 runtime·sigprocmask関数のプロトタイプが追加されました。

// 変更前
// ...
// void	runtiem·setitimerval(int32, Itimerval*, Itimerval*);

// 変更後
// ...
void	runtime·sigprocmask(Sigset *, Sigset *); // 新規追加
// ...

これにより、GoランタイムのCコードからアセンブリで実装されたruntime·sigprocmask関数を呼び出すことができるようになります。

src/pkg/runtime/sys_freebsd_386.s および src/pkg/runtime/sys_freebsd_amd64.s

これらのアセンブリファイルには、sigprocmaskシステムコールを呼び出すためのラッパー関数runtime·sigprocmaskが追加されています。GoランタイムのCコードから直接システムコールを呼び出すのではなく、アセンブリラッパーを介して呼び出すことで、Goのランタイム環境とOSのシステムコール間のインターフェースを適切に管理しています。

// 例: sys_freebsd_386.s
TEXT runtime·sigprocmask(SB),7,$16
	MOVL	$0, 0(SP)		// syscall gap
	MOVL	$3, 4(SP)		// arg 1 - how (SIG_SETMASK)
	MOVL	args+0(FP), AX
	MOVL	AX, 8(SP)		// arg 2 - set
	MOVL	args+4(FP), AX
	MOVL	AX, 12(SP)		// arg 3 - oset
	MOVL	$340, AX		// sys_sigprocmask (FreeBSDのsigprocmaskシステムコール番号)
	INT	$0x80           // システムコール呼び出し
	JAE	2(PC)
	CALL	runtime·notok(SB)
	RET

// 例: sys_freebsd_amd64.s
TEXT runtime·sigprocmask(SB),7,$0
	MOVL	$3, DI			// arg 1 - how (SIG_SETMASK)
	MOVQ	8(SP), SI		// arg 2 - set
	MOVQ	16(SP), DX		// arg 3 - oset
	MOVL	$340, AX		// sys_sigprocmask (FreeBSDのsigprocmaskシステムコール番号)
	SYSCALL                 // システムコール呼び出し
	JAE	2(PC)
	CALL	runtime·notok(SB)
	RET

これらのアセンブリコードは、sigprocmaskシステムコールを呼び出すために必要な引数をレジスタまたはスタックに配置し、適切なシステムコール番号(FreeBSDでは340)を使用してシステムコールを実行します。

src/pkg/runtime/thread_freebsd.c

このファイルは、FreeBSD固有のスレッド管理ロジックを含んでいます。 runtime·newosproc関数とruntime·minit関数が変更されています。

  1. runtime·newosproc関数: 新しいOSスレッドを生成するGoランタイムの内部関数です。ここでも、thr_new(FreeBSDのスレッド作成システムコール)を呼び出す前後にシグナルマスクを操作しています。

    // 変更前
    // ...
    // runtime·thr_new(&param, sizeof param);
    
    // 変更後
    // ...
    static Sigset sigset_all = { ~(uint32)0, ~(uint32)0, ~(uint32)0, ~(uint32)0, }; // 新規追加: すべてのシグナルを含むセット
    // ...
    void
    runtime·newosproc(M *m, G *g, void *stk, void (*fn)(void))
    {
    	ThrParam param;
    	Sigset oset; // 新規追加: 元のシグナルマスクを保存する変数
    
    	// ... (既存のコード)
    
    	runtime·sigprocmask(&sigset_all, &oset); // 新規追加: すべてのシグナルをブロックし、元のマスクをosetに保存
    	runtime·memclr((byte*)&param, sizeof param);
    
    	// ... (既存のコード)
    
    	runtime·thr_new(&param, sizeof param);
    	runtime·sigprocmask(&oset, nil); // 新規追加: 元のシグナルマスクを復元
    }
    

    libcgo_sys_thread_startと同様に、thr_newシステムコール呼び出しの前後でシグナルマスクを操作することで、スレッド作成中の安全性を確保しています。

  2. runtime·minit関数: Goランタイムの初期化処理の一部です。この関数は、シグナルハンドリングの初期化も行います。

    // 変更前
    // ...
    // runtime·signalstack(m->gsignal->stackguard - StackGuard, 32*1024);
    
    // 変更後
    // ...
    static Sigset sigset_none = { 0, 0, 0, 0, }; // 新規追加: シグナルを含まないセット
    // ...
    void
    runtime·minit(void)
    {
    	// Initialize signal handling
    	m->gsignal = runtime·malg(32*1024);
    	runtime·signalstack(m->gsignal->stackguard - StackGuard, 32*1024);
    	runtime·sigprocmask(&sigset_none, nil); // 新規追加: シグナルマスクをクリア(すべてのシグナルを許可)
    }
    

    runtime·minitの最後にruntime·sigprocmask(&sigset_none, nil)を追加することで、ランタイムの初期化が完了した時点で、すべてのシグナルがブロックされていない状態(つまり、シグナルが正常に配送される状態)であることを保証しています。これは、システムが起動した直後にシグナルがブロックされたままになることを防ぐための重要なステップです。

これらの変更は、GoランタイムがFreeBSD上でより堅牢に動作し、スレッド作成時のシグナル関連の競合状態によるクラッシュを防ぐために不可欠でした。

関連リンク

参考にした情報源リンク

  • sigprocmask(2) man page (Unix/Linux): シグナルマスクの操作に関する詳細。
  • pthread_create(3) man page (Unix/Linux): POSIXスレッド作成に関する詳細。
  • FreeBSDのシステムコールに関するドキュメント: FreeBSDにおけるシステムコールの呼び出し規約や番号に関する情報。
  • Goランタイムのシグナルハンドリングに関する一般的な情報源(Goのドキュメントやブログ記事など)。
  • シグナル・トランポリンに関するOSの内部動作に関する資料。
  • 二重フォルトに関する一般的なコンピュータアーキテクチャの概念。