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

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

このコミットは、Goコマンド(cmd/go)におけるOSシグナル(特にSIGINTSIGQUIT)のハンドリングを改善するものです。具体的には、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 rungo testの挙動

  • go run: 指定されたGoソースファイルをコンパイルし、その結果生成された実行可能ファイルを一時ディレクトリに配置して実行します。通常、この実行可能ファイルはgo runコマンドの子プロセスとして起動されます。
  • go test: Goパッケージのテストを実行します。テストは通常、コンパイルされたテストバイナリとして実行され、これもgo testコマンドの子プロセスとして起動されます。

これらのコマンドは、ユーザーがCLIからGoプログラムやテストを直接操作するための主要なインターフェースであり、シグナルハンドリングの改善は、これらのコマンドのユーザーエクスペリエンスに直接影響します。

技術的詳細

このコミットの技術的な核心は、GoコマンドがOSシグナルを捕捉し、そのシグナルに応じてgo rungo testの挙動を調整するメカニズムを導入した点にあります。

  1. シグナルハンドリングの共通化とプラットフォーム依存性の分離:

    • 新しいファイルsrc/cmd/go/signal.goが導入され、シグナルハンドリングの共通ロジックがカプセル化されました。
    • interruptedというchan struct{}型のチャネルが定義され、シグナルが捕捉された際にこのチャネルが閉じられることで、他のゴルーチンに中断を通知するメカニズムが提供されます。
    • processSignals()関数は、os/signalパッケージを使用して、特定のシグナル(signalsToIgnoreで定義される)を捕捉し、interruptedチャネルを閉じます。
    • startSigHandlers()関数は、sync.Onceを使用してprocessSignals()が一度だけ実行されることを保証します。これにより、複数の場所からシグナルハンドラが起動されても、二重登録を防ぎます。
    • src/cmd/go/signal_notunix.gosrc/cmd/go/signal_unix.goが導入され、プラットフォーム(Windows/Plan9とUnix系)ごとに無視すべきシグナルのリスト(signalsToIgnore)が定義されました。Unix系ではos.Interruptsyscall.SIGQUITが、Windows/Plan9ではos.Interruptのみが対象となります。これは、OSによって利用可能なシグナルが異なるためです。
  2. go runにおけるシグナル処理:

    • src/cmd/go/run.gorunStdin関数内で、子プロセス(実行されるGoプログラム)を起動する直前にstartSigHandlers()が呼び出されるようになりました。
    • これにより、go runコマンド自身がシグナルを捕捉し、子プロセスが終了するのを待つことができます。コミットメッセージにある「Ignore signals during "go run" and wait for running child process to exit」という挙動が実現されます。これは、go runが子プロセスをフォアグラウンドで実行し、シグナルを子プロセスに転送するのではなく、親プロセスがシグナルを処理し、子プロセスのライフサイクルを管理するというアプローチです。
  3. go testにおけるシグナル処理:

    • src/cmd/go/test.gorunTest関数内で、テストバイナリを実行する直前にstartSigHandlers()が呼び出されるようになりました。
    • これにより、go testコマンドがシグナルを捕捉し、テストの実行を中断するロジックが有効になります。
  4. ビルドプロセスのシグナル対応 (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シグナルに対してより予測可能で、ユーザーフレンドリーな挙動を示すようになります。

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

このコミットにおけるコアとなるコードの変更は、主に以下のファイルと関数に集中しています。

  1. src/cmd/go/signal.go (新規ファイル):

    • var interrupted = make(chan struct{}): シグナルによる中断を通知するためのチャネル。
    • func processSignals(): os/signal.Notifyを使ってシグナルを捕捉し、interruptedチャネルを閉じる。
    • func startSigHandlers(): sync.Onceを使ってprocessSignalsが一度だけ実行されることを保証。
  2. src/cmd/go/signal_notunix.go および src/cmd/go/signal_unix.go (新規ファイル):

    • var signalsToIgnore = []os.Signal{...}: OSごとに無視するシグナルを定義。
  3. 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)を呼び出してゴルーチンを終了するロジックが追加された。
      • <-donewg.Wait()に置き換えられた。
  4. src/cmd/go/run.go:

    • func runStdin(cmdargs ...interface{}): cmd.Run()の直前にstartSigHandlers()の呼び出しを追加。
  5. src/cmd/go/test.go:

    • func (b *builder) runTest(a *action) error: tick := time.NewTimer(testKillTimeout)の後にstartSigHandlers()の呼び出しを追加。

コアとなるコードの解説

このコミットの核となるのは、GoコマンドがOSシグナルを捕捉し、そのシグナルに応じて内部の処理フローを制御する新しいメカニズムです。

  1. シグナルハンドラの初期化 (signal.go):

    • interruptedチャネルは、シグナルが捕捉されたことをGoコマンドの他の部分に通知するためのグローバルなフラグとして機能します。これは、チャネルが閉じられることで「イベント発生」を伝えるイディオムです。
    • processSignals()は、os/signal.Notifyを使ってsignalsToIgnoreリストに含まれるシグナル(Unix系ではSIGINTSIGQUIT、Windows/Plan9ではSIGINT)を監視します。これらのシグナルが受信されると、interruptedチャネルが閉じられます。
    • startSigHandlers()は、sync.Onceを使用してprocessSignals()がGoコマンドのライフサイクル中に一度だけ実行されることを保証します。これは、シグナルハンドラの二重登録を防ぎ、リソースの無駄遣いを避けるために重要です。
  2. ビルドプロセスのシグナル対応 (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コマンド全体がエラー終了します。
  3. go rungo testへの統合 (run.go, test.go):

    • go runが子プロセスを起動する前と、go testがテストバイナリを実行する前に、それぞれstartSigHandlers()が呼び出されます。これにより、Goコマンドが子プロセスを起動する前にシグナルハンドリングの準備が整い、シグナルが捕捉された際に上記で説明したbuild.goのロジックが適切に機能するようになります。

この一連の変更により、Goコマンドはユーザーからのシグナルに対してより応答性が高く、予測可能な挙動を示すようになり、開発者のワークフローが改善されます。

関連リンク

参考にした情報源リンク