[インデックス 15783] ファイルの概要
このコミットは、Go言語の os/signal
パッケージに Stop
関数を追加し、シグナルハンドリング、特に SIGHUP
の扱いを改善することを目的としています。これにより、特定のチャネルへのシグナル通知を停止する機能が提供され、シグナルハンドリングの柔軟性と堅牢性が向上します。また、SIGHUP
シグナルが nohup
コマンドによって無視されるべき場合に、Goランタイムがその挙動を尊重するように修正されています。
コミット
commit cb4428e555f664a64b0e2f5f4fe6e3c5991dbdd7
Author: Russ Cox <rsc@golang.org>
Date: Fri Mar 15 00:00:02 2013 -0400
os/signal: add Stop, be careful about SIGHUP
Fixes #4268.
Fixes #4491.
R=golang-dev, nightlyone, fullung, r
CC=golang-dev
https://golang.org/cl/7546048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cb4428e555f664a64b0e2f5f4fe6e3c5991dbdd7
元コミット内容
os/signal: add Stop, be careful about SIGHUP
このコミットは、os/signal
パッケージに Stop
関数を追加し、SIGHUP
シグナルの扱いをより慎重に行うように変更します。
関連する問題:
- Fixes #4268.
- Fixes #4491.
変更の背景
このコミットは、主に以下の2つのGoイシューを解決するために導入されました。
-
Issue #4268:
os/signal
: need a way to unregister a channel このイシューは、os/signal.Notify
関数で登録されたチャネルへのシグナル通知を停止するメカニズムがないという問題提起です。Notify
を呼び出すと、指定されたシグナルがそのチャネルに送信され始めますが、一度登録すると、そのチャネルがガベージコレクションされるまでシグナルを受け取り続ける可能性がありました。これにより、リソースリークや不要なシグナル処理が発生する可能性がありました。このコミットは、Stop
関数を導入することで、この問題を解決します。 -
Issue #4491:
os/signal
: SIGHUP handling is too aggressive このイシューは、Goプログラムがnohup
コマンドで実行された場合でも、SIGHUP
シグナルを捕捉して処理しようとする Go のos/signal
パッケージの挙動に関するものです。通常、nohup
コマンドは、端末が切断されてもプロセスが実行を継続できるように、SIGHUP
シグナルを無視するように設定します。しかし、GoのランタイムがSIGHUP
を捕捉してしまうと、nohup
の意図に反してプログラムが終了してしまう可能性がありました。このコミットは、GoランタイムがSIGHUP
のデフォルトのSIG_IGN
(無視) ハンドラを尊重するように変更することで、この問題を解決します。
これらの問題に対処するため、シグナルハンドリングの内部構造が変更され、より柔軟で予測可能な挙動が実現されました。
前提知識の解説
Unixシグナル
Unix系OSにおけるシグナルは、プロセスに対して非同期的にイベントを通知するメカニズムです。以下に、このコミットに関連する主要なシグナルを説明します。
-
SIGHUP (Signal Hang Up):
- 通常、制御端末が切断されたときにプロセスに送信されます。
- デーモンプロセスなどでは、設定ファイルの再読み込みなどの目的で捕捉・処理されることがあります。
nohup
コマンドで実行されたプロセスは、通常SIGHUP
を無視するように設定されます。
-
SIGINT (Signal Interrupt):
- 通常、Ctrl+C が押されたときにプロセスに送信されます。
- プログラムの正常終了を促すために使用されます。
-
SIGWINCH (Signal Window Change):
- 端末のウィンドウサイズが変更されたときにプロセスに送信されます。
- 通常、インタラクティブなアプリケーションが画面表示を調整するために使用します。
os/signal
パッケージ
Go言語の os/signal
パッケージは、OSからのシグナルをGoプログラム内で処理するための機能を提供します。
-
signal.Notify(c chan<- os.Signal, sig ...os.Signal)
:- 指定されたシグナル(
sig
)がプロセスに送信されたときに、それらのシグナルをチャネルc
にリレーするように設定します。 sig
が指定されない場合、すべての受信シグナルがチャネルにリレーされます。- このコミット以前は、一度
Notify
で登録すると、そのチャネルへの通知を停止する直接的な方法がありませんでした。
- 指定されたシグナル(
-
signal.Stop(c chan<- os.Signal)
(このコミットで追加):Notify
で登録されたチャネルc
へのシグナル通知を停止します。- これにより、不要になったシグナルハンドラをクリーンアップし、リソースを解放できるようになります。
nohup
コマンド
nohup
は "no hang up" の略で、端末が切断されてもコマンドが実行を継続できるようにするために使用されるUnixコマンドです。nohup
は通常、実行されるコマンドの SIGHUP
シグナルを無視するように設定します。
Goランタイムとシグナルハンドリング
Goランタイムは、OSからのシグナルを捕捉し、Goプログラム内の os/signal
パッケージにリレーする役割を担っています。ランタイムは、シグナルハンドラをOSに登録し、シグナルを受信した際に適切なGoルーチンにディスパッチします。このコミットでは、ランタイムレベルでの SIGHUP
の扱いが改善され、nohup
の挙動が尊重されるようになります。
技術的詳細
このコミットは、os/signal
パッケージとGoランタイムの両方にわたる広範な変更を含んでいます。
os/signal
パッケージの変更
-
Stop
関数の追加:Stop(c chan<- os.Signal)
関数が追加されました。この関数は、指定されたチャネルc
へのシグナル通知を停止します。- 内部的には、
handlers.m
マップからチャネルc
に関連付けられたハンドラを削除し、そのハンドラが要求していた各シグナルについて参照カウントhandlers.ref
をデクリメントします。 - 参照カウントが0になったシグナルについては、
disableSignal
関数を呼び出して、そのシグナルのOSレベルでのハンドリングを無効化します。
-
handlers
構造体の変更:- 以前は
list []handler
であったhandlers
構造体が、m map[chan<- os.Signal]*handler
とref [numSig]int64
に変更されました。 m
は、各チャネルとそれに関連付けられたシグナルマスク(どのシグナルをそのチャネルに送るか)を管理します。ref
は、各シグナルが現在いくつのチャネルによって要求されているかの参照カウントを保持します。これにより、特定のシグナルが不要になったときに、OSレベルでのハンドリングを安全に無効化できるようになります。
- 以前は
-
Notify
関数の変更:Notify
関数は、handlers.m
を使用して、同じチャネルが複数回Notify
された場合に、そのチャネルに送信されるシグナルのセットを拡張するように変更されました。- シグナルがチャネルによって要求されると、
handlers.ref
の参照カウントが増加し、そのシグナルがまだ有効化されていない場合はenableSignal
が呼び出されます。
-
handler
構造体の変更:handler
構造体は、mask [(numSig + 31) / 32]uint32
を持つように変更されました。これは、ビットマスクを使用して、そのハンドラがどのシグナルを処理するかを効率的に表現します。want(sig int)
とset(sig int)
メソッドが追加され、マスクの操作を容易にします。
-
process
関数の変更:process
関数は、handlers.m
マップをイテレートし、各ハンドラのwant(n)
メソッドを使用して、どのチャネルにシグナルを送信すべきかを判断するように変更されました。
Goランタイムの変更
-
signal_disable
関数の追加:os/signal
パッケージから呼び出されるsignal_disable
関数が追加されました。これは、Goランタイムのruntime.sigdisable
関数を呼び出します。runtime.sigdisable
は、指定されたシグナルのOSレベルでのハンドリングを無効化します。もしシグナルがGoランタイムによって無視されるべき (SigIgnored
フラグがセットされている) 場合はSIG_IGN
に、そうでなければSIG_DFL
(デフォルトの挙動) に設定します。
-
runtime.h
の変更:SigHandling
とSigIgnored
という新しいフラグがruntime.h
のSigTab
エントリに追加されました。SigHandling
: Goランタイムがこのシグナルのハンドラを登録していることを示します。SigIgnored
: Goランタイムがハンドラを登録する前に、このシグナルがOSによってSIG_IGN
に設定されていたことを示します。これは、特にSIGHUP
のnohup
挙動を尊重するために重要です。
-
runtime/signal_unix.c
の変更:runtime.initsig
関数内で、SIGHUP
とSIGINT
について、Goランタイムがハンドラを登録する前にSIG_IGN
が設定されているかどうかをチェックするロジックが追加されました。もしSIG_IGN
であれば、SigIgnored
フラグがセットされ、Goランタイムは独自のハンドラを登録しません。runtime.sigenable
とruntime.sigdisable
関数が、SigHandling
とSigIgnored
フラグを考慮して、シグナルハンドラの設定と解除を行うように変更されました。
-
テストケースの追加 (
signal_test.go
):TestStop
関数が追加され、Stop
関数がチャネルの登録を正しく解除することを確認します。TestNohup
関数が追加され、nohup
環境下でのSIGHUP
の挙動が正しく処理されることを確認します。これは、exec.Command
を使用して、send_uncaught_sighup
フラグを伴うテストを子プロセスとして実行することで実現されます。
これらの変更により、Goのシグナルハンドリングはより洗練され、特に SIGHUP
のような特殊なシグナルに対する挙動が、OSの慣習とより一致するようになりました。
コアとなるコードの変更箇所
src/pkg/os/signal/signal.go
var handlers struct {
sync.Mutex
- list []handler
+ m map[chan<- os.Signal]*handler
+ ref [numSig]int64
}
type handler struct {
- c chan<- os.Signal
- sig os.Signal
- all bool
+ mask [(numSig + 31) / 32]uint32
+}
+
+func (h *handler) want(sig int) bool {
+ return (h.mask[sig/32]>>uint(sig&31))&1 != 0
+}
+
+func (h *handler) set(sig int) {
+ h.mask[sig/32] |= 1 << uint(sig&31)
}
// Notify causes package signal to relay incoming signals to c.
@@ -39,32 +53,77 @@ func Notify(c chan<- os.Signal, sig ...os.Signal) {
handlers.Lock()
defer handlers.Unlock()
-
- if len(sig) == 0 {
- enableSignal(nil)
- handlers.list = append(handlers.list, handler{c: c, all: true})
- } else {
- for _, s := range sig {
- // We use nil as a special wildcard value for enableSignal,
- // so filter it out of the list of arguments. This is safe because
- // we will never get an incoming nil signal, so discarding the
- // registration cannot affect the observed behavior.
- if s != nil {
- enableSignal(s)
- handlers.list = append(handlers.list, handler{c: c, sig: s})
- }
- }
- }
-}
-
+
+ h := handlers.m[c]
+ if h == nil {
+ if handlers.m == nil {
+ handlers.m = make(map[chan<- os.Signal]*handler)
+ }
+ h = new(handler)
+ handlers.m[c] = h
+ }
+
+ add := func(n int) {
+ if n < 0 {
+ return
+ }
+ if !h.want(n) {
+ h.set(n)
+ if handlers.ref[n] == 0 {
+ enableSignal(n)
+ }
+ handlers.ref[n]++
+ }
+ }
+
+ if len(sig) == 0 {
+ for n := 0; n < numSig; n++ {
+ add(n)
+ }
+ } else {
+ for _, s := range sig {
+ add(signum(s))
+ }
+ }
+}
+
+// Stop causes package signal to stop relaying incoming signals to c.
+// It undoes the effect of all prior calls to Notify using c.
+// When Stop returns, it is guaranteed that c will receive no more signals.
+func Stop(c chan<- os.Signal) {
+ handlers.Lock()
+ defer handlers.Unlock()
+
+ h := handlers.m[c]
+ if h == nil {
+ return
+ }
+ delete(handlers.m, c)
+
+ for n := 0; n < numSig; n++ {
+ if h.want(n) {
+ handlers.ref[n]--
+ if handlers.ref[n] == 0 {
+ disableSignal(n)
+ }
+ }
+ }
+}
+
func process(sig os.Signal) {
+ n := signum(sig)
+ if n < 0 {
+ return
+ }
+
handlers.Lock()
defer handlers.Unlock()
-
- for _, h := range handlers.list {
- if h.all || h.sig == sig {
+
+ for c, h := range handlers.m {
+ if h.want(n) {
// send but do not block for it
select {
- case h.c <- sig:
+ case c <- sig:
default:
}
}
src/pkg/runtime/runtime.h
enum
{
SigThrow = 1<<2, // if signal.Notify doesn't take it, exit loudly
SigPanic = 1<<3, // if the signal is from the kernel, panic
SigDefault = 1<<4, // if the signal isn't explicitly requested, don't monitor it
+ SigHandling = 1<<5, // our signal handler is registered
+ SigIgnored = 1<<6, // the signal was ignored before we registered for it
};
// NOTE(rsc): keep in sync with extern.go:/type.Func.
@@ -696,6 +698,7 @@ String runtime·gostringnocopy(byte*);
String runtime·gostringw(uint16*);
void runtime·initsig(void);
void runtime·sigenable(uint32 sig);
+void runtime·sigdisable(uint32 sig);
int32 runtime·gotraceback(void);
void runtime·goroutineheader(G*);
void runtime·traceback(uint8 *pc, uint8 *sp, uint8 *lr, G* gp);
src/pkg/runtime/signal_unix.c
void
runtime·initsig(void)
{
int32 i;
SigTab *t;
for(i = 0; i<NSIG; i++) {
t = &runtime·sigtab[i];
if((t->flags == 0) || (t->flags & SigDefault))
continue;
+
+ // For some signals, we respect an inherited SIG_IGN handler
+ // rather than insist on installing our own default handler.
+ // Even these signals can be fetched using the os/signal package.
+ switch(i) {
+ case SIGHUP:
+ case SIGINT:
+ if(runtime·getsig(i) == SIG_IGN) {
+ t->flags = SigNotify | SigIgnored;
+ continue;
+ }
+ }
+
+ t->flags |= SigHandling;
runtime·setsig(i, runtime·sighandler, true);
}
}
@@ -29,18 +43,35 @@ runtime·initsig(void)
void
runtime·sigenable(uint32 sig)
{
-\tint32 i;\n \tSigTab *t;\n \n-\tfor(i = 0; i<NSIG; i++) {\n-\t\t// ~0 means all signals.\n-\t\tif(~sig == 0 || i == sig) {\n-\t\t\tt = &runtime·sigtab[i];\n-\t\t\tif(t->flags & SigDefault) {\n-\t\t\t\truntime·setsig(i, runtime·sighandler, true);\n-\t\t\t\tt->flags &= ~SigDefault; // make this idempotent\n-\t\t\t}\n-\t\t}\n+\tif(sig >= NSIG)\n+\t\treturn;\n+\n+\tt = &runtime·sigtab[sig];\n+\tif((t->flags & SigNotify) && !(t->flags & SigHandling)) {\n+\t\tt->flags |= SigHandling;\n+\t\tif(runtime·getsig(sig) == SIG_IGN)\n+\t\t\tt->flags |= SigIgnored;\n+\t\truntime·setsig(sig, runtime·sighandler, true);\n+\t}\n+}
+\n+void
+runtime·sigdisable(uint32 sig)
+{
+\tSigTab *t;\n+\n+\tif(sig >= NSIG)\n+\t\treturn;\n+\n+\tt = &runtime·sigtab[sig];\n+\tif((t->flags & SigNotify) && (t->flags & SigHandling)) {\n+\t\tt->flags &= ~SigHandling;\n+\t\tif(t->flags & SigIgnored)\n+\t\t\truntime·setsig(sig, SIG_IGN, true);\n+\t\telse\n+\t\t\truntime·setsig(sig, SIG_DFL, true);\n \t}\n }
コアとなるコードの解説
src/pkg/os/signal/signal.go
の変更
-
handlers
構造体の再定義:- 以前は
list []handler
というスライスでシグナルハンドラを管理していましたが、m map[chan<- os.Signal]*handler
とref [numSig]int64
に変更されました。 m
は、Notify
で登録された各チャネル (chan<- os.Signal
) をキーとし、それに対応するhandler
構造体へのポインタを値とするマップです。これにより、特定のチャネルに関連付けられたシグナル設定を効率的に検索・更新できるようになります。ref
は、各シグナル番号 (numSig
はシステムでサポートされる最大シグナル番号) ごとに、そのシグナルを現在要求しているチャネルの数を追跡する参照カウントです。この参照カウントが0になると、そのシグナルのOSレベルでのハンドリングを無効化できるため、リソースの最適化と不要なシグナル処理の回避が可能になります。
- 以前は
-
handler
構造体の再定義とメソッドの追加:handler
構造体は、以前のc
,sig
,all
フィールドの代わりに、mask [(numSig + 31) / 32]uint32
を持つようになりました。これは、ビットマスクを使用して、このハンドラがどのシグナルを処理するかを効率的に表現します。例えば、mask[0]
の0ビット目が1であればシグナル0を処理し、1ビット目が1であればシグナル1を処理するといった具合です。want(sig int)
メソッドは、指定されたシグナルがこのハンドラによって処理されるべきかどうかをマスクに基づいてチェックします。set(sig int)
メソッドは、指定されたシグナルをこのハンドラのマスクに追加します。
-
Notify
関数のロジック変更:Notify
が呼び出されると、まずhandlers.m
をチェックして、そのチャネルが既に登録されているかどうかを確認します。登録されていなければ新しいhandler
を作成し、マップに追加します。add
という内部関数が導入され、指定されたシグナル (n
) をハンドラのマスクに設定し、handlers.ref
の参照カウントをインクリメントします。もしそのシグナルの参照カウントが0から1になった場合(つまり、そのシグナルを要求する最初のチャネルである場合)、enableSignal(n)
を呼び出してOSレベルでそのシグナルハンドリングを有効化します。sig
引数が空の場合(すべてのシグナルを要求する場合)、numSig
までのすべてのシグナルに対してadd
が呼び出されます。それ以外の場合は、指定されたシグナルに対してのみadd
が呼び出されます。
-
Stop
関数の追加:Stop
関数は、handlers.m
から指定されたチャネルc
を削除します。- その後、削除されたハンドラが要求していた各シグナルについて、
handlers.ref
の参照カウントをデクリメントします。 - もし参照カウントが0になった場合、
disableSignal(n)
を呼び出してOSレベルでそのシグナルハンドリングを無効化します。これにより、不要になったシグナルハンドラが適切にクリーンアップされます。
-
process
関数のロジック変更:process
関数は、受信したシグナル (sig
) の番号 (n
) を取得します。handlers.m
マップをイテレートし、各チャネル (c
) とそれに対応するハンドラ (h
) を取得します。h.want(n)
を呼び出して、現在のハンドラがこのシグナルを処理すべきかどうかをチェックします。- もし処理すべきであれば、
select
ステートメントを使用して、チャネルc
にシグナルを非同期的に送信します(ブロックしない)。
src/pkg/runtime/runtime.h
の変更
-
新しいシグナルフラグの追加:
SigHandling
(1<<5): GoランタイムがこのシグナルのハンドラをOSに登録していることを示すフラグです。SigIgnored
(1<<6): Goランタイムがハンドラを登録する前に、このシグナルがOSによってSIG_IGN
(無視) に設定されていたことを示すフラグです。これは、nohup
のような外部要因によってシグナルが無視されるべき場合に、Goランタイムがその挙動を尊重するために使用されます。
-
runtime.sigdisable
関数のプロトタイプ宣言:void runtime·sigdisable(uint32 sig);
が追加され、Goランタイムが特定のシグナルのOSレベルでのハンドリングを無効化する機能を提供します。
src/pkg/runtime/signal_unix.c
の変更
-
runtime.initsig
におけるSIGHUP
とSIGINT
の特殊処理:runtime.initsig
は、Goプログラム起動時にシグナルハンドラを初期化する関数です。- このコミットでは、
SIGHUP
とSIGINT
について、runtime.getsig(i) == SIG_IGN
をチェックするロジックが追加されました。これは、Goランタイムがハンドラを登録する前に、これらのシグナルが既にOSによって無視されるように設定されているかどうかを確認します。 - もし
SIG_IGN
であれば、そのシグナルのSigTab
エントリにSigIgnored
フラグがセットされ、Goランタイムは独自のハンドラを登録せずにcontinue
します。これにより、nohup
で実行されたプログラムがSIGHUP
を捕捉しようとする問題を回避します。 - それ以外の場合、
SigHandling
フラグがセットされ、runtime.setsig
を呼び出してGoランタイムのシグナルハンドラ (runtime.sighandler
) を登録します。
-
runtime.sigenable
の変更:runtime.sigenable
は、os/signal
パッケージから特定のシグナルのハンドリングを有効化するよう要求されたときに呼び出されます。- この関数は、指定されたシグナル (
sig
) のSigTab
エントリをチェックし、もしSigNotify
フラグがセットされており、かつSigHandling
フラグがセットされていない場合(つまり、まだGoランタイムがハンドラを登録していない場合)に処理を行います。 SigHandling
フラグをセットし、もしruntime.getsig(sig) == SIG_IGN
であればSigIgnored
フラグもセットします。- 最後に
runtime.setsig
を呼び出して、Goランタイムのシグナルハンドラを登録します。
-
runtime.sigdisable
の追加:runtime.sigdisable
は、os/signal
パッケージから特定のシグナルのハンドリングを無効化するよう要求されたときに呼び出されます。- この関数は、指定されたシグナル (
sig
) のSigTab
エントリをチェックし、もしSigNotify
フラグとSigHandling
フラグの両方がセットされている場合(つまり、Goランタイムが現在ハンドラを登録している場合)に処理を行います。 SigHandling
フラグをクリアします。- もし
SigIgnored
フラグがセットされていれば、runtime.setsig
を呼び出してシグナルハンドラをSIG_IGN
に戻します(元の無視状態に戻す)。 - そうでなければ、
runtime.setsig
を呼び出してシグナルハンドラをSIG_DFL
(デフォルトの挙動) に戻します。
これらの変更により、Goのシグナルハンドリングはよりきめ細かく制御できるようになり、特に SIGHUP
のようなシグナルに対するOSの慣習的な挙動を尊重するようになりました。
関連リンク
- Go CL: https://golang.org/cl/7546048
- Go Issue #4268:
os/signal
: need a way to unregister a channel: https://github.com/golang/go/issues/4268 - Go Issue #4491:
os/signal
: SIGHUP handling is too aggressive: https://github.com/golang/go/issues/4491
参考にした情報源リンク
- Go os/signal package documentation (コミット当時のバージョンに基づく理解)
- Unix Signals (man 7 signal)
- nohup(1) - Linux man page
- Go runtime source code (コミット当時のバージョンに基づく理解)
- Go issue tracker