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

[インデックス 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イシューを解決するために導入されました。

  1. Issue #4268: os/signal: need a way to unregister a channel このイシューは、os/signal.Notify 関数で登録されたチャネルへのシグナル通知を停止するメカニズムがないという問題提起です。Notify を呼び出すと、指定されたシグナルがそのチャネルに送信され始めますが、一度登録すると、そのチャネルがガベージコレクションされるまでシグナルを受け取り続ける可能性がありました。これにより、リソースリークや不要なシグナル処理が発生する可能性がありました。このコミットは、Stop 関数を導入することで、この問題を解決します。

  2. 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 パッケージの変更

  1. Stop 関数の追加:

    • Stop(c chan<- os.Signal) 関数が追加されました。この関数は、指定されたチャネル c へのシグナル通知を停止します。
    • 内部的には、handlers.m マップからチャネル c に関連付けられたハンドラを削除し、そのハンドラが要求していた各シグナルについて参照カウント handlers.ref をデクリメントします。
    • 参照カウントが0になったシグナルについては、disableSignal 関数を呼び出して、そのシグナルのOSレベルでのハンドリングを無効化します。
  2. handlers 構造体の変更:

    • 以前は list []handler であった handlers 構造体が、m map[chan<- os.Signal]*handlerref [numSig]int64 に変更されました。
    • m は、各チャネルとそれに関連付けられたシグナルマスク(どのシグナルをそのチャネルに送るか)を管理します。
    • ref は、各シグナルが現在いくつのチャネルによって要求されているかの参照カウントを保持します。これにより、特定のシグナルが不要になったときに、OSレベルでのハンドリングを安全に無効化できるようになります。
  3. Notify 関数の変更:

    • Notify 関数は、handlers.m を使用して、同じチャネルが複数回 Notify された場合に、そのチャネルに送信されるシグナルのセットを拡張するように変更されました。
    • シグナルがチャネルによって要求されると、handlers.ref の参照カウントが増加し、そのシグナルがまだ有効化されていない場合は enableSignal が呼び出されます。
  4. handler 構造体の変更:

    • handler 構造体は、mask [(numSig + 31) / 32]uint32 を持つように変更されました。これは、ビットマスクを使用して、そのハンドラがどのシグナルを処理するかを効率的に表現します。
    • want(sig int)set(sig int) メソッドが追加され、マスクの操作を容易にします。
  5. process 関数の変更:

    • process 関数は、handlers.m マップをイテレートし、各ハンドラの want(n) メソッドを使用して、どのチャネルにシグナルを送信すべきかを判断するように変更されました。

Goランタイムの変更

  1. signal_disable 関数の追加:

    • os/signal パッケージから呼び出される signal_disable 関数が追加されました。これは、Goランタイムの runtime.sigdisable 関数を呼び出します。
    • runtime.sigdisable は、指定されたシグナルのOSレベルでのハンドリングを無効化します。もしシグナルがGoランタイムによって無視されるべき (SigIgnored フラグがセットされている) 場合は SIG_IGN に、そうでなければ SIG_DFL (デフォルトの挙動) に設定します。
  2. runtime.h の変更:

    • SigHandlingSigIgnored という新しいフラグが runtime.hSigTab エントリに追加されました。
      • SigHandling: Goランタイムがこのシグナルのハンドラを登録していることを示します。
      • SigIgnored: Goランタイムがハンドラを登録する前に、このシグナルがOSによって SIG_IGN に設定されていたことを示します。これは、特に SIGHUPnohup 挙動を尊重するために重要です。
  3. runtime/signal_unix.c の変更:

    • runtime.initsig 関数内で、SIGHUPSIGINT について、Goランタイムがハンドラを登録する前に SIG_IGN が設定されているかどうかをチェックするロジックが追加されました。もし SIG_IGN であれば、SigIgnored フラグがセットされ、Goランタイムは独自のハンドラを登録しません。
    • runtime.sigenableruntime.sigdisable 関数が、SigHandlingSigIgnored フラグを考慮して、シグナルハンドラの設定と解除を行うように変更されました。
  4. テストケースの追加 (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]*handlerref [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 における SIGHUPSIGINT の特殊処理:

    • runtime.initsig は、Goプログラム起動時にシグナルハンドラを初期化する関数です。
    • このコミットでは、SIGHUPSIGINT について、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の慣習的な挙動を尊重するようになりました。

関連リンク

参考にした情報源リンク