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

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

コミット

commit 098b9dcf2f92823342fcddef9d606ea176062a63
Author: Joel Sing <jsing@google.com>
Date:   Tue Apr 10 21:57:05 2012 +1000

    runtime: block signals during thread creation on openbsd
    
    Block signals during thread creation, otherwise the new thread can
    receive a signal prior to initialisation completing.
    
    Fixes #3102.
    
    R=golang-dev, rsc, devon.odell, minux.ma
    CC=golang-dev
    https://golang.org/cl/5757064

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

https://github.com/golang/go/commit/098b9dcf2f92823342fcddef9d606ea176062a63

元コミット内容

このコミットは、GoランタイムがOpenBSD上でスレッドを作成する際に、シグナルをブロックするように変更するものです。これにより、新しいスレッドが初期化が完了する前にシグナルを受信してしまう問題を解決します。具体的には、runtime·newosproc関数内で新しいOSスレッドを作成する際に、一時的にすべてのシグナルをブロックし、スレッド作成後に元のシグナルマスクに戻す処理が追加されています。また、runtime·minit関数でも初期化時にシグナルマスクをクリアする処理が追加されています。

変更の背景

この変更の背景には、OpenBSD環境におけるGoランタイムのスレッド作成時の競合状態があります。Goランタイムは、OSのスレッド(OSスレッド)を抽象化してGoルーチンを管理しています。新しいOSスレッドが作成される際、そのスレッドは完全に初期化される前に、OSからシグナル(例えば、タイマーシグナルやI/O関連のシグナルなど)を受信する可能性があります。

もしスレッドが初期化途中でシグナルを受信した場合、そのシグナルハンドラが未初期化の状態のスレッドコンテキストで実行されることになり、予期せぬ動作、クラッシュ、またはデッドロックなどの不安定な状態を引き起こす可能性がありました。特に、GoランタイムはシグナルをGoルーチンにディスパッチするために独自のシグナルハンドリングメカニズムを持っていますが、新しいスレッドがこのメカニズムに完全に組み込まれる前にシグナルが届くと問題が発生します。

この問題は、GoのIssue #3102として報告されており、このコミットはその問題を修正するために導入されました。

前提知識の解説

シグナル (Signals)

UNIX系OSにおけるシグナルは、プロセス間通信や非同期イベント通知のためのソフトウェア割り込みメカニズムです。OSや他のプロセスから特定のイベント(例: 割り込み、セグメンテーション違反、子プロセスの終了など)が発生したことをプロセスに通知するために使用されます。プロセスはシグナルを受信すると、デフォルトの動作を実行するか、事前に登録されたシグナルハンドラ関数を実行します。

シグナルマスク (Signal Mask)

シグナルマスクは、プロセスが現在ブロックしている(受信を一時的に保留している)シグナルの集合です。シグナルがブロックされている間は、そのシグナルが発生してもプロセスに配信されず、シグナルマスクから解除されるまで保留されます。sigprocmaskシステムコールは、このシグナルマスクを操作するために使用されます。

sigprocmask システムコール

sigprocmaskは、プロセスのシグナルマスクを検査または変更するために使用されるシステムコールです。

  • how: シグナルマスクの変更方法を指定します。
    • SIG_BLOCK: 現在のシグナルマスクにsetで指定されたシグナルを追加します。
    • SIG_UNBLOCK: 現在のシグナルマスクからsetで指定されたシグナルを削除します。
    • SIG_SETMASK: 現在のシグナルマスクをsetで指定されたシグナルに置き換えます。
  • set: 変更するシグナルの集合を指定します。
  • oset: 変更前のシグナルマスクが格納されます(NULLの場合もある)。

rfork_thread システムコール (OpenBSD特有)

rfork_threadはOpenBSDに特有のシステムコールで、新しいスレッドを作成するために使用されます。これは、一般的なUNIX系OSのforkcloneに似ていますが、スレッド作成に特化しており、より軽量なプロセス(スレッド)の作成を可能にします。Goランタイムは、OSスレッドを生成する際にこのシステムコールを利用しています。

GoランタイムのMとG (GoroutineとMachine)

Goランタイムは、Goルーチン(G)をOSスレッド(M: Machine)上で実行します。MはOSスレッドを表し、GはGoの軽量な並行処理単位であるGoルーチンを表します。Goランタイムは、MとGを効率的にスケジューリングし、マルチコアプロセッサを最大限に活用します。新しいOSスレッド(M)が作成される際には、そのスレッドがGoランタイムのスケジューラに組み込まれ、Goルーチンを実行できる状態になる必要があります。

技術的詳細

このコミットの主要な目的は、OpenBSD上で新しいOSスレッドが作成される際に発生するシグナル関連の競合状態を解消することです。具体的には、runtime·newosproc関数内でrfork_threadシステムコールを呼び出す前後にsigprocmaskシステムコールを使用してシグナルマスクを操作します。

  1. runtime·newosproc関数:

    • この関数は、Goランタイムが新しいOSスレッド(M)を起動する際に呼び出されます。
    • 変更前は、直接runtime·rfork_threadを呼び出していましたが、変更後はその呼び出しの前後でシグナルマスクを操作するようになりました。
    • oset = runtime·sigprocmask(SIG_SETMASK, sigset_all);
      • sigset_allはすべてのシグナルを含むシグナルセットです。
      • SIG_SETMASKオプションを使用することで、現在のスレッドのシグナルマスクを一時的にすべてのシグナルをブロックする状態に設定します。これにより、rfork_threadが新しいスレッドを作成する際に、その新しいスレッドが初期化中にシグナルを受信するのを防ぎます。
      • 変更前のシグナルマスクはosetに保存されます。
    • ret = runtime·rfork_thread(flags, stk, m, g, fn);
      • ここで新しいOSスレッドが作成されます。この間、シグナルはブロックされています。
    • runtime·sigprocmask(SIG_SETMASK, oset);
      • 新しいスレッドの作成が完了した後、保存しておいたoset(元のシグナルマスク)を復元します。これにより、スレッドは通常のシグナル処理に戻ります。
  2. runtime·minit関数:

    • この関数は、GoランタイムのM(OSスレッド)が初期化される際に呼び出されます。
    • runtime·sigprocmask(SIG_SETMASK, sigset_none);
      • sigset_noneは空のシグナルセットです。
      • この行は、Mの初期化が完了した時点で、そのMのシグナルマスクをクリアし、すべてのシグナルを受信できる状態に戻すことを保証します。これは、newosprocで一時的にブロックされたシグナルマスクが、Mの初期化プロセス全体を通じて適切に管理されることを意味します。
  3. src/pkg/runtime/os_openbsd.h:

    • SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASKといったsigprocmaskで使用される定数が追加されました。
    • runtime·sigprocmask関数のプロトタイプ宣言が追加されました。
  4. src/pkg/runtime/sys_openbsd_386.s および src/pkg/runtime/sys_openbsd_amd64.s:

    • runtime·sigprocmaskのシステムコールラッパーがアセンブリ言語で実装されました。これは、Goランタイムが直接OSのsigprocmaskシステムコールを呼び出すためのインターフェースを提供します。
  5. src/pkg/runtime/signal_openbsd_amd64.c:

    • runtime·setsig関数内のsa.sa_maskの型が~0ULLから~0Uに変更されました。これは、シグナルマスクのビット幅に関する修正で、OpenBSDのシグナルセットのサイズに合わせたものです。
    • runtime·sighandler関数内のコメントで参照されているレジスタ名がr->mc_ripからr->sc_ripに修正されました。これは、コンテキスト構造体内のプログラムカウンタ(Instruction Pointer)を指すフィールド名の修正です。

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

src/pkg/runtime/thread_openbsd.c

--- a/src/pkg/runtime/thread_openbsd.c
+++ b/src/pkg/runtime/thread_openbsd.c
@@ -20,6 +20,9 @@ enum
 
 extern SigTab runtime·sigtab[];
 
+static Sigset sigset_all = ~(Sigset)0;
+static Sigset sigset_none;
+
 extern int64 runtime·rfork_thread(int32 flags, void *stack, M *m, G *g, void (*fn)(void));
 extern int32 runtime·thrsleep(void *ident, int32 clock_id, void *tsp, void *lock);\n extern int32 runtime·thrwakeup(void *ident, int32 n);\n@@ -128,6 +131,7 @@ runtime·semawakeup(M *mp)\n void\n runtime·newosproc(M *m, G *g, void *stk, void (*fn)(void))\n {\n+\tSigset oset;\n  \tint32 flags;\n  \tint32 ret;\n  \n@@ -141,7 +145,11 @@ runtime·newosproc(M *m, G *g, void *stk, void (*fn)(void))\n  \tm->tls[0] = m->id;\t// so 386 asm can find it\n  \n-\tif((ret = runtime·rfork_thread(flags, stk, m, g, fn)) < 0) {\n+\toset = runtime·sigprocmask(SIG_SETMASK, sigset_all);\n+\tret = runtime·rfork_thread(flags, stk, m, g, fn);\n+\truntime·sigprocmask(SIG_SETMASK, oset);\n+\n+\tif(ret < 0) {\n  \t\truntime·printf(\"runtime: failed to create new OS thread (have %d already; errno=%d)\\n\", runtime·mcount() - 1, -ret);\n  \t\tif (ret == -ENOTSUP)\n  \t\t\truntime·printf(\"runtime: is kern.rthreads disabled?\\n\");\n@@ -168,6 +176,7 @@ runtime·minit(void)\n  \t// Initialize signal handling\n  \tm->gsignal = runtime·malg(32*1024);\n  \truntime·signalstack(m->gsignal->stackguard - StackGuard, 32*1024);\n+\truntime·sigprocmask(SIG_SETMASK, sigset_none);\n }\n \n void\n```

### `src/pkg/runtime/os_openbsd.h`

```diff
--- a/src/pkg/runtime/os_openbsd.h
+++ b/src/pkg/runtime/os_openbsd.h
@@ -5,17 +5,22 @@
 #define SIG_DFL ((void*)0)\n #define SIG_IGN ((void*)1)\n \n+#define SIG_BLOCK 1\n+#define SIG_UNBLOCK 2\n+#define SIG_SETMASK 3\n+\n struct sigaction;\n \n+void\truntime·raisesigpipe(void);\n+void\truntime·setsig(int32, void(*)(int32, Siginfo*, void*, G*), bool);\n void\truntime·sigpanic(void);\n-void\truntime·sigaltstack(Sigaltstack*, Sigaltstack*);\n+\n+void\truntime·setitimer(int32, Itimerval*, Itimerval*);\n void\truntime·sigaction(int32, struct sigaction*, struct sigaction*);\n-void\truntime·setsig(int32, void(*)(int32, Siginfo*, void*, G*), bool);\n+void\truntime·sigaltstack(Sigaltstack*, Sigaltstack*);\n void\truntime·sighandler(int32 sig, Siginfo *info, void *context, G *gp);\n-void\truntime·setitimer(int32, Itimerval*, Itimerval*);\n+Sigset\truntime·sigprocmask(int32, Sigset);\n int32\truntime·sysctl(uint32*, uint32, byte*, uintptr*, byte*, uintptr);\n \n-void\truntime·raisesigpipe(void);\n-\n #define\tNSIG 33\n #define\tSI_USER\t0

コアとなるコードの解説

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

  • sigset_allsigset_none の追加:
    • static Sigset sigset_all = ~(Sigset)0; は、すべてのシグナルをブロックするためのシグナルセットを初期化します。~(Sigset)0 は、Sigset型のすべてのビットを1に設定することを意味し、これによりすべてのシグナルがマスクされます。
    • static Sigset sigset_none; は、シグナルをブロックしない(すべてのシグナルを受信する)ための空のシグナルセットを宣言します。これはデフォルトでゼロ初期化されます。
  • runtime·newosproc 関数内のシグナルブロックロジック:
    • Sigset oset; で、元のシグナルマスクを保存するための変数を宣言します。
    • oset = runtime·sigprocmask(SIG_SETMASK, sigset_all);
      • runtime·rfork_threadを呼び出す直前に、現在のスレッドのシグナルマスクをsigset_all(すべてのシグナルをブロック)に設定します。これにより、新しいスレッドが作成される際に、そのスレッドが初期化が完了する前にシグナルを受信するのを防ぎます。
      • この呼び出しは、変更前のシグナルマスクをosetに返します。
    • ret = runtime·rfork_thread(flags, stk, m, g, fn);
      • 新しいOSスレッドが作成されます。この間、シグナルはブロックされた状態です。
    • runtime·sigprocmask(SIG_SETMASK, oset);
      • runtime·rfork_threadの呼び出し後、保存しておいたoset(元のシグナルマスク)を復元します。これにより、スレッドは通常のシグナル処理に戻ります。
  • runtime·minit 関数内のシグナルマスククリア:
    • runtime·sigprocmask(SIG_SETMASK, sigset_none);
      • M(OSスレッド)の初期化が完了した時点で、そのMのシグナルマスクをsigset_none(シグナルをブロックしない)に設定します。これは、MがGoルーチンを実行する準備ができたときに、すべてのシグナルを適切に処理できるようにするための最終的なクリーンアップステップです。

src/pkg/runtime/os_openbsd.h の変更

  • SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK の定義:
    • これらはsigprocmaskシステムコールで使用される定数で、シグナルマスクの操作方法を指定します。Goランタイムがこれらの定数を内部的に使用できるように定義されています。
  • Sigset runtime·sigprocmask(int32, Sigset); のプロトタイプ宣言:
    • runtime·sigprocmask関数がGoランタイム内で利用可能であることを宣言しています。この関数は、OpenBSDのsigprocmaskシステムコールへのラッパーとして機能します。

src/pkg/runtime/sys_openbsd_386.s および src/pkg/runtime/sys_openbsd_amd64.s の変更

  • TEXT runtime·sigprocmask(SB),7,$-4 (386) および TEXT runtime·sigprocmask(SB),7,$0 (amd64) の追加:
    • これらのアセンブリコードは、GoランタイムがOpenBSDのsigprocmaskシステムコールを呼び出すための低レベルなインターフェースを提供します。
    • システムコール番号(OpenBSDでは48sys_sigprocmaskに対応)をレジスタにロードし、INT $0x80 (386) または SYSCALL (amd64) 命令を使用してシステムコールを実行します。
    • システムコールの引数(howset)はスタックまたはレジスタ経由で渡され、戻り値(変更前のシグナルマスク)はAXレジスタから取得され、呼び出し元のスタックフレームに格納されます。

これらの変更により、GoランタイムはOpenBSD上でスレッドを作成する際のシグナル関連の競合状態を効果的に回避し、より堅牢な動作を実現しています。

関連リンク

参考にした情報源リンク