[インデックス 14754] ファイルの概要
このコミットは、Goランタイムにおけるシグナルキュー(sigqueue
)処理における潜在的なクラッシュを修正するものです。具体的には、シグナルハンドラとシグナル処理ゴルーチン間の同期メカニズムを改善し、競合状態による不安定性を解消しています。この修正は、Goのシグナル処理の堅牢性と信頼性を向上させることを目的としています。
コミット
commit 91484c6c4861b56c77702d0d9ccb9315192bb0e4
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Fri Dec 28 15:36:06 2012 +0400
runtime: fix potential crash in sigqueue
Fixes #4383.
R=golang-dev, minux.ma, rsc, iant
CC=golang-dev
https://golang.org/cl/6996060
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/91484c6c4861b56c77702d0d9ccb9315192bb0e4
元コミット内容
runtime: fix potential crash in sigqueue
Fixes #4383.
このコミットは、Goランタイムのシグナルキュー処理における潜在的なクラッシュを修正します。Issue #4383を解決します。
変更の背景
Goプログラムは、オペレーティングシステムからのシグナル(例: SIGINT
、SIGTERM
、SIGUSR1
など)を処理する必要があります。これらのシグナルは、プログラムの実行を中断したり、特定のイベントを通知したりするために使用されます。Goランタイムは、これらのシグナルを捕捉し、Goのチャネルを通じてユーザーゴルーチンに配信するメカニズムを提供しています。
しかし、シグナルハンドラは非常に特殊な実行コンテキストで動作します。シグナルハンドラ内では、通常のGoコードが実行される環境とは異なり、多くの制約があります。例えば、シグナルハンドラはブロックしてはならず、ロックを使用できず、メモリを割り当てることもできません。これは、シグナルハンドラが非同期に、かつ予測不能なタイミングで任意のゴルーチンの実行中に割り込む可能性があるためです。もしシグナルハンドラがブロックしたり、ロックを保持したりすると、デッドロックや他の深刻な問題を引き起こす可能性があります。
このコミットが修正しようとしている問題は、sigqueue
と呼ばれるGoランタイムの内部コンポーネントにおける潜在的なクラッシュです。sigqueue
は、シグナルハンドラが捕捉したシグナルを、安全な方法で通常のGoゴルーチンに引き渡すためのキューイングメカニズムを提供します。以前の実装では、このシグナルハンドラとシグナル処理ゴルーチン間の同期ロジックに競合状態(race condition)が存在し、特定の条件下でクラッシュを引き起こす可能性がありました。Issue #4383は、この不安定性を報告したものです。
具体的には、複数のシグナルが同時に発生したり、シグナル処理ゴルーチンがシグナルを消費している最中に新しいシグナルが到着したりするような、高負荷な状況下で同期の不整合が発生し、ランタイムが予期せぬ状態に陥る可能性がありました。このコミットは、この同期メカニズムをより堅牢な状態ベースのアプローチに置き換えることで、このクラッシュの可能性を排除します。
前提知識の解説
Goランタイムにおけるシグナルハンドリング
GoプログラムがOSシグナルを処理する際、OSはまずGoランタイムが設定したシグナルハンドラを呼び出します。このシグナルハンドラはC言語で書かれており、非常に限られた操作しかできません。その主な役割は、受け取ったシグナルを内部キュー(sigqueue
)に安全に格納し、その後、Goのスケジューラが管理する通常のゴルーチン(シグナル処理ゴルーチン)にそのシグナルを処理させることです。これにより、シグナルハンドラの制約を回避しつつ、Goの並行処理モデルにシグナル処理を統合できます。
sigqueue
の役割
sigqueue
は、シグナルハンドラとシグナル処理ゴルーチン間のバッファとして機能します。シグナルハンドラは、受け取ったシグナルをsigqueue
に「送信」し、シグナル処理ゴルーチンはsigqueue
からシグナルを「受信」します。このキューイングメカニズムは、シグナルハンドラがOSからシグナルを受け取った瞬間に、すぐにその処理を終えてOSに制御を戻せるようにするために不可欠です。
Note
とnotewakeup
/notesleep
によるゴルーチン間の同期
Goランタイムには、Note
という低レベルの同期プリミティブがあります。これは、ゴルーチンが特定のイベントを待機したり、他のゴルーチンにイベントの発生を通知したりするために使用されます。
runtime·notesleep(¬e)
: 現在のゴルーチンをnote
上でスリープさせます。runtime·notewakeup(¬e)
:note
上でスリープしているゴルーチンをウェイクアップさせます。runtime·noteclear(¬e)
:note
の状態をクリアします。
これらは、ミューテックスやチャネルのような高レベルの同期メカニズムが利用できない、ランタイムの非常に低レベルな部分(特にシグナルハンドラのようなコンテキスト)で、ゴルーチン間のシンプルな通知メカニズムとして利用されます。
runtime·cas
(Compare-and-Swap)
runtime·cas
は、アトミックな比較と交換(Compare-and-Swap)操作を実行する関数です。これは、マルチスレッド環境で共有メモリ上のデータを安全に更新するために不可欠なプリミティブです。
runtime·cas(&addr, old, new)
は、addr
が指すメモリ位置の値がold
と等しい場合にのみ、その値をnew
に更新します。この操作全体はアトミック(不可分)であり、他のスレッドからの干渉なしに実行されることが保証されます。これにより、ロックを使用せずに競合状態を回避し、データの整合性を保つことができます。
シグナルハンドラ内での制約
前述の通り、シグナルハンドラは非常に制約の多い環境で実行されます。
- ブロック不可: シグナルハンドラは、I/O操作やロックの取得など、ブロックする可能性のある操作を実行してはなりません。
- ロック不可: シグナルハンドラは、ミューテックスなどのロックを取得してはなりません。これは、シグナルがロックを保持しているゴルーチンに割り込んだ場合、デッドロックを引き起こす可能性があるためです。
- メモリ割り当て不可: シグナルハンドラは、ヒープメモリを割り当ててはなりません。メモリ割り当ては、内部的にロックを必要とする場合があり、また、メモリ割り当て中にシグナルが割り込むと、ヒープが破損する可能性があります。
これらの制約のため、シグナルハンドラは非常にシンプルな操作(例: アトミックな変数更新、キューへのデータ追加)に限定され、複雑な処理は通常のゴルーチンに委譲する必要があります。
技術的詳細
このコミットの核心は、src/pkg/runtime/sigqueue.goc
におけるシグナルハンドラ(runtime·sigsend
)とシグナル処理ゴルーチン(signal_recv
)間の同期メカニズムの変更です。
旧来の同期メカニズムの問題点
変更前のsigqueue.goc
のコメントには、sig.Note
の所有権がシグナルハンドラとシグナルゴルーチンの間で「行き来する」という複雑な同期モデルが記述されていました。このモデルは、sig.mask
(受信したシグナルをビットマスクで表現)とsig.kick
(ウェイクアップが必要かどうかを示すフラグ)を使用していました。
旧モデルの概要:
- シグナルハンドラが
sig.mask
をゼロから非ゼロに変更する際にnotewakeup(&sig)
を呼び出す。 signal_recv
はnotesleep(&sig)
でウェイクアップを待つ。- ウェイクアップ後、
signal_recv
はnoteclear(&sig)
で次のラウンドに備える。 signal_recv
はcas
でsig.mask
を取得しゼロにする。
このモデルは、特にsig.kick
フラグの管理において、複数の並行するsigsend
呼び出しやsignal_recv
の再チェックとの間で競合状態を引き起こす可能性がありました。例えば、signal_recv
がsig.kick
をチェックし、noteclear
を呼び出した直後に、別のシグナルが到着してsigsend
がsig.kick
をチェックし、ウェイクアップが必要ないと判断してしまうようなタイミングの問題が発生し得ました。これにより、シグナルが失われたり、signal_recv
がデッドロックしたりする「潜在的なクラッシュ」につながる可能性がありました。
新しい同期メカニズム: sig.state
ベースのアプローチ
新しいアプローチでは、sig.kick
フラグを廃止し、代わりにsig.state
という単一のuint32
変数を使用して、シグナルハンドラとシグナル処理ゴルーチン間の同期状態を管理します。sig.state
は以下の3つの状態を取ります。
0
: シグナル処理ゴルーチンはブロックされておらず、新しい保留中のシグナルもありません。HASWAITER
(1
): シグナル処理ゴルーチン(signal_recv()
)がsig.Note
上でブロックされており、新しい保留中のシグナルはありません。HASSIGNAL
(2
):sig.mask
に新しい保留中のシグナルが含まれている可能性があり、シグナル処理ゴルーチンはこの状態ではブロックされていません。
これらの状態間の遷移は、すべてruntime·cas
(Compare-and-Swap)操作によってアトミックに行われます。これにより、複数のシグナルハンドラやシグナル処理ゴルーチンが同時に動作しても、状態の整合性が保たれ、競合状態が回避されます。
runtime·sigsend(int32 s)
の変更点:
シグナルハンドラから呼び出されるruntime·sigsend
は、新しいシグナルをキューに入れる際にsig.state
を更新します。
- まず、受け取ったシグナル
s
をsig.mask
にアトミックに追加します。 - その後、
for(;;)
ループ内でsig.state
をアトミックに読み込み(runtime·atomicload
)、その値に基づいて新しい状態を決定し、runtime·cas
で更新を試みます。- もし
sig.state
がHASSIGNAL
であれば、すでにシグナルがあることを示しているので、ウェイクアップは不要でループを抜けます。 - もし
sig.state
がHASWAITER
であれば、signal_recv
が待機しているので、sig.state
を0
に遷移させ、runtime·notewakeup(&sig)
を呼び出してsignal_recv
をウェイクアップします。 - もし
sig.state
が0
であれば、HASSIGNAL
に遷移させます(ウェイクアップは不要)。
- もし
runtime·cas
が成功するまでこのループを繰り返します。
このロジックにより、signal_recv
が待機している場合にのみウェイクアップがトリガーされ、不要なウェイクアップやウェイクアップの取りこぼしがなくなります。
signal_recv()
の変更点:
シグナル処理ゴルーチンから呼び出されるsignal_recv
は、シグナルを消費する際にsig.state
を更新します。
- まず、
sig.mask
から利用可能なシグナルをローカルコピーrecv
に移動します。 - その後、
for(;;)
ループ内でsig.state
をアトミックに読み込み、その値に基づいて新しい状態を決定し、runtime·cas
で更新を試みます。- もし
sig.state
がHASWAITER
であれば、これは矛盾した状態であり、ランタイムエラー(runtime·throw
)を発生させます。signal_recv
が待機しているはずなのに、なぜかHASWAITER
状態になっているのはロジックの誤りを示唆します。 - もし
sig.state
がHASSIGNAL
であれば、シグナルがあることを示しているので、0
に遷移させます。 - もし
sig.state
が0
であれば、シグナルがないことを示しているので、HASWAITER
に遷移させ、runtime·entersyscall()
、runtime·notesleep(&sig)
、runtime·exitsyscall()
を呼び出してスリープします。スリープから戻った後、runtime·noteclear(&sig)
を呼び出してNote
の状態をクリアします。
- もし
runtime·cas
が成功するまでこのループを繰り返します。
この新しい状態ベースの同期メカニズムは、より明確で堅牢な状態遷移を保証し、シグナルハンドラとシグナル処理ゴルーチン間の競合状態を効果的に排除します。
TestStress
の追加
src/pkg/os/signal/signal_test.go
にTestStress
という新しいテストが追加されました。このテストは、シグナル処理メカニズムに意図的に高い負荷をかけることで、潜在的な競合状態やデッドロックを検出することを目的としています。
TestStress
の動作:
- 複数のゴルーチンを起動します(
GOMAXPROCS
を4に設定)。 - 1つのゴルーチンは
os.Signal
チャネルでsyscall.SIGUSR1
シグナルを待ち受けます(Notify(sig, syscall.SIGUSR1)
)。 - もう1つのゴルーチンは、ループ内で自身のプロセスに
syscall.SIGUSR1
シグナルを繰り返し送信します(syscall.Kill(syscall.Getpid(), syscall.SIGUSR1)
)。 - シグナル送信ゴルーチンは、シグナル送信後に
runtime.Gosched()
を呼び出し、他のゴルーチンにCPUを譲ることで、より多くのコンテキストスイッチと競合状態の機会を創出します。 - この処理を短時間(デフォルト3秒、
testing.Short()
の場合は100ミリ秒)実行し、その後、両方のゴルーチンが終了するのを待ちます。
このテストは、シグナルハンドラが頻繁に呼び出され、シグナル処理ゴルーチンがシグナルを消費するのと同時に新しいシグナルがキューに追加されるという、まさに競合状態が発生しやすいシナリオをシミュレートします。このテストがクラッシュせずに正常に完了することで、sigqueue
の修正が効果的であることが検証されます。
コアとなるコードの変更箇所
src/pkg/os/signal/signal_test.go
--- a/src/pkg/os/signal/signal_test.go
+++ b/src/pkg/os/signal/signal_test.go
@@ -8,6 +8,7 @@ package signal
import (
"os"
+ "runtime"
"syscall"
"testing"
"time"
@@ -58,3 +59,43 @@ func TestSignal(t *testing.T) {
// The first SIGHUP should be waiting for us on c.
waitSig(t, c, syscall.SIGHUP)
}
+
+func TestStress(t *testing.T) {
+ dur := 3 * time.Second
+ if testing.Short() {
+ dur = 100 * time.Millisecond
+ }
+ defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4))
+ done := make(chan bool)
+ finished := make(chan bool)
+ go func() {
+ sig := make(chan os.Signal, 1)
+ Notify(sig, syscall.SIGUSR1)
+ Loop:
+ for {
+ select {
+ case <-sig:
+ case <-done:
+ break Loop
+ }
+ }
+ finished <- true
+ }()
+ go func() {
+ Loop:
+ for {
+ select {
+ case <-done:
+ break Loop
+ default:
+ syscall.Kill(syscall.Getpid(), syscall.SIGUSR1)
+ runtime.Gosched()
+ }
+ }
+ finished <- true
+ }()
+ time.Sleep(dur)
+ close(done)
+ <-finished
+ <-finished
+}
src/pkg/runtime/sigqueue.goc
--- a/src/pkg/runtime/sigqueue.goc
+++ b/src/pkg/runtime/sigqueue.goc
@@ -5,36 +5,24 @@
// This file implements runtime support for signal handling.
//
// Most synchronization primitives are not available from
-// the signal handler (it cannot block and cannot use locks)
+// the signal handler (it cannot block, allocate memory, or use locks)
// so the handler communicates with a processing goroutine
// via struct sig, below.
//
-// Ownership for sig.Note passes back and forth between
-// the signal handler and the signal goroutine in rounds.
-// The initial state is that sig.note is cleared (setup by signal_enable).
-// At the beginning of each round, mask == 0.
-// The round goes through three stages:\n-//\n-// (In parallel)\n-// 1a) One or more signals arrive and are handled\n-// by sigsend using cas to set bits in sig.mask.\n-// The handler that changes sig.mask from zero to non-zero\n-// calls notewakeup(&sig).\n-// 1b) Sigrecv calls notesleep(&sig) to wait for the wakeup.\n-//\n-// 2) Having received the wakeup, sigrecv knows that sigsend\n-// will not send another wakeup, so it can noteclear(&sig)\n-// to prepare for the next round. (Sigsend may still be adding\n-// signals to sig.mask at this point, which is fine.)\n-//\n-// 3) Sigrecv uses cas to grab the current sig.mask and zero it,\n-// triggering the next round.\n-//\n-// The signal handler takes ownership of the note by atomically\n-// changing mask from a zero to non-zero value. It gives up\n-// ownership by calling notewakeup. The signal goroutine takes\n-// ownership by returning from notesleep (caused by the notewakeup)\n-// and gives up ownership by clearing mask.\n+// sigsend() is called by the signal handler to queue a new signal.
+// signal_recv() is called by the Go program to receive a newly queued signal.
+// Synchronization between sigsend() and signal_recv() is based on the sig.state
+// variable. It can be in 3 states: 0, HASWAITER and HASSIGNAL.
+// HASWAITER means that signal_recv() is blocked on sig.Note and there are no
+// new pending signals.
+// HASSIGNAL means that sig.mask *may* contain new pending signals,
+// signal_recv() can't be blocked in this state.
+// 0 means that there are no new pending signals and signal_recv() is not blocked.
+// Transitions between states are done atomically with CAS.
+// When signal_recv() is unblocked, it resets sig.Note and rechecks sig.mask.
+// If several sigsend()'s and signal_recv() execute concurrently, it can lead to
+// unnecessary rechecks of sig.mask, but must not lead to missed signals
+// nor deadlocks.
package runtime
#include "runtime.h"
@@ -45,15 +33,20 @@ static struct {
Note;
uint32 mask[(NSIG+31)/32];
uint32 wanted[(NSIG+31)/32];
- uint32 kick;
+ uint32 state;
bool inuse;
} sig;
+enum {
+ HASWAITER = 1,
+ HASSIGNAL = 2,
+};
+
// Called from sighandler to send a signal back out of the signal handling thread.
bool
runtime·sigsend(int32 s)
{
- uint32 bit, mask;
+ uint32 bit, mask, old, new;
if(!sig.inuse || s < 0 || s >= 32*nelem(sig.wanted) || !(sig.wanted[s/32]&(1U<<(s&31))))
return false;
@@ -65,8 +58,20 @@ runtime·sigsend(int32 s)
if(runtime·cas(&sig.mask[s/32], mask, mask|bit)) {
// Added to queue.
// Only send a wakeup if the receiver needs a kick.
- if(runtime·cas(&sig.kick, 1, 0))
- runtime·notewakeup(&sig);
+ for(;;) {
+ old = runtime·atomicload(&sig.state);
+ if(old == HASSIGNAL)
+ break;
+ if(old == HASWAITER)
+ new = 0;
+ else // if(old == 0)
+ new = HASSIGNAL;
+ if(runtime·cas(&sig.state, old, new)) {
+ if (old == HASWAITER)
+ runtime·notewakeup(&sig);
+ break;
+ }
+ }
break;
}
}
@@ -77,7 +82,7 @@ runtime·sigsend(int32 s)
// Must only be called from a single goroutine at a time.\n func signal_recv() (m uint32) {
static uint32 recv[nelem(sig.mask)];
- int32 i, more;
+ uint32 i, old, new;
for(;;) {
// Serve from local copy if there are bits left.
@@ -89,15 +94,27 @@ func signal_recv() (m uint32) {
}
}
- // Get a new local copy.
- // Ask for a kick if more signals come in
- // during or after our check (before the sleep).
- if(sig.kick == 0) {
- runtime·noteclear(&sig);
- runtime·cas(&sig.kick, 0, 1);
+ // Check and update sig.state.
+ for(;;) {
+ old = runtime·atomicload(&sig.state);
+ if(old == HASWAITER)
+ runtime·throw("inconsistent state in signal_recv");
+ if(old == HASSIGNAL)
+ new = 0;
+ else // if(old == 0)
+ new = HASWAITER;
+ if(runtime·cas(&sig.state, old, new)) {
+ if (new == HASWAITER) {
+ runtime·entersyscall();
+ runtime·notesleep(&sig);
+ runtime·exitsyscall();
+ runtime·noteclear(&sig);
+ }
+ break;
+ }
}
- more = 0;
+ // Get a new local copy.
for(i=0; i<nelem(sig.mask); i++) {
for(;;) {
m = sig.mask[i];
@@ -105,16 +122,7 @@ func signal_recv() (m uint32) {
break;
}
recv[i] = m;
-- if(m != 0)
-- more = 1;
}
-- if(more)
-- continue;
--
-- // Sleep waiting for more.
-- runtime·entersyscall();
-- runtime·notesleep(&sig);
-- runtime·exitsyscall();
}
done:;
コアとなるコードの解説
src/pkg/runtime/sigqueue.goc
sig
構造体の変更
static struct {
Note;
uint32 mask[(NSIG+31)/32];
uint32 wanted[(NSIG+31)/32];
- uint32 kick;
+ uint32 state;
bool inuse;
} sig;
+enum {
+ HASWAITER = 1,
+ HASSIGNAL = 2,
+};
uint32 kick;
が削除され、代わりにuint32 state;
が追加されました。これは、従来の単純な「ウェイクアップが必要か」というフラグから、より複雑な状態管理への移行を示します。enum
でHASWAITER
とHASSIGNAL
という定数が定義されました。これらはsig.state
が取りうる値であり、それぞれシグナル処理ゴルーチンが待機中であるか、新しいシグナルがキューにあるかを示します。
runtime·sigsend
関数の変更
この関数はシグナルハンドラから呼び出され、OSから受け取ったシグナルをキューに入れます。
bool
runtime·sigsend(int32 s)
{
- uint32 bit, mask;
+ uint32 bit, mask, old, new;
// ... (シグナルが有効かどうかのチェック)
// シグナルを sig.mask にアトミックに追加
for(;;) {
mask = sig.mask[s/32];
bit = 1U<<(s&31);
if(mask & bit) // Already in queue.
return true;
if(runtime·cas(&sig.mask[s/32], mask, mask|bit)) {
// Added to queue.
// Only send a wakeup if the receiver needs a kick.
- if(runtime·cas(&sig.kick, 1, 0))
- runtime·notewakeup(&sig);
+ for(;;) { // sig.state のアトミックな更新ループ
+ old = runtime·atomicload(&sig.state); // 現在の状態を読み込む
+ if(old == HASSIGNAL) // 既にシグナルがある状態なら、ウェイクアップは不要
+ break;
+ if(old == HASWAITER) // 待機中のゴルーチンがいるなら、ウェイクアップが必要
+ new = 0; // 状態を 0 に遷移させる(ウェイクアップ後、待機ゴルーチンはいなくなるため)
+ else // if(old == 0) // 待機ゴルーチンもシグナルもない状態なら、シグナルがある状態へ
+ new = HASSIGNAL;
+ if(runtime·cas(&sig.state, old, new)) { // アトミックに状態を更新
+ if (old == HASWAITER) // 以前の状態が HASWAITER だった場合のみウェイクアップ
+ runtime·notewakeup(&sig);
+ break; // 状態更新に成功したらループを抜ける
+ }
+ }
break;
}
}
return true;
}
sig.kick
を使用したウェイクアップロジックが完全に削除され、sig.state
を使用した新しいアトミックな状態遷移ロジックに置き換えられました。for(;;)
ループとruntime·atomicload
、runtime·cas
を組み合わせることで、sig.state
の読み込み、新しい状態の決定、そして更新までの一連の操作がアトミックに、かつ競合状態を安全に処理できるようになりました。old == HASWAITER
の場合にのみruntime·notewakeup(&sig)
が呼び出されることで、必要な場合にのみシグナル処理ゴルーチンがウェイクアップされることが保証されます。
signal_recv
関数の変更
この関数は通常のGoゴルーチンから呼び出され、キューからシグナルを受信します。
func signal_recv() (m uint32) {
static uint32 recv[nelem(sig.mask)];
- int32 i, more;
+ uint32 i, old, new;
for(;;) {
// Serve from local copy if there are bits left.
// ... (ローカルコピーからのシグナル処理ロジック)
- // Get a new local copy.
- // Ask for a kick if more signals come in
- // during or after our check (before the sleep).
- if(sig.kick == 0) {
- runtime·noteclear(&sig);
- runtime·cas(&sig.kick, 0, 1);
- }
-
- more = 0;
+ // Check and update sig.state.
+ for(;;) { // sig.state のアトミックな更新ループ
+ old = runtime·atomicload(&sig.state); // 現在の状態を読み込む
+ if(old == HASWAITER) // 矛盾した状態: signal_recv が待機中なのに、なぜかこのコードパスに来た
+ runtime·throw("inconsistent state in signal_recv");
+ if(old == HASSIGNAL) // シグナルがある状態なら、シグナルを消費するので状態を 0 に
+ new = 0;
+ else // if(old == 0) // シグナルがない状態なら、待機状態へ
+ new = HASWAITER;
+ if(runtime·cas(&sig.state, old, new)) { // アトミックに状態を更新
+ if (new == HASWAITER) { // 新しい状態が HASWAITER ならスリープ
+ runtime·entersyscall(); // システムコールに入る準備
+ runtime·notesleep(&sig); // Note でスリープ
+ runtime·exitsyscall(); // システムコールから出る
+ runtime·noteclear(&sig); // Note の状態をクリア
+ }
+ break; // 状態更新に成功したらループを抜ける
+ }
+ }
+
+ // Get a new local copy.
for(i=0; i<nelem(sig.mask); i++) {
for(;;) {
m = sig.mask[i];
if(runtime·cas(&sig.mask[i], m, 0))
break;
}
recv[i] = m;
- if(m != 0)
- more = 1;
}
- if(more)
- continue;
-
- // Sleep waiting for more.
- runtime·entersyscall();
- runtime·notesleep(&sig);
- runtime·exitsyscall();
}
done:;
sig.kick
を使用したロジックが削除されました。for(;;)
ループ内でsig.state
をアトミックに更新する新しいロジックが導入されました。signal_recv
がシグナルを待機する必要がある場合(new == HASWAITER
)、runtime·entersyscall()
、runtime·notesleep(&sig)
、runtime·exitsyscall()
を呼び出してスリープします。これにより、OSのシグナル処理を待つ間、Goスケジューラが他のゴルーチンを実行できるようになります。- スリープから戻った後、
runtime·noteclear(&sig)
を呼び出してNote
の状態をクリアし、次のウェイクアップに備えます。 old == HASWAITER
の場合にruntime·throw("inconsistent state in signal_recv")
が追加されました。これは、signal_recv
が既に待機状態にあるはずなのに、このコードパスに到達した場合は論理的な矛盾があることを示し、ランタイムエラーとして検出します。
src/pkg/os/signal/signal_test.go
TestStress
関数の追加
func TestStress(t *testing.T) {
dur := 3 * time.Second
if testing.Short() {
dur = 100 * time.Millisecond
}
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4)) // CPU数を4に設定し、並行性を高める
done := make(chan bool) // 終了通知用チャネル
finished := make(chan bool) // ゴルーチン終了通知用チャネル
go func() { // シグナル受信ゴルーチン
sig := make(chan os.Signal, 1)
Notify(sig, syscall.SIGUSR1) // SIGUSR1 シグナルをこのチャネルで受け取る
Loop:
for {
select {
case <-sig: // シグナルを受信したら何もしない
case <-done: // 終了通知を受け取ったらループを抜ける
break Loop
}
}
finished <- true // 終了を通知
}()
go func() { // シグナル送信ゴルーチン
Loop:
for {
select {
case <-done: // 終了通知を受け取ったらループを抜ける
break Loop
default: // 終了通知がなければ
syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) // 自身のプロセスに SIGUSR1 を送信
runtime.Gosched() // 他のゴルーチンにCPUを譲る
}
}
finished <- true // 終了を通知
}()
time.Sleep(dur) // 指定された時間だけ実行を待つ
close(done) // 終了通知チャネルをクローズし、両ゴルーチンに終了を促す
<-finished // 両ゴルーチンが終了するのを待つ
<-finished
}
このテストは、Goのシグナル処理メカニズムが並行性の高い環境下で正しく動作するかを検証するために設計されています。
runtime.GOMAXPROCS(4)
: 複数のCPUコアを利用するように設定し、ゴルーチンが真に並行して実行される可能性を高めます。syscall.Kill(syscall.Getpid(), syscall.SIGUSR1)
: プロセス自身にシグナルを送信することで、シグナルハンドラが頻繁に呼び出される状況を作り出します。runtime.Gosched()
: シグナル送信後に他のゴルーチンに実行を譲ることで、シグナルハンドラとシグナル処理ゴルーチン間のコンテキストスイッチを促し、競合状態が発生しやすいタイミングを作り出します。 このテストがクラッシュせずに完了することは、sigqueue
の新しい同期メカニズムが堅牢であることを示します。
関連リンク
- Go Issue #4383: https://github.com/golang/go/issues/4383
- Go Code Review (CL) 6996060: https://golang.org/cl/6996060
参考にした情報源リンク
- Go言語のソースコード (
src/pkg/runtime/sigqueue.goc
,src/pkg/os/signal/signal_test.go
) - Go言語のドキュメント (特に
os/signal
パッケージ) - アトミック操作に関する一般的な情報 (Compare-and-Swapなど)
- OSシグナルハンドリングに関する一般的な情報I have generated the detailed technical explanation in Markdown format, following all the specified instructions and chapter structure. The content is in Japanese and aims to be as comprehensive as possible, covering background, prerequisites, and technical details. I have also included the core code changes and their explanations.
I will now output the generated explanation to standard output.