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

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

このコミットは、Go言語のランタイムにおけるCgo(C言語との相互運用機能)がmacOS (Darwin) 環境でシグナル処理に関連するデッドロックを引き起こす問題を修正するものです。具体的には、sigprocmask() システムコールがDarwin上でプロセス全体に影響を与えるために発生する、シグナルが永続的にブロックされる可能性のある状況を解消します。

コミット

commit 4eb7ba743d82029063f993bc8eea8940c3c61ac6
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Feb 25 16:36:29 2013 -0500

    runtime/cgo: fix deadlock involving signals on darwin
    sigprocmask() is process-wide on darwin, so two concurrent
    libcgo_sys_thread_start() can result in all signals permanently
    blocked, which in particular blocks handling of nil derefs.
    Fixes #4833.
    
    R=golang-dev, dave, rsc
    CC=golang-dev
    https://golang.org/cl/7324058

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

https://github.com/golang/go/commit/4eb7ba743d82029063f993bc8eea8940c3c61ac6

元コミット内容

このコミットの目的は、Darwin (macOS) 環境におけるGoランタイムのCgo部分で発生する、シグナル処理に関連するデッドロックを修正することです。問題の根源は、sigprocmask() 関数がDarwinではプロセス全体に作用するため、複数のlibcgo_sys_thread_start()呼び出しが同時に発生すると、すべてのシグナルが永続的にブロックされてしまう可能性があった点にあります。これにより、特にnilポインタ参照(nil dereference)のような重要なエラーに対するシグナルハンドリングが機能しなくなるという深刻な問題が発生していました。この修正は、Go issue #4833 を解決します。

変更の背景

Goランタイムは、Cgoを介してC言語のコードを呼び出す際に、OSのスレッドを管理します。特に、GoのgoroutineがCコードを呼び出す際には、OSのスレッドにアタッチされることがあります。このプロセスにおいて、シグナルマスク(スレッドがブロックするシグナルのセット)の操作が必要になる場合があります。

従来のGoランタイムでは、Darwin環境でCgoスレッドを開始する際に、sigprocmask() 関数を使用してシグナルマスクを一時的に変更していました。sigprocmask() はPOSIX標準で定義されており、通常は呼び出し元のスレッドのシグナルマスクを変更します。しかし、Darwinの特定のバージョン(このコミットが作成された2013年頃)では、sigprocmask() の挙動が異なり、プロセス全体にシグナルマスクの変更が適用されるという非標準的な実装になっていました。

この非標準的な挙動が問題を引き起こしました。複数のCgoスレッドがほぼ同時にlibcgo_sys_thread_start()関数を呼び出し、それぞれがsigprocmask()を使ってシグナルをブロックしようとすると、競合状態が発生します。もし、あるスレッドがシグナルをブロックした後に、別のスレッドがシグナルマスクを元に戻す前にクラッシュしたり、予期せぬ終了をしたりすると、プロセス全体のシグナルマスクがブロックされた状態のままになってしまう可能性がありました。

特に深刻だったのは、nilポインタ参照のような実行時エラーが発生した際に、OSがプロセスに送るSIGSEGV(セグメンテーション違反)などのシグナルがブロックされてしまうことでした。これにより、Goランタイムがクラッシュを適切に処理できず、デッドロックやハングアップが発生する原因となっていました。Go issue #4833 はこの具体的な問題点を報告しており、このコミットはその解決を目指しています。

前提知識の解説

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

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

  2. シグナルマスク (Signal Mask): 各スレッドは、自身がブロックするシグナルのセット(シグナルマスク)を持っています。シグナルがブロックされている間は、そのシグナルが配送されてもすぐに処理されず、シグナルマスクから解除されるまで保留されます。

  3. sigprocmask(): POSIX標準で定義されている関数で、呼び出し元のスレッドのシグナルマスクを検査または変更するために使用されます。通常、この関数はスレッドローカルなシグナルマスクにのみ影響を与えます。

  4. pthread_sigmask(): POSIXスレッド(pthreads)ライブラリで定義されている関数で、sigprocmask() と同様にスレッドのシグナルマスクを操作しますが、こちらは明示的にスレッドのコンテキストで動作することが保証されています。pthread_sigmask() は、sigprocmask() がプロセス全体に影響を与えるような非標準的な実装を持つOS環境でも、スレッドローカルなシグナルマスクを安全に操作するために推奨されます。

  5. Cgo: Go言語の機能の一つで、GoプログラムからC言語の関数を呼び出したり、C言語のコードからGoの関数を呼び出したりするためのメカニズムです。Cgoを使用すると、GoのランタイムとCのランタイムが共存し、GoのgoroutineがCの関数を呼び出す際にOSのスレッドにアタッチされるなど、複雑な相互作用が発生します。

  6. nil dereference (nilポインタ参照): プログラミングにおいて、初期化されていない(nilまたはnull)ポインタが指すメモリ領域にアクセスしようとすることです。これは通常、プログラムのクラッシュを引き起こす重大なエラーであり、OSはこれに対してSIGSEGVなどのシグナルを生成します。

  7. Darwin (macOS): AppleのmacOSオペレーティングシステムの基盤となるUnix系OSです。このコミットの時点では、Darwinのsigprocmask()の実装がPOSIX標準と異なり、プロセス全体に影響を与えるという特殊な挙動をしていました。

技術的詳細

このコミットの核心は、Darwin環境におけるsigprocmask()の非標準的な挙動を回避することにあります。

GoランタイムのCgo部分、特にlibcgo_sys_thread_start関数は、GoのgoroutineがCコードを呼び出すために新しいOSスレッドを起動する際に使用されます。この関数内では、スレッドの初期化の一環として、シグナルマスクの操作が行われていました。具体的には、pthread_createを呼び出す前に一時的にすべてのシグナルをブロックし、pthread_createの呼び出し後に元のシグナルマスクに戻すという処理です。これは、スレッド作成中の競合状態や予期せぬシグナル配送を防ぐための一般的なプラクティスです。

問題は、Darwinのsigprocmask()がプロセス全体に作用するため、複数のlibcgo_sys_thread_startが同時に実行された場合、以下のようなシナリオでデッドロックが発生する可能性があったことです。

  1. スレッドAがsigprocmask(SIG_SETMASK, &ign, &oset)を呼び出し、プロセス全体のシグナルをブロックする。
  2. スレッドBがほぼ同時にsigprocmask(SIG_SETMASK, &ign, &oset)を呼び出し、同様にプロセス全体のシグナルをブロックする。
  3. スレッドAがpthread_createを呼び出し、その後sigprocmask(SIG_SETMASK, &oset, nil)を呼び出して元のシグナルマスクに戻す。
  4. この時、もしスレッドBが何らかの理由でsigprocmask(SIG_SETMASK, &oset, nil)を呼び出す前にクラッシュしたり、デッドロックに陥ったりすると、プロセス全体のシグナルマスクがブロックされたままになってしまう。

この状態になると、nilポインタ参照などによって発生するSIGSEGVのような重要なシグナルがプロセスに配送されても、それがブロックされてしまい、Goランタイムがクラッシュを検知・処理できなくなります。結果として、プログラムがハングアップしたり、予期せぬ動作をしたりする原因となっていました。

この修正では、sigprocmask()の代わりにpthread_sigmask()を使用することでこの問題を解決しています。pthread_sigmask()は、POSIXスレッド標準の一部であり、その名の通りスレッドローカルなシグナルマスクを操作することを意図しています。Darwinにおいても、pthread_sigmask()sigprocmask()とは異なり、呼び出し元のスレッドのシグナルマスクのみに影響を与えるため、プロセス全体に影響を与える競合状態を回避できます。これにより、複数のCgoスレッドが同時に起動されても、シグナルマスクの操作が安全に行われ、シグナルが永続的にブロックされることがなくなります。

また、このコミットには、このデッドロック問題を再現し、修正が正しく機能することを確認するための新しいテストケースTestCgoSignalDeadlockが追加されています。このテストは、多数のgoroutineとOSスレッドを起動し、意図的にnilポインタ参照を引き起こすことで、シグナルハンドリングが正しく機能するかどうかを検証します。

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

変更は主に以下の2つのファイルと1つのテストファイルにあります。

  1. src/pkg/runtime/cgo/gcc_darwin_386.c

    • libcgo_sys_thread_start 関数内で、sigprocmask(SIG_SETMASK, &ign, &oset);pthread_sigmask(SIG_SETMASK, &ign, &oset); に変更されました。
    • sigprocmask(SIG_SETMASK, &oset, nil);pthread_sigmask(SIG_SETMASK, &oset, nil); に変更されました。
  2. src/pkg/runtime/cgo/gcc_darwin_amd64.c

    • libcgo_sys_thread_start 関数内で、sigprocmask(SIG_SETMASK, &ign, &oset);pthread_sigmask(SIG_SETMASK, &ign, &oset); に変更されました。
    • sigprocmask(SIG_SETMASK, &oset, nil);pthread_sigmask(SIG_SETMASK, &oset, nil); に変更されました。
  3. src/pkg/runtime/crash_test.go

    • TestCgoSignalDeadlock という新しいテスト関数が追加されました。
    • このテスト関数内で使用される cgoSignalDeadlockSource という文字列定数が追加されました。これは、デッドロックを再現するためのGoプログラムのソースコードを含んでいます。

コアとなるコードの解説

変更の核心は、src/pkg/runtime/cgo/gcc_darwin_386.csrc/pkg/runtime/cgo/gcc_darwin_amd64.c の両方にある libcgo_sys_thread_start 関数内のシグナルマスク操作の変更です。

// 変更前 (例: gcc_darwin_386.c)
 libcgo_sys_thread_start(ThreadStart *ts)
 {
 	pthread_attr_t attr;
 	size_t size;
 	pthread_t p;
 	sigset_t ign, oset;
 	int err;

 	sigfillset(&ign);
-	sigprocmask(SIG_SETMASK, &ign, &oset); // ここが問題
+	pthread_sigmask(SIG_SETMASK, &ign, &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); // ここも問題
+	pthread_sigmask(SIG_SETMASK, &oset, nil); // 修正後

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

この変更により、sigprocmaskpthread_sigmask に置き換えられました。

  • sigfillset(&ign); は、ign シグナルセットにすべてのシグナルを追加します。
  • pthread_sigmask(SIG_SETMASK, &ign, &oset); は、現在のスレッドのシグナルマスクを ign(すべてのシグナル)に設定し、元のシグナルマスクを oset に保存します。これにより、pthread_create が呼び出される間、現在のスレッドはすべてのシグナルをブロックします。
  • pthread_create は新しいスレッドを作成します。
  • pthread_sigmask(SIG_SETMASK, &oset, nil); は、現在のスレッドのシグナルマスクを osetpthread_create 呼び出し前の元のシグナルマスク)に戻します。

この修正により、シグナルマスクの変更がスレッドローカルに限定されるため、他のCgoスレッドのシグナルマスク操作と競合することがなくなり、プロセス全体のシグナルが永続的にブロックされる問題が解消されました。

src/pkg/runtime/crash_test.go に追加された cgoSignalDeadlockSource は、この問題を再現するための巧妙なテストケースです。

  • runtime.GOMAXPROCS(100): 多数のOSスレッドが利用可能になるように設定します。
  • runtime.LockOSThread(): goroutineを特定のOSスレッドに固定します。これにより、CgoがOSスレッドを頻繁に作成・破棄する状況をシミュレートします。
  • var s *string; *s = "": 意図的にnilポインタ参照を引き起こし、SIGSEGVシグナルを発生させます。
  • recover(): パニックを捕捉し、プログラムがクラッシュしないようにします。これにより、シグナルが正しく配送され、ハンドリングされるかどうかがテストされます。
  • time.Sleepping チャネル: 複数のgoroutineとOSスレッドが同時に動作し、シグナル処理が競合する状況を作り出します。
  • fmt.Printf("HANG\\n"): デッドロックが発生した場合に"HANG"が出力されるようにし、テストが失敗することを示します。
  • fmt.Printf("OK\\n"): テストが成功した場合に"OK"が出力されるようにします。

このテストは、多数のOSスレッドを起動し、それらがシグナルマスクを操作するCgoのコードパスを通過する際に、nilポインタ参照によるシグナルが正しく処理されることを確認することで、デッドロックが解消されたことを検証します。

関連リンク

参考にした情報源リンク