[インデックス 15495] ファイルの概要
このコミットは、Go言語のコマンドラインツール go run におけるCgoソースファイルの処理に関するバグを修正し、Cgoに依存するテストをより適切なテストファイルに移動させることを目的としています。具体的には、Cgoが無効な場合に go run がCgoソースファイルを適切に扱えない問題を解決し、runtime パッケージ内のCgo関連のクラッシュテストを crash_test.go から crash_cgo_test.go へと整理しています。
コミット
commit 6ecb39fce66e2fb08dea2d798822c8f0a93e29d5
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Thu Feb 28 16:07:26 2013 +0800
cmd/go: fix "go run" cgo source when cgo is disabled
also move a cgo-depend test to appropriate source file in runtime.
R=golang-dev, dave, adg, rsc
CC=golang-dev
https://golang.org/cl/7393063
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6ecb39fce66e2fb08dea2d798822c8f0a93e29d5
元コミット内容
cmd/go: fix "go run" cgo source when cgo is disabled
also move a cgo-depend test to appropriate source file in runtime.
R=golang-dev, dave, adg, rsc
CC=golang-dev
https://golang.org/cl/7393063
変更の背景
このコミットには主に二つの背景があります。
go runコマンドのCgoソースファイル処理の不備: 以前のgo runコマンドは、実行対象のソースファイルがCgoファイル(.goファイル内にimport "C"を含むファイル)であるにもかかわらず、Cgoがビルド環境で無効化されている場合に、適切なエラーメッセージを出力せずに「適切なソースファイルが見つからない」という誤ったエラーを報告する可能性がありました。これはユーザーにとって混乱を招く挙動であり、Cgoの有効/無効状態に応じた適切なフィードバックが求められていました。- テストコードの整理:
src/pkg/runtime/crash_test.goには、Cgoのシグナルハンドリングに関連するデッドロックをテストするTestCgoSignalDeadlockというテストが含まれていました。しかし、このテストはCgoに強く依存しており、crash_test.goという一般的なクラッシュテストファイルに置かれているのは適切ではありませんでした。Cgo関連のテストは、より専門的なcrash_cgo_test.goに移動させることで、テストコードの分類と保守性を向上させる必要がありました。
これらの問題に対処するため、go run のロジックが改善され、Cgo関連のテストが適切なファイルに再配置されました。
前提知識の解説
go run コマンド
go run はGo言語のソースファイルをコンパイルして実行するコマンドです。通常、単一のGoソースファイルや、main パッケージを含む複数のGoソースファイルを指定して実行します。go run は一時的な実行可能ファイルを生成し、それを実行した後、クリーンアップを行います。
Cgo
Cgoは、GoプログラムからC言語のコードを呼び出すためのGoの機能です。Goのソースファイル内で import "C" を記述することで、C言語の関数やデータ構造をGoから利用できるようになります。Cgoを使用すると、既存のCライブラリをGoプロジェクトに統合したり、Goでは実現が難しい低レベルの操作を行ったりすることが可能になります。Cgoはビルド時にCコンパイラ(通常はGCCやClang)を必要とし、GoのビルドプロセスにC言語のコンパイルステップが追加されます。
buildContext.CgoEnabled
Goのビルドシステムには、Cgoが有効になっているかどうかを示す CgoEnabled というフラグがあります。これは、環境変数 CGO_ENABLED によって制御され、CGO_ENABLED=0 に設定するとCgoが無効になります。Cgoが無効な場合、GoコンパイラはCgoのコードを処理せず、Cgoに依存するGoプログラムはビルドエラーになるか、実行時に問題を引き起こす可能性があります。
fatalf 関数
Goのコマンドラインツールでは、通常、エラーが発生した場合にプログラムを終了させるために log.Fatalf やカスタムの fatalf 関数が使用されます。これは、エラーメッセージを出力し、非ゼロの終了コードでプログラムを終了させる役割を担います。
runtime.LockOSThread()
Goのランタイムには、runtime.LockOSThread() という関数があります。これは、現在のGoルーチンを特定のOSスレッドにロックする(紐付ける)ために使用されます。一度ロックされると、そのGoルーチンは常に同じOSスレッド上で実行され、他のGoルーチンはそのスレッドを使用できなくなります。これは、Cgoとの連携や、OSスレッドの特定のプロパティ(例: スレッドローカルストレージ、シグナルハンドリング)に依存する処理を行う際に重要になります。
runtime.Gosched()
runtime.Gosched() は、現在のGoルーチンを一時停止し、他のGoルーチンが実行される機会を与えるために使用されます。これは、Goスケジューラに制御を戻し、協調的なマルチタスクを促進します。
select ステートメントと time.After()
Goの select ステートメントは、複数の通信操作(チャネルの送受信)を待機するために使用されます。case <-time.After(duration) は、指定された期間が経過した後にチャネルに値が送信されるのを待つためのイディオムです。これは、タイムアウト処理を実装する際によく使われます。
recover() 関数
recover() は、Goの組み込み関数で、panic から回復するために使用されます。defer 関数内で呼び出された場合、panic が発生したGoルーチンのパニック状態を停止し、プログラムのクラッシュを防ぎ、通常の実行を再開させることができます。
runtime.GOMAXPROCS()
runtime.GOMAXPROCS() は、Goランタイムが同時に実行できるOSスレッドの最大数を設定します。これは、GoスケジューラがGoルーチンをOSスレッドにマッピングする方法に影響を与え、並行処理のパフォーマンスに影響を与える可能性があります。
シグナルハンドリングとデッドロック
オペレーティングシステムは、プログラムに対してシグナル(例: SIGSEGV, SIGABRT)を送信して、特定のイベントやエラーを通知します。Goプログラム、特にCgoを使用している場合、CコードがOSシグナルを処理する方法とGoランタイムがシグナルを処理する方法との間で競合やデッドロックが発生する可能性があります。TestCgoSignalDeadlock は、このような複雑な状況下でのデッドロックを検出するために設計されたテストです。
技術的詳細
cmd/go/run.go の変更
このコミットの主要な変更点の一つは、go run コマンドがソースファイルを特定するロジックの改善です。
変更前は、p.GoFiles(通常のGoソースファイル)が空の場合、無条件に p.CgoFiles[0] を src として選択していました。これは、Cgoファイルが存在すればそれを実行対象と見なすという単純なロジックでした。
変更後は、p.GoFiles が空の場合に p.CgoFiles が空でないかをチェックするようになりました。
- もし
p.CgoFilesが空でなければ、引き続きp.CgoFiles[0]をsrcとして選択します。 - しかし、
p.GoFilesもp.CgoFilesも両方とも空の場合、つまりgo runに渡されたファイルがGoソースファイルでもCgoソースファイルでもない場合、またはCgoソースファイルだがCgoが無効になっているために認識されない場合に、エラーを報告するようになりました。
このエラー報告の際に、buildContext.CgoEnabled をチェックし、Cgoが無効になっている場合に「(cgo is disabled)」というヒントをエラーメッセージに追加するようになりました。これにより、ユーザーはなぜ go run がソースファイルを認識できないのか、より明確な理由を理解できるようになります。これは、Cgoが有効な環境と無効な環境の両方で go run の挙動をより堅牢にするための重要な改善です。
テストファイルの移動と整理
もう一つの重要な変更は、TestCgoSignalDeadlock テストとその関連ソースコード cgoSignalDeadlockSource の移動です。
- 移動元:
src/pkg/runtime/crash_test.go - 移動先:
src/pkg/runtime/crash_cgo_test.go
このテストは、CgoとGoランタイムのシグナルハンドリングが複雑に絡み合う状況でデッドロックが発生しないことを確認するためのものです。import "C" を含み、runtime.LockOSThread() を多用していることから、Cgoに特化したテストであることは明らかです。
crash_test.go はGoランタイムの一般的なクラッシュテストを扱うファイルであるのに対し、crash_cgo_test.go はCgoに関連するクラッシュやデッドロックのテストを専門に扱います。この移動により、テストコードの論理的な分類が改善され、Cgo関連のテストがより適切な場所に配置されることで、コードベースの可読性と保守性が向上しました。
cgoSignalDeadlockSource は、TestCgoSignalDeadlock テスト内で executeTest 関数に渡される文字列定数で、テスト対象となるGoプログラムのソースコードを直接埋め込んでいます。この埋め込みコードは、多数のGoルーチンを起動し、runtime.LockOSThread() を繰り返し呼び出し、チャネル通信と panic/recover を組み合わせることで、CgoとGoランタイム間の複雑な相互作用をシミュレートし、シグナルハンドリングにおけるデッドロックの可能性を検証しています。
コアとなるコードの変更箇所
src/cmd/go/run.go
--- a/src/cmd/go/run.go
+++ b/src/cmd/go/run.go
@@ -68,8 +68,16 @@ func runRun(cmd *Command, args []string) {
var src string
if len(p.GoFiles) > 0 {
src = p.GoFiles[0]
- } else {
+ } else if len(p.CgoFiles) > 0 {
src = p.CgoFiles[0]
+ } else {
+ // this case could only happen if the provided source uses cgo
+ // while cgo is disabled.
+ hint := ""
+ if !buildContext.CgoEnabled {
+ hint = " (cgo is disabled)"
+ }
+ fatalf("go run: no suitable source files%s", hint)
}
p.exeName = src[:len(src)-len(".go")] // name temporary executable for first go file
a1 := b.action(modeBuild, modeBuild, p)
src/pkg/runtime/crash_cgo_test.go (追加)
--- /dev/null
+++ b/src/pkg/runtime/crash_cgo_test.go
@@ -0,0 +1,76 @@
+package runtime_test
+
+import (
+ "testing"
+)
+
+func TestCgoCrashHandler(t *testing.T) {
+ testCrashHandler(t, true)
+}
+
+func TestCgoSignalDeadlock(t *testing.T) {
+ got := executeTest(t, cgoSignalDeadlockSource, nil)
+ want := "OK\n"
+ if got != want {
+ t.Fatalf("expected %q, but got %q", want, got)
+ }
+}
+
+const cgoSignalDeadlockSource = `
+package main
+
+import "C"
+
+import (
+ "fmt"
+ "runtime"
+ "time"
+)
+
+func main() {
+ runtime.GOMAXPROCS(100)
+ ping := make(chan bool)
+ go func() {
+ for i := 0; ; i++ {
+ runtime.Gosched()
+ select {
+ case done := <-ping:
+ if done {
+ ping <- true
+ return
+ }
+ ping <- true
+ default:
+ }
+ func() {
+ defer func() {
+ recover()
+ }()
+ var s *string
+ *s = ""
+ }()
+ }
+ }()
+ time.Sleep(time.Millisecond)
+ for i := 0; i < 64; i++ {
+ go func() {
+ runtime.LockOSThread()
+ select {}
+ }()
+ go func() {
+ runtime.LockOSThread()
+ select {}
+ }()
+ time.Sleep(time.Millisecond)
+ ping <- false
+ select {
+ case <-ping:
+ case <-time.After(time.Second):
+ fmt.Printf("HANG\n")
+ return
+ }
+ }
+ ping <- true
+ select {
+ case <-ping:
+ case <-time.After(time.Second):
+ fmt.Printf("HANG\n")
+ return
+ }
+ fmt.Printf("OK\n")
+}
+`
src/pkg/runtime/crash_test.go (削除)
--- a/src/pkg/runtime/crash_test.go
+++ b/src/pkg/runtime/crash_test.go
@@ -99,14 +99,6 @@ func TestLockedDeadlock2(t *testing.T) {
testDeadlock(t, lockedDeadlockSource2)
}
-func TestCgoSignalDeadlock(t *testing.T) {
- got := executeTest(t, cgoSignalDeadlockSource, nil)
- want := "OK\n"
- if got != want {
- t.Fatalf("expected %q, but got %q", want, got)
- }
-}
-
const crashSource = `
package main
@@ -191,68 +183,3 @@ func main() {
select {}\n
}
`
-
-const cgoSignalDeadlockSource = `
-package main
-
-import "C"
-
-import (
- "fmt"
- "runtime"
- "time"
-)
-
-func main() {
- runtime.GOMAXPROCS(100)
- ping := make(chan bool)
- go func() {
- for i := 0; ; i++ {
- runtime.Gosched()
- select {
- case done := <-ping:
- if done {
- ping <- true
- return
- }
- ping <- true
- default:
- }
- func() {
- defer func() {
- recover()
- }()
- var s *string
- *s = ""
- }()
- }
- }()
- time.Sleep(time.Millisecond)
- for i := 0; i < 64; i++ {
- go func() {
- runtime.LockOSThread()
- select {}
- }()
- go func() {
- runtime.LockOSThread()
- select {}
- }()
- time.Sleep(time.Millisecond)
- ping <- false
- select {
- case <-ping:
- case <-time.After(time.Second):
- fmt.Printf("HANG\n")
- return
- }
- }
- ping <- true
- select {
- case <-ping:
- case <-time.After(time.Second):
- fmt.Printf("HANG\n")
- return
- }
- fmt.Printf("OK\n")
-}
-`
コアとなるコードの解説
src/cmd/go/run.go の変更点
else if len(p.CgoFiles) > 0の追加:- これは、
go runが実行対象のGoソースファイル (p.GoFiles) を見つけられなかった場合に、次にCgoソースファイル (p.CgoFiles) が存在するかどうかを確認する新しい条件分岐です。これにより、Cgoファイルがプロジェクトの主要なエントリポイントである場合に、go runがそれを正しく認識できるようになります。
- これは、
- 新しい
elseブロックとエラーハンドリング:p.GoFilesもp.CgoFilesも両方とも空の場合にこのブロックが実行されます。これは、go runに渡された引数から実行可能なソースファイルが見つからなかったことを意味します。hint := ""とif !buildContext.CgoEnabled { hint = " (cgo is disabled)" }の行は、Cgoが無効になっている場合に、エラーメッセージにその旨のヒントを追加するためのものです。fatalf("go run: no suitable source files%s", hint)は、適切なソースファイルが見つからなかったことをユーザーに通知し、Cgoが無効な場合はその理由も併記することで、デバッグを容易にします。
この変更により、go run はCgoソースファイルをより適切に扱い、Cgoの有効/無効状態に応じた、より分かりやすいエラーメッセージを提供するようになりました。
src/pkg/runtime/crash_cgo_test.go と src/pkg/runtime/crash_test.go の変更点
TestCgoSignalDeadlockとcgoSignalDeadlockSourceの移動:src/pkg/runtime/crash_test.goからTestCgoSignalDeadlock関数とそのテスト対象となるソースコードcgoSignalDeadlockSourceが完全に削除されました。- これらのコードは
src/pkg/runtime/crash_cgo_test.goにそのまま追加されました。 - この移動は、テストの分類を改善し、Cgoに特化したテストをCgo関連のテストファイルに集約することを目的としています。これにより、Goランタイムのテストスイート全体の構造がより明確になり、特定の機能に関連するテストを見つけやすくなります。
cgoSignalDeadlockSource 内のコードは、Goルーチン、チャネル、runtime.LockOSThread()、panic/recover、そして time.After を組み合わせることで、GoランタイムとCgoがシグナルを処理する際の複雑な相互作用をシミュレートしています。特に、runtime.LockOSThread() を多数のGoルーチンで呼び出すことで、OSスレッドの枯渇やシグナルハンドリングの競合状態を意図的に作り出し、デッドロックが発生しないことを検証しています。fmt.Printf("HANG\\n") は、デッドロックが発生してテストがタイムアウトした場合に、その旨を報告するためのものです。
関連リンク
- Go言語のCgoに関する公式ドキュメント: https://go.dev/blog/c-go-is-not-go (Cgoの概念について)
- Goコマンドのドキュメント: https://go.dev/cmd/go/
- Goのランタイムパッケージドキュメント: https://go.dev/pkg/runtime/
参考にした情報源リンク
- Go Gerrit Code Review: https://golang.org/cl/7393063 (このコミットの元のコードレビューページ)