[インデックス 14648] ファイルの概要
このコミットは、Goコマンド(cmd/go
)におけるOSシグナル(特にSIGINT
やSIGQUIT
)のハンドリングを改善するものです。具体的には、go run
およびgo test
コマンドがシグナルを受け取った際の挙動をより堅牢にし、ユーザーエクスペリエンスを向上させます。
変更されたファイルは以下の通りです。
src/cmd/dist/build.c
: ビルド順序にpkg/os/signal
を追加。src/cmd/go/build.go
: ビルドプロセスの並行処理において、シグナルによる中断を適切に処理するように変更。src/cmd/go/run.go
:go run
コマンドが子プロセスを実行する前にシグナルハンドラを起動するように変更。src/cmd/go/signal.go
: 新規ファイル。シグナルハンドリングの共通ロジックを定義。src/cmd/go/signal_notunix.go
: 新規ファイル。Unix系以外のOS(Plan9, Windows)向けのシグナル無視リストを定義。src/cmd/go/signal_unix.go
: 新規ファイル。Unix系OS(darwin, freebsd, linux, netbsd, openbsd)向けのシグナル無視リストを定義。src/cmd/go/test.go
:go test
コマンドがテストを実行する前にシグナルハンドラを起動するように変更。
コミット
commit 04f0d148e9946d4ec20d8d8a521560b091877665
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Fri Dec 14 17:33:59 2012 +1100
cmd/go: handle os signals
Ignore signals during "go run" and wait for running child
process to exit. Stop executing further tests during "go test",
wait for running tests to exit and report error exit code.
Original CL 6351053 by dfc.
Fixes #3572.
Fixes #3581.
R=golang-dev, dave, rsc
CC=golang-dev
https://golang.org/cl/6903061
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/04f0d148e9946d4ec20d8d8a521560b091877665
元コミット内容
このコミットは、GoコマンドがOSシグナルをどのように処理するかを改善することを目的としています。具体的には、go run
コマンド実行中にシグナル(例: Ctrl+C)を受け取った場合、Goコマンド自身はシグナルを無視し、実行中の子プロセスが終了するのを待つようにします。これにより、子プロセスが適切にクリーンアップされる機会が与えられます。
一方、go test
コマンド実行中にシグナルを受け取った場合、それ以降のテストの実行を停止し、現在実行中のテストが終了するのを待ちます。そして、エラー終了コードを報告することで、テストが中断されたことを明確に示します。
この変更は、dfcによる元の変更リスト(CL 6351053)に基づいています。また、Issue #3572とIssue #3581を修正します。
変更の背景
このコミットは、GoコマンドがOSシグナルを適切に処理しないという既存の問題を解決するために導入されました。特に、以下の2つのGitHub Issueがこの変更の直接的な背景となっています。
-
Issue #3572:
go run
doesn't pass signals to child process: この問題は、go run
コマンドがGoプログラムをコンパイルして実行する際に、親プロセス(go run
)が受け取ったシグナル(例えばCtrl+CによるSIGINT
)を子プロセス(実行されたGoプログラム)に適切に転送しない、あるいは子プロセスの終了を待たずに自身が終了してしまうというものでした。これにより、ユーザーがgo run
で起動したプログラムをCtrl+Cで終了させようとしても、プログラムが終了せず、ゾンビプロセスになったり、リソースが適切に解放されないといった問題が発生していました。 -
Issue #3581:
go test
doesn't stop on SIGINT: この問題は、go test
コマンドがテストを実行している最中にSIGINT
(Ctrl+C)を受け取っても、テストの実行がすぐに停止しないというものでした。ユーザーはテストの実行を中断したい場合、何度もCtrl+Cを押すか、プロセスを強制終了させる必要がありました。これは、特に多数のテストや長時間実行されるテストがある場合に、開発者の生産性を著しく低下させる要因となっていました。
これらの問題に対処するため、GoコマンドはOSシグナルをより適切に捕捉し、go run
では子プロセスの終了を待ち、go test
ではテストの実行を中断して適切な終了コードを返すように変更されました。これにより、Go開発者がCLIからGoプログラムやテストを操作する際の信頼性と利便性が向上します。
前提知識の解説
このコミットの変更内容を理解するためには、以下の概念について知っておく必要があります。
1. OSシグナル
OSシグナルは、オペレーティングシステムがプロセスに対して非同期にイベントを通知するメカニズムです。これらは、プログラムの異常終了、ユーザーからの割り込み、タイマーの期限切れなど、様々な状況で発生します。
- SIGINT (Interrupt Signal): 通常、ユーザーがターミナルでCtrl+Cを押したときにプロセスに送信されます。プログラムの正常な終了を要求するために使用されます。多くのプログラムは、このシグナルを受け取るとクリーンアップ処理を行って終了します。
- SIGQUIT (Quit Signal):
通常、ユーザーがターミナルでCtrl+\を押したときにプロセスに送信されます。
SIGINT
よりも強制的な終了を意味し、通常はコアダンプを生成して終了します。 - SIGTERM (Termination Signal):
プログラムの終了を要求するために送信される汎用的なシグナルです。
kill
コマンドなどでプロセスを終了させる際にデフォルトで送信されます。SIGINT
と同様に、プログラムはこれを受け取るとクリーンアップ処理を行って終了することが期待されます。
プロセスは、これらのシグナルに対してデフォルトの挙動(例: 終了、コアダンプ)を持つか、カスタムのシグナルハンドラを登録して特定の処理を実行することができます。
2. Goのos/signal
パッケージ
Go言語の標準ライブラリには、OSシグナルを扱うためのos/signal
パッケージが用意されています。
signal.Notify(c chan<- os.Signal, sig ...os.Signal)
: 指定されたシグナル(sig
)がプロセスに送信されたときに、それらのシグナルをチャネルc
に転送するようにOSに登録します。これにより、Goプログラムはシグナルをイベントとして受け取り、それに対応する処理を行うことができます。signal.Stop(c chan<- os.Signal)
:signal.Notify
で登録されたシグナル転送を停止します。os.Interrupt
:SIGINT
シグナルを表すos.Signal
型です。syscall.SIGQUIT
:SIGQUIT
シグナルを表すsyscall.Signal
型です。syscall
パッケージは、OS固有のシステムコールや定数を提供します。
3. sync.WaitGroup
sync.WaitGroup
は、Goの並行処理において、複数のゴルーチンが完了するのを待つための同期プリミティブです。
Add(delta int)
: 待機するゴルーチンの数をdelta
だけ増やします。Done()
:WaitGroup
のカウンタを1減らします。通常、ゴルーチンの終了時に呼び出されます。Wait()
:WaitGroup
のカウンタが0になるまでブロックします。
このメカニズムにより、メインゴルーチンは、起動した複数の子ゴルーチンがすべて処理を終えるまで待機することができます。
4. sync.Once
sync.Once
は、Goの並行処理において、特定の処理が一度だけ実行されることを保証するためのプリミティブです。
Do(f func())
: 引数として渡された関数f
を一度だけ実行します。複数のゴルーチンが同時にDo
を呼び出しても、f
は一度しか実行されません。これは、初期化処理などで非常に有用です。
5. go run
とgo test
の挙動
go run
: 指定されたGoソースファイルをコンパイルし、その結果生成された実行可能ファイルを一時ディレクトリに配置して実行します。通常、この実行可能ファイルはgo run
コマンドの子プロセスとして起動されます。go test
: Goパッケージのテストを実行します。テストは通常、コンパイルされたテストバイナリとして実行され、これもgo test
コマンドの子プロセスとして起動されます。
これらのコマンドは、ユーザーがCLIからGoプログラムやテストを直接操作するための主要なインターフェースであり、シグナルハンドリングの改善は、これらのコマンドのユーザーエクスペリエンスに直接影響します。
技術的詳細
このコミットの技術的な核心は、GoコマンドがOSシグナルを捕捉し、そのシグナルに応じてgo run
とgo test
の挙動を調整するメカニズムを導入した点にあります。
-
シグナルハンドリングの共通化とプラットフォーム依存性の分離:
- 新しいファイル
src/cmd/go/signal.go
が導入され、シグナルハンドリングの共通ロジックがカプセル化されました。 interrupted
というchan struct{}
型のチャネルが定義され、シグナルが捕捉された際にこのチャネルが閉じられることで、他のゴルーチンに中断を通知するメカニズムが提供されます。processSignals()
関数は、os/signal
パッケージを使用して、特定のシグナル(signalsToIgnore
で定義される)を捕捉し、interrupted
チャネルを閉じます。startSigHandlers()
関数は、sync.Once
を使用してprocessSignals()
が一度だけ実行されることを保証します。これにより、複数の場所からシグナルハンドラが起動されても、二重登録を防ぎます。src/cmd/go/signal_notunix.go
とsrc/cmd/go/signal_unix.go
が導入され、プラットフォーム(Windows/Plan9とUnix系)ごとに無視すべきシグナルのリスト(signalsToIgnore
)が定義されました。Unix系ではos.Interrupt
とsyscall.SIGQUIT
が、Windows/Plan9ではos.Interrupt
のみが対象となります。これは、OSによって利用可能なシグナルが異なるためです。
- 新しいファイル
-
go run
におけるシグナル処理:src/cmd/go/run.go
のrunStdin
関数内で、子プロセス(実行されるGoプログラム)を起動する直前にstartSigHandlers()
が呼び出されるようになりました。- これにより、
go run
コマンド自身がシグナルを捕捉し、子プロセスが終了するのを待つことができます。コミットメッセージにある「Ignore signals during "go run" and wait for running child process to exit」という挙動が実現されます。これは、go run
が子プロセスをフォアグラウンドで実行し、シグナルを子プロセスに転送するのではなく、親プロセスがシグナルを処理し、子プロセスのライフサイクルを管理するというアプローチです。
-
go test
におけるシグナル処理:src/cmd/go/test.go
のrunTest
関数内で、テストバイナリを実行する直前にstartSigHandlers()
が呼び出されるようになりました。- これにより、
go test
コマンドがシグナルを捕捉し、テストの実行を中断するロジックが有効になります。
-
ビルドプロセスのシグナル対応 (
src/cmd/go/build.go
):builder.do
関数は、Goコマンドのビルドプロセスにおける並行処理を管理する中心的なロジックです。- 以前は、ビルドの完了を待つために
done
チャネルを使用していましたが、このコミットではdone
チャネルが削除され、代わりにsync.WaitGroup
が導入されました。これにより、ビルドワーカーゴルーチンの終了をより適切に管理できるようになります。 - 最も重要な変更は、ビルドワーカーゴルーチン内に
select
ステートメントが導入されたことです。
このselect { case _, ok := <-b.readySema: if !ok { return } // ... 既存の処理 ... case <-interrupted: setExitStatus(1) return }
select
により、ビルドワーカーは、新しいビルドタスクが利用可能になるのを待つ(b.readySema
)だけでなく、interrupted
チャネルが閉じられる(つまり、シグナルが捕捉された)ことも監視するようになりました。 interrupted
チャネルが閉じられた場合、setExitStatus(1)
が呼び出され、Goコマンドの終了ステータスがエラー(1)に設定されます。そして、ゴルーチンは即座に終了します。これにより、「Stop executing further tests during "go test", wait for running tests to exit and report error exit code」という挙動が実現されます。ビルドワーカーが中断されることで、それ以降のテストやビルドタスクは実行されなくなります。
これらの変更により、GoコマンドはOSシグナルに対してより予測可能で、ユーザーフレンドリーな挙動を示すようになります。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下のファイルと関数に集中しています。
-
src/cmd/go/signal.go
(新規ファイル):var interrupted = make(chan struct{})
: シグナルによる中断を通知するためのチャネル。func processSignals()
:os/signal.Notify
を使ってシグナルを捕捉し、interrupted
チャネルを閉じる。func startSigHandlers()
:sync.Once
を使ってprocessSignals
が一度だけ実行されることを保証。
-
src/cmd/go/signal_notunix.go
およびsrc/cmd/go/signal_unix.go
(新規ファイル):var signalsToIgnore = []os.Signal{...}
: OSごとに無視するシグナルを定義。
-
src/cmd/go/build.go
:func (b *builder) do(root *action)
:done := make(chan bool)
の削除。var wg sync.WaitGroup
の追加と、ワーカーゴルーチンでのwg.Add(1)
とwg.Done()
の利用。- ワーカーゴルーチン内のループが
for _ = range b.readySema
からfor {}
に変更され、select
文が導入された。 select
文にcase <-interrupted:
が追加され、シグナル受信時にsetExitStatus(1)
を呼び出してゴルーチンを終了するロジックが追加された。<-done
がwg.Wait()
に置き換えられた。
-
src/cmd/go/run.go
:func runStdin(cmdargs ...interface{})
:cmd.Run()
の直前にstartSigHandlers()
の呼び出しを追加。
-
src/cmd/go/test.go
:func (b *builder) runTest(a *action) error
:tick := time.NewTimer(testKillTimeout)
の後にstartSigHandlers()
の呼び出しを追加。
コアとなるコードの解説
このコミットの核となるのは、GoコマンドがOSシグナルを捕捉し、そのシグナルに応じて内部の処理フローを制御する新しいメカニズムです。
-
シグナルハンドラの初期化 (
signal.go
):interrupted
チャネルは、シグナルが捕捉されたことをGoコマンドの他の部分に通知するためのグローバルなフラグとして機能します。これは、チャネルが閉じられることで「イベント発生」を伝えるイディオムです。processSignals()
は、os/signal.Notify
を使ってsignalsToIgnore
リストに含まれるシグナル(Unix系ではSIGINT
とSIGQUIT
、Windows/Plan9ではSIGINT
)を監視します。これらのシグナルが受信されると、interrupted
チャネルが閉じられます。startSigHandlers()
は、sync.Once
を使用してprocessSignals()
がGoコマンドのライフサイクル中に一度だけ実行されることを保証します。これは、シグナルハンドラの二重登録を防ぎ、リソースの無駄遣いを避けるために重要です。
-
ビルドプロセスのシグナル対応 (
build.go
):builder.do
関数は、Goコマンドのビルドやテスト実行の並行処理をオーケストレーションします。以前は、done
チャネルを使ってすべてのビルドタスクが完了するのを待っていましたが、このコミットではsync.WaitGroup
に置き換えられました。WaitGroup
は、複数のワーカーゴルーチンがすべて終了するのを待つためのより一般的なメカニズムです。- 最も重要な変更は、ビルドワーカーゴルーチン内の
select
文です。
このselect { case _, ok := <-b.readySema: // 新しいビルドタスクが利用可能になった場合 if !ok { return // チャネルが閉じられたら終了 } // タスクを処理 case <-interrupted: // シグナルが捕捉され、interruptedチャネルが閉じられた場合 setExitStatus(1) // 終了ステータスをエラーに設定 return // ゴルーチンを終了 }
select
文により、各ビルドワーカーゴルーチンは、新しいタスクを待つだけでなく、interrupted
チャネルからの通知も同時に監視します。シグナルが捕捉されるとinterrupted
チャネルが閉じられ、このcase
が選択されます。これにより、ワーカーは即座に現在の処理を中断し、エラー終了コードを設定して終了します。結果として、go test
中にシグナルが送られると、それ以降のテスト実行が停止し、Goコマンド全体がエラー終了します。
-
go run
とgo test
への統合 (run.go
,test.go
):go run
が子プロセスを起動する前と、go test
がテストバイナリを実行する前に、それぞれstartSigHandlers()
が呼び出されます。これにより、Goコマンドが子プロセスを起動する前にシグナルハンドリングの準備が整い、シグナルが捕捉された際に上記で説明したbuild.go
のロジックが適切に機能するようになります。
この一連の変更により、Goコマンドはユーザーからのシグナルに対してより応答性が高く、予測可能な挙動を示すようになり、開発者のワークフローが改善されます。
関連リンク
- Go CL (Change List): https://golang.org/cl/6903061
- GitHub Issue #3572:
go run
doesn't pass signals to child process: https://code.google.com/p/go/issues/detail?id=3572 (現在はGitHubに移行済み) - GitHub Issue #3581:
go test
doesn't stop on SIGINT: https://code.google.com/p/go/issues/detail?id=3581 (現在はGitHubに移行済み)
参考にした情報源リンク
- Go言語公式ドキュメント:
os/signal
パッケージ: https://pkg.go.dev/os/signal - Go言語公式ドキュメント:
sync
パッケージ: https://pkg.go.dev/sync - Unixシグナルに関する一般的な情報 (例: Wikipedia, man pages for
signal(7)
): https://ja.wikipedia.org/wiki/%E3%82%B7%E3%82%B0%E3%83%8A%E3%83%AB_(Unix) - Go言語における並行処理のパターン (チャネル、WaitGroupなど): https://go.dev/tour/concurrency/1