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

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

このコミットは、Goランタイムのcgo(CgoはGoプログラムがC言語のコードを呼び出すためのメカニズム)における、macOS (Darwin) 環境でのシグナルマスキングに関するバグ修正を目的としています。具体的には、pthread_create 呼び出しの前後でシグナルマスクを適切に設定し、スレッド作成時の競合状態や予期せぬシグナルハンドリングの問題を回避します。

コミット

commit 1fc9a17c7ea276ee80045dc8cc9411eb024cf8ea
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Wed Mar 14 12:49:59 2012 +0900

    runtime/cgo: darwin signal masking
    
    Fixes #3101 (again).
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5825043

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

https://github.com/golang/go/commit/1fc9a17c7ea276ee80045dc8cc9411eb024cf8ea

元コミット内容

このコミットは、runtime/cgo ディレクトリ内の gcc_darwin_386.c および gcc_darwin_amd64.c ファイルに対して行われた変更です。主な変更内容は、libcgo_sys_thread_start 関数内で pthread_create を呼び出す前後にシグナルマスクを操作するコードを追加することです。

具体的には、以下の行が追加されています。

  • sigset_t ign, oset;
  • sigfillset(&ign);
  • sigprocmask(SIG_SETMASK, &ign, &oset);
  • sigprocmask(SIG_SETMASK, &oset, nil);

これらの変更は、pthread_create が新しいスレッドを作成する際に、親スレッドのシグナルマスクが子スレッドに継承されるというPOSIXの動作を考慮し、予期せぬシグナルが新しいスレッドに配送されるのを防ぐためのものです。

変更の背景

このコミットは「Fixes #3101 (again)」と記載されており、以前にも同様の問題が報告され、修正が試みられたものの、完全に解決されていなかったことを示唆しています。Goのcgoメカニズムは、GoランタイムとCライブラリ間の相互運用を可能にしますが、異なる言語のランタイムが混在する環境では、シグナルハンドリングのような低レベルのシステムコールが複雑な問題を引き起こすことがあります。

特に、macOS (Darwin) 環境では、シグナル配送のセマンティクスが他のUnix系システムと微妙に異なる場合があり、pthread_create が新しいスレッドを作成する際に、親スレッドのシグナルマスクが子スレッドに継承されることが問題の原因となることがあります。もし親スレッドが特定のシグナルをブロックしている場合、新しく作成されたスレッドもそのシグナルをブロックした状態で開始され、予期せぬ動作やデッドロックを引き起こす可能性があります。

この修正は、pthread_create の呼び出し中にすべてのシグナルを一時的にブロックし、スレッド作成後に元のシグナルマスクに戻すことで、この問題を解決しようとしています。これにより、新しいスレッドがクリーンなシグナルマスクで開始され、シグナル関連の競合状態が回避されます。

前提知識の解説

Cgo

Cgoは、GoプログラムがC言語のコードを呼び出すためのGoの機能です。GoのコードからCの関数を呼び出したり、CのコードからGoの関数を呼び出したりすることができます。これは、既存のCライブラリを利用したり、パフォーマンスが重要な部分をCで記述したりする場合に非常に便利です。しかし、GoとCのランタイムが混在するため、メモリ管理、スレッド、シグナルハンドリングなど、低レベルのシステムインタラクションにおいて複雑な問題が発生することがあります。

POSIXスレッド (pthreads)

POSIXスレッド(pthreads)は、Unix系オペレーティングシステムにおけるスレッドAPIの標準です。pthread_create は新しいスレッドを作成するための関数で、pthread_attr_t 構造体を使ってスレッドの属性(スタックサイズ、スケジューリングポリシーなど)を設定できます。重要な点として、POSIXスレッドの仕様では、新しく作成されたスレッドは親スレッドのシグナルマスクを継承すると定められています。

シグナルとシグナルマスキング

シグナルは、Unix系システムにおいてプロセス間通信やイベント通知のために使用されるソフトウェア割り込みの一種です。例えば、Ctrl+Cを押すと SIGINT シグナルがプロセスに送られ、プロセスを終了させることができます。

シグナルマスクとは、プロセスまたはスレッドが現在ブロックしている(受け取らない)シグナルの集合です。シグナルがブロックされている間は、そのシグナルが配送されてもすぐに処理されず、ブロックが解除されるまで保留されます。

  • sigset_t: シグナルセットを表すデータ型。
  • sigfillset(sigset_t *set): set が指すシグナルセットにすべてのシグナルを追加します。つまり、すべてのシグナルをブロックするように設定します。
  • sigprocmask(int how, const sigset_t *set, sigset_t *oldset): プロセスまたはスレッドのシグナルマスクを変更します。
    • how: シグナルマスクの変更方法を指定します。
      • SIG_SETMASK: set で指定されたシグナルセットにシグナルマスクを設定します。
      • SIG_BLOCK: set で指定されたシグナルを現在のシグナルマスクに追加します。
      • SIG_UNBLOCK: set で指定されたシグナルを現在のシグナルマスクから削除します。
    • set: 新しいシグナルマスクまたは追加/削除するシグナルを指定するシグナルセットへのポインタ。
    • oldset: 変更前のシグナルマスクを保存するためのシグナルセットへのポインタ。元のシグナルマスクを保存しておき、後で復元するために使用されます。

Darwin (macOS) 特有の考慮事項

macOS (Darwin) はBSDベースのUnix系OSであり、そのカーネル(XNU)はMachマイクロカーネルとBSD層のハイブリッドです。シグナルハンドリングの動作はPOSIX標準に準拠していますが、実装の詳細や特定の条件下での挙動が他のLinuxなどのシステムと異なる場合があります。特に、スレッド作成時のシグナルマスクの継承は、競合状態やデッドロックの潜在的な原因となることがあります。

技術的詳細

このコミットの技術的詳細を掘り下げると、libcgo_sys_thread_start 関数は、GoランタイムがCgoを介して新しいOSスレッドを作成する際に呼び出される関数です。この関数内で pthread_create が使用され、新しいスレッドが生成されます。

問題は、pthread_create が呼び出される際に、呼び出し元のスレッド(親スレッド)のシグナルマスクが新しいスレッド(子スレッド)に継承されるというPOSIXのセマンティクスにあります。もし親スレッドが特定のシグナルをブロックしている場合、子スレッドもそのシグナルをブロックした状態で開始されます。

このコミットの修正は、以下のステップでこの問題を解決します。

  1. 現在のシグナルマスクの保存: sigprocmask(SIG_SETMASK, &ign, &oset); の呼び出しで、pthread_create を呼び出す直前の現在のスレッドのシグナルマスクが oset に保存されます。
  2. すべてのシグナルのブロック: sigfillset(&ign);ign シグナルセットにすべてのシグナルが設定され、その後に sigprocmask(SIG_SETMASK, &ign, &oset); が呼び出されることで、pthread_create の呼び出し中はすべてのシグナルがブロックされます。これにより、新しいスレッドが作成される際に、親スレッドが一時的にすべてのシグナルをブロックしている状態となり、子スレッドもすべてのシグナルをブロックした状態で開始されます。
  3. スレッドの作成: err = pthread_create(&p, &attr, threadentry, ts); が実行され、新しいスレッドが作成されます。
  4. 元のシグナルマスクの復元: sigprocmask(SIG_SETMASK, &oset, nil); が呼び出されることで、pthread_create の呼び出し前に保存しておいた元のシグナルマスクが復元されます。これにより、親スレッドは通常のシグナルハンドリングの状態に戻ります。

この一連の操作により、pthread_create が実行されている間、親スレッドはシグナルによって中断されることなく、新しいスレッドが作成される際のシグナルマスクの継承も制御されます。新しいスレッドは、親スレッドが一時的にすべてのシグナルをブロックしている状態で作成されるため、予期せぬシグナルが新しいスレッドに配送されることを防ぎ、より予測可能な動作を保証します。

この修正は、特にGoランタイムがCgoを介して外部のCライブラリと連携し、それらのライブラリが独自のスレッドを作成したり、シグナルハンドリングを行ったりする場合に重要です。シグナルマスクの不適切な継承は、デッドロック、クラッシュ、または予期せぬプログラムの終了につながる可能性があります。

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

src/pkg/runtime/cgo/gcc_darwin_386.c および src/pkg/runtime/cgo/gcc_darwin_amd64.clibcgo_sys_thread_start 関数。

--- a/src/pkg/runtime/cgo/gcc_darwin_386.c
+++ b/src/pkg/runtime/cgo/gcc_darwin_386.c
@@ -4,6 +4,7 @@
 
  #include <string.h> /* for strerror */
  #include <pthread.h>
 +#include <signal.h>
  #include "libcgo.h"
 
  static void* threadentry(void*);
@@ -120,14 +121,21 @@ 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);
+	sigprocmask(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);
+
  	if (err != 0) {
  		fprintf(stderr, "runtime/cgo: pthread_create failed: %s\n", strerror(err));
  		abort();

gcc_darwin_amd64.c も同様の変更が適用されています。

コアとなるコードの解説

追加されたコードは、pthread_create の呼び出しをシグナルマスクの操作で囲んでいます。

  1. #include <signal.h>: sigset_t, sigfillset, sigprocmask などのシグナル関連の関数や型を使用するために必要なヘッダーファイルです。
  2. sigset_t ign, oset;: ign はすべてのシグナルを含むシグナルセットとして使用され、osetsigprocmask 呼び出し前の元のシグナルマスクを保存するために使用されます。
  3. sigfillset(&ign);: ign シグナルセットにすべての標準シグナルを追加します。これにより、ign は「すべてのシグナルをブロックする」という意図を表すセットになります。
  4. sigprocmask(SIG_SETMASK, &ign, &oset);:
    • SIG_SETMASK: シグナルマスクを &ign で指定されたセットに設定します。
    • &ign: すべてのシグナルをブロックするように設定します。
    • &oset: この呼び出しが実行される前の現在のシグナルマスクを oset に保存します。 この行により、pthread_create が呼び出される間、現在のスレッドはすべてのシグナルをブロックする状態になります。
  5. err = pthread_create(&p, &attr, threadentry, ts);: 新しいスレッドを作成します。この時点で、親スレッドはすべてのシグナルをブロックしているため、子スレッドもそのシグナルマスクを継承します。
  6. sigprocmask(SIG_SETMASK, &oset, nil);:
    • SIG_SETMASK: シグナルマスクを &oset で指定されたセットに設定します。
    • &oset: pthread_create 呼び出し前に保存しておいた元のシグナルマスクを復元します。
    • nil: 復元後のシグナルマスクを保存する必要がないため nil を指定します。 この行により、親スレッドのシグナルマスクが元の状態に戻されます。

このシーケンスは、pthread_create の呼び出し中にシグナルが配送されることによる競合状態を防ぎ、また、新しく作成されるスレッドが予期せぬシグナルマスクを継承することを防ぐための、堅牢な方法を提供します。

関連リンク

  • Go issue #3101 (元の問題報告): このコミットが修正しようとしている具体的な問題の詳細が記載されているはずです。GoのIssue Trackerで検索することで見つかる可能性があります。
  • Go CL 5825043: このコミットに対応するGoのコードレビューシステム (Gerrit) のチェンジリストです。より詳細な議論や以前の修正試行に関する情報が含まれている可能性があります。

参考にした情報源リンク

  • POSIX pthread_create man page: スレッド作成時のシグナルマスク継承に関する詳細。
  • POSIX sigprocmask man page: シグナルマスク操作に関する詳細。
  • Go言語のcgoに関するドキュメント: cgoの仕組みと、C言語との相互運用における注意点。
  • Darwin (macOS) のシグナルハンドリングに関するシステムプログラミングガイド。
  • 以前のGoのコミット履歴や関連するバグ報告: 「(again)」という記述から、過去に同様の問題が議論された形跡があるため、それらの情報も参考になります。
  • https://golang.org/cl/5825043 (Go Code Review)
  • https://github.com/golang/go/commit/1fc9a17c7ea276ee80045dc8cc9411eb024cf8ea (GitHub Commit)