[インデックス 12667] ファイルの概要
このコミットは、Go言語のコマンドラインツール cmd/go において、cgo 実行時に稀に発生する ETXTBSY (text file busy) エラーを回避するための修正です。具体的には、build.go ファイル内の runOut 関数が変更され、cgo バイナリの実行に失敗した場合にリトライロジックが導入されています。
コミット
commit a4b2c5efbc259c7d23159d304f9cb4266cd64643
Author: Russ Cox <rsc@golang.org>
Date: Fri Mar 16 10:44:09 2012 -0400
cmd/go: work around occasional ETXTBSY running cgo
Fixes #3001. (This time for sure!)
R=golang-dev, r, fullung
CC=golang-dev
https://golang.org/cl/5845044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a4b2c5efbc259c7d23159d304f9cb4266cd64643
元コミット内容
cmd/go: work around occasional ETXTBSY running cgo
Fixes #3001. (This time for sure!)
このコミットメッセージは、cmd/go ツールが cgo を実行する際に時折発生する ETXTBSY エラーを回避するための対応であることを示しています。また、GoのIssueトラッカーのIssue 3001を修正するものであることが明記されており、「今度こそは!」という表現から、過去にも同様の問題に対する試みがあったことが示唆されます。
変更の背景
この変更の背景には、Unix系システムにおける ETXTBSY エラーという特定の問題があります。ETXTBSY は "text file busy" の略で、実行中のプログラムのバイナリファイルが、別のプロセスによって書き込みのために開かれている場合に発生するエラーです。
Goのビルドプロセス、特にcgo(C言語のコードをGoから呼び出すためのツール)を使用する際に、この問題が発生することがありました。cmd/go は cgo コマンドをビルドし、その直後に実行しようとします。しかし、稀に、cgo バイナリが書き込まれた直後に、別のプロセス(例えば、cmd/go 自身がフォークした子プロセス)がそのバイナリファイルを参照している状態になり、exec システムコールが ETXTBSY エラーを返すことがありました。
コミットメッセージの「(This time for sure!)」という記述は、この問題が以前にも報告され(Issue 3001)、過去にも修正が試みられたものの、完全に解決されていなかったことを示唆しています。このコミットは、より堅牢な解決策を提供することを目的としています。
前提知識の解説
ETXTBSY (Text file busy) エラー
ETXTBSY は、Unix系オペレーティングシステムで発生するエラーコードの一つで、"Text file busy" を意味します。これは、実行中のプログラムのバイナリファイル(テキストセグメント)が、別のプロセスによって書き込みのために開かれている場合に、そのバイナリを実行しようとすると発生します。
具体的には、以下のようなシナリオで発生し得ます。
- 実行中のバイナリの更新: プログラムが自身を実行中に、そのバイナリファイルを更新しようとすると発生します。
- 共有ライブラリのロック: 共有ライブラリがロードされている間に、そのライブラリファイルを更新しようとすると発生します。
- フォークとexecの競合:
fork()システムコールで子プロセスが作成され、親プロセスがexec()システムコールで新しいプログラムを実行しようとする際に、子プロセスがまだ親プロセスのバイナリファイルディスクリプタを保持している場合に発生する可能性があります。今回のGoのケースはこれに該当します。
fork() と exec() システムコール
Unix系システムで新しいプログラムを実行する際の基本的なメカニズムです。
fork(): 現在のプロセス(親プロセス)のほぼ完全なコピーである新しいプロセス(子プロセス)を作成します。子プロセスは親プロセスのメモリ空間、ファイルディスクリプタなどを継承します。exec()(またはexecve()): 現在のプロセスイメージを、指定された新しいプログラムのイメージで置き換えます。exec()が成功すると、現在のプロセスは新しいプログラムに変わりますが、プロセスIDは変更されません。
cmd.Run() は内部的に fork() と exec() を使用します。問題は、fork() された子プロセスが exec() を呼び出すまでの短い期間、親プロセスのファイルディスクリプタを継承している点にあります。もしこの間に親プロセスが cgo バイナリを書き込み、その直後に実行しようとすると、子プロセスがまだ cgo バイナリのファイルディスクリプタを保持しているために ETXTBSY が発生する可能性があります。
close-on-exec フラグ
ファイルディスクリプタに設定できるフラグの一つで、exec() システムコールが成功した際に、そのディスクリプタを自動的に閉じるように指定します。これにより、子プロセスが不必要に親プロセスのファイルディスクリプタを継承することを防ぎ、リソースリークや意図しないファイルロックを回避できます。
今回のコミットのコメントでは、cgo のファイルディスクリプタが close-on-exec であるにもかかわらず ETXTBSY が発生する理由について詳しく説明されています。これは、fork() された子プロセスが exec() を呼び出すまでの間、一時的にディスクリプタを保持してしまうためです。
技術的詳細
このコミットは、src/cmd/go/build.go ファイル内の builder.runOut 関数にリトライロジックを追加することで ETXTBSY エラーを回避します。
runOut 関数は、指定されたディレクトリでコマンドを実行し、その標準出力と標準エラー出力をバイト配列として返す役割を担っています。
変更前は、exec.Command を作成し、cmd.Run() を一度だけ実行していました。エラーが発生した場合、そのエラーがそのまま返されていました。
変更後は、cmd.Run() の実行をループ内に配置し、ETXTBSY エラーが発生した場合に特定の条件でリトライするようになりました。
具体的には以下のロジックが追加されています。
nbusyというカウンタ変数を導入し、リトライ回数を追跡します。cmd.Run()がエラーを返し、かつnbusyが3未満であり、かつエラーメッセージに"text file busy"という文字列が含まれている場合、リトライ処理に入ります。- リトライ時には、
time.Sleepを使用して短い時間(100ms, 200ms, 400ms)待機します。待機時間はnbusyの値に応じて指数関数的に増加します (100 * time.Millisecond << uint(nbusy))。 nbusyをインクリメントし、ループの先頭に戻ってコマンドを再実行します。- 上記以外のエラー、または3回のリトライを超えた場合は、エラーをそのまま返します。
このアプローチは、ETXTBSY が一時的な競合状態によって発生するという仮定に基づいています。少し待機して再試行することで、競合状態が解消され、コマンドが正常に実行される可能性が高まります。
コミットのコメントでは、このリトライロジックが最も信頼性の高いオプションであると説明されています。cmd.Start と cmd.Wait を分割し、RWLock を使用する代替案も検討されたようですが、exec がコミットされるまでの短い期間にファイルディスクリプタが保持される可能性が完全に排除できないため、採用されませんでした。
コアとなるコードの変更箇所
src/cmd/go/build.go ファイルの builder.runOut 関数が変更されています。
--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -21,6 +21,7 @@ import (
"runtime"
"strings"
"sync"
+ "time"
)
var cmdBuild = &Command{
@@ -1047,14 +1048,66 @@ func (b *builder) runOut(dir string, desc string, cmdargs ...interface{}) ([]byt
}
}
- var buf bytes.Buffer
- cmd := exec.Command(cmdline[0], cmdline[1:]...)
- cmd.Stdout = &buf
- cmd.Stderr = &buf
- cmd.Dir = dir
- // TODO: cmd.Env
- err := cmd.Run()
- return buf.Bytes(), err
+ nbusy := 0
+ for {
+ var buf bytes.Buffer
+ cmd := exec.Command(cmdline[0], cmdline[1:]...)
+ cmd.Stdout = &buf
+ cmd.Stderr = &buf
+ cmd.Dir = dir
+ // TODO: cmd.Env
+ err := cmd.Run()
+
+ // cmd.Run will fail on Unix if some other process has the binary
+ // we want to run open for writing. This can happen here because
+ // we build and install the cgo command and then run it.
+ // If another command was kicked off while we were writing the
+ // cgo binary, the child process for that command may be holding
+ // a reference to the fd, keeping us from running exec.
+ //
+ // But, you might reasonably wonder, how can this happen?
+ // The cgo fd, like all our fds, is close-on-exec, so that we need
+ // not worry about other processes inheriting the fd accidentally.
+ // The answer is that running a command is fork and exec.
+ // A child forked while the cgo fd is open inherits that fd.
+ // Until the child has called exec, it holds the fd open and the
+ // kernel will not let us run cgo. Even if the child were to close
+ // the fd explicitly, it would still be open from the time of the fork
+ // until the time of the explicit close, and the race would remain.
+ //
+ // On Unix systems, this results in ETXTBSY, which formats
+ // as "text file busy". Rather than hard-code specific error cases,
+ // we just look for that string. If this happens, sleep a little
+ // and try again. We let this happen three times, with increasing
+ // sleep lengths: 100+200+400 ms = 0.7 seconds.
+ //
+ // An alternate solution might be to split the cmd.Run into
+ // separate cmd.Start and cmd.Wait, and then use an RWLock
+ // to make sure that copyFile only executes when no cmd.Start
+ // call is in progress. However, cmd.Start (really syscall.forkExec)
+ // only guarantees that when it returns, the exec is committed to
+ // happen and succeed. It uses a close-on-exec file descriptor
+ // itself to determine this, so we know that when cmd.Start returns,
+ // at least one close-on-exec file descriptor has been closed.
+ // However, we cannot be sure that all of them have been closed,
+ // so the program might still encounter ETXTBSY even with such
+ // an RWLock. The race window would be smaller, perhaps, but not
+ // guaranteed to be gone.
+ //
+ // Sleeping when we observe the race seems to be the most reliable
+ // option we have.
+ //
+ // http://golang.org/issue/3001
+ //
+ if err != nil && nbusy < 3 && strings.Contains(err.Error(), "text file busy") {
+ time.Sleep(100 * time.Millisecond << uint(nbusy))
+ nbusy++
+ continue
+ }
+
+ return buf.Bytes(), err
+ }
+ panic("unreachable")
}
// mkdir makes the named directory.
コアとなるコードの解説
変更された builder.runOut 関数は、コマンド実行の堅牢性を高めるために、ETXTBSY エラーに対するリトライメカニズムを実装しています。
import "time"の追加:time.Sleep関数を使用するために、timeパッケージがインポートされています。- ループの導入:
forループが導入され、コマンドの実行が複数回試行される可能性があります。 nbusyカウンタ:nbusy変数は、ETXTBSYエラーによるリトライの回数を追跡します。最大3回のリトライが許可されます。- エラーチェックとリトライ条件:
err != nil: コマンド実行がエラーを返した場合。nbusy < 3: リトライ回数が3回未満である場合。strings.Contains(err.Error(), "text file busy"): エラーメッセージに"text file busy"という文字列が含まれている場合。これはETXTBSYエラーを識別するためのヒューリスティックな方法です。
- 待機とリトライ: 上記の条件がすべて満たされた場合、以下の処理が行われます。
time.Sleep(100 * time.Millisecond << uint(nbusy)): 指数バックオフ戦略で待機します。nbusyが0の場合は100ms、1の場合は200ms、2の場合は400ms待機します。これにより、連続するリトライの間隔が徐々に長くなり、競合状態が解消される可能性が高まります。nbusy++: リトライカウンタをインクリメントします。continue: ループの次のイテレーションに進み、コマンドを再実行します。
- 正常終了または非リトライエラー: リトライ条件が満たされない場合(コマンドが成功したか、リトライ対象外のエラーが発生したか、リトライ回数が上限に達した場合)、
buf.Bytes(), errが返され、関数が終了します。 panic("unreachable"): ループの最後にpanic("unreachable")が追加されています。これは、ループが無限に続くことはないという保証を示すもので、通常は到達しないコードパスであることを示します。
この変更により、cgo のようなバイナリのビルドと実行が連続して行われるようなシナリオで、一時的なファイルロックによるエラーが原因でビルドが失敗するのを防ぎ、Goのビルドシステムの堅牢性が向上しました。
関連リンク
- Go Issue 3001: https://github.com/golang/go/issues/3001 (このコミットが修正したIssue)
- Go CL 5845044: https://golang.org/cl/5845044 (このコミットのChange List)
参考にした情報源リンク
ETXTBSYエラーに関する一般的な情報- Unixの
fork()とexec()システムコールに関する情報 - Go言語の
os/execパッケージのドキュメント - Go言語のIssueトラッカー (Issue 3001の内容確認)
- Go言語のChange List (CL 5845044の内容確認)
- 指数バックオフ (Exponential Backoff) 戦略に関する情報# [インデックス 12667] ファイルの概要
このコミットは、Go言語のコマンドラインツール cmd/go において、cgo 実行時に稀に発生する ETXTBSY (text file busy) エラーを回避するための修正です。具体的には、build.go ファイル内の runOut 関数が変更され、cgo バイナリの実行に失敗した場合にリトライロジックが導入されています。
コミット
commit a4b2c5efbc259c7d23159d304f9cb4266cd64643
Author: Russ Cox <rsc@golang.org>
Date: Fri Mar 16 10:44:09 2012 -0400
cmd/go: work around occasional ETXTBSY running cgo
Fixes #3001. (This time for sure!)
R=golang-dev, r, fullung
CC=golang-dev
https://golang.org/cl/5845044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a4b2c5efbc259c7d23159d304f9cb4266cd64643
元コミット内容
cmd/go: work around occasional ETXTBSY running cgo
Fixes #3001. (This time for sure!)
このコミットメッセージは、cmd/go ツールが cgo を実行する際に時折発生する ETXTBSY エラーを回避するための対応であることを示しています。また、GoのIssueトラッカーのIssue 3001を修正するものであることが明記されており、「今度こそは!」という表現から、過去にも同様の問題に対する試みがあったことが示唆されます。
変更の背景
この変更の背景には、Unix系システムにおける ETXTBSY エラーという特定の問題があります。ETXTBSY は "text file busy" の略で、実行中のプログラムのバイナリファイルが、別のプロセスによって書き込みのために開かれている場合に発生するエラーです。
Goのビルドプロセス、特にcgo(C言語のコードをGoから呼び出すためのツール)を使用する際に、この問題が発生することがありました。cmd/go は cgo コマンドをビルドし、その直後に実行しようとします。しかし、稀に、cgo バイナリが書き込まれた直後に、別のプロセス(例えば、cmd/go 自身がフォークした子プロセス)がそのバイナリファイルを参照している状態になり、exec システムコールが ETXTBSY エラーを返すことがありました。
コミットメッセージの「(This time for sure!)」という記述は、この問題が以前にも報告され(Issue 3001)、過去にも修正が試みられたものの、完全に解決されていなかったことを示唆しています。このコミットは、より堅牢な解決策を提供することを目的としています。
前提知識の解説
ETXTBSY (Text file busy) エラー
ETXTBSY は、Unix系オペレーティングシステムで発生するエラーコードの一つで、"Text file busy" を意味します。これは、実行中のプログラムのバイナリファイル(テキストセグメント)が、別のプロセスによって書き込みのために開かれている場合に、そのバイナリを実行しようとすると発生します。
具体的には、以下のようなシナリオで発生し得ます。
- 実行中のバイナリの更新: プログラムが自身を実行中に、そのバイナリファイルを更新しようとすると発生します。
- 共有ライブラリのロック: 共有ライブラリがロードされている間に、そのライブラリファイルを更新しようとすると発生します。
- フォークとexecの競合:
fork()システムコールで子プロセスが作成され、親プロセスがexec()システムコールで新しいプログラムを実行しようとする際に、子プロセスがまだ親プロセスのバイナリファイルディスクリプタを保持している場合に発生する可能性があります。今回のGoのケースはこれに該当します。
fork() と exec() システムコール
Unix系システムで新しいプログラムを実行する際の基本的なメカニズムです。
fork(): 現在のプロセス(親プロセス)のほぼ完全なコピーである新しいプロセス(子プロセス)を作成します。子プロセスは親プロセスのメモリ空間、ファイルディスクリプタなどを継承します。exec()(またはexecve()): 現在のプロセスイメージを、指定された新しいプログラムのイメージで置き換えます。exec()が成功すると、現在のプロセスは新しいプログラムに変わりますが、プロセスIDは変更されません。
cmd.Run() は内部的に fork() と exec() を使用します。問題は、fork() された子プロセスが exec() を呼び出すまでの短い期間、親プロセスのファイルディスクリプタを継承している点にあります。もしこの間に親プロセスが cgo バイナリを書き込み、その直後に実行しようとすると、子プロセスがまだ cgo バイナリのファイルディスクリプタを保持しているために ETXTBSY が発生する可能性があります。
close-on-exec フラグ
ファイルディスクリプタに設定できるフラグの一つで、exec() システムコールが成功した際に、そのディスクリプタを自動的に閉じるように指定します。これにより、子プロセスが不必要に親プロセスのファイルディスクリプタを継承することを防ぎ、リソースリークや意図しないファイルロックを回避できます。
今回のコミットのコメントでは、cgo のファイルディスクリプタが close-on-exec であるにもかかわらず ETXTBSY が発生する理由について詳しく説明されています。これは、fork() された子プロセスが exec() を呼び出すまでの間、一時的にディスクリプタを保持してしまうためです。
技術的詳細
このコミットは、src/cmd/go/build.go ファイル内の builder.runOut 関数にリトライロジックを追加することで ETXTBSY エラーを回避します。
runOut 関数は、指定されたディレクトリでコマンドを実行し、その標準出力と標準エラー出力をバイト配列として返す役割を担っています。
変更前は、exec.Command を作成し、cmd.Run() を一度だけ実行していました。エラーが発生した場合、そのエラーがそのまま返されていました。
変更後は、cmd.Run() の実行をループ内に配置し、ETXTBSY エラーが発生した場合に特定の条件でリトライするようになりました。
具体的には以下のロジックが追加されています。
nbusyというカウンタ変数を導入し、リトライ回数を追跡します。cmd.Run()がエラーを返し、かつnbusyが3未満であり、かつエラーメッセージに"text file busy"という文字列が含まれている場合、リトライ処理に入ります。- リトライ時には、
time.Sleepを使用して短い時間(100ms, 200ms, 400ms)待機します。待機時間はnbusyの値に応じて指数関数的に増加します (100 * time.Millisecond << uint(nbusy))。 nbusyをインクリメントし、ループの先頭に戻ってコマンドを再実行します。- 上記以外のエラー、または3回のリトライを超えた場合は、エラーをそのまま返します。
このアプローチは、ETXTBSY が一時的な競合状態によって発生するという仮定に基づいています。少し待機して再試行することで、競合状態が解消され、コマンドが正常に実行される可能性が高まります。
コミットのコメントでは、このリトライロジックが最も信頼性の高いオプションであると説明されています。cmd.Start と cmd.Wait を分割し、RWLock を使用する代替案も検討されたようですが、exec がコミットされるまでの短い期間にファイルディスクリプタが保持される可能性が完全に排除できないため、採用されませんでした。
コアとなるコードの変更箇所
src/cmd/go/build.go ファイルの builder.runOut 関数が変更されています。
--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -21,6 +21,7 @@ import (
"runtime"
"strings"
"sync"
+ "time"
)
var cmdBuild = &Command{
@@ -1047,14 +1048,66 @@ func (b *builder) runOut(dir string, desc string, cmdargs ...interface{}) ([]byt
}
}
- var buf bytes.Buffer
- cmd := exec.Command(cmdline[0], cmdline[1:]...)
- cmd.Stdout = &buf
- cmd.Stderr = &buf
- cmd.Dir = dir
- // TODO: cmd.Env
- err := cmd.Run()
- return buf.Bytes(), err
+ nbusy := 0
+ for {
+ var buf bytes.Buffer
+ cmd := exec.Command(cmdline[0], cmdline[1:]...)
+ cmd.Stdout = &buf
+ cmd.Stderr = &buf
+ cmd.Dir = dir
+ // TODO: cmd.Env
+ err := cmd.Run()
+
+ // cmd.Run will fail on Unix if some other process has the binary
+ // we want to run open for writing. This can happen here because
+ // we build and install the cgo command and then run it.
+ // If another command was kicked off while we were writing the
+ // cgo binary, the child process for that command may be holding
+ // a reference to the fd, keeping us from running exec.
+ //
+ // But, you might reasonably wonder, how can this happen?
+ // The cgo fd, like all our fds, is close-on-exec, so that we need
+ // not worry about other processes inheriting the fd accidentally.
+ // The answer is that running a command is fork and exec.
+ // A child forked while the cgo fd is open inherits that fd.
+ // Until the child has called exec, it holds the fd open and the
+ // kernel will not let us run cgo. Even if the child were to close
+ // the fd explicitly, it would still be open from the time of the fork
+ // until the time of the explicit close, and the race would remain.
+ //
+ // On Unix systems, this results in ETXTBSY, which formats
+ // as "text file busy". Rather than hard-code specific error cases,
+ // we just look for that string. If this happens, sleep a little
+ // and try again. We let this happen three times, with increasing
+ // sleep lengths: 100+200+400 ms = 0.7 seconds.
+ //
+ // An alternate solution might be to split the cmd.Run into
+ // separate cmd.Start and cmd.Wait, and then use an RWLock
+ // to make sure that copyFile only executes when no cmd.Start
+ // call is in progress. However, cmd.Start (really syscall.forkExec)
+ // only guarantees that when it returns, the exec is committed to
+ // happen and succeed. It uses a close-on-exec file descriptor
+ // itself to determine this, so we know that when cmd.Start returns,
+ // at least one close-on-exec file descriptor has been closed.
+ // However, we cannot be sure that all of them have been closed,
+ // so the program might still encounter ETXTBSY even with such
+ // an RWLock. The race window would be smaller, perhaps, but not
+ // guaranteed to be gone.
+ //
+ // Sleeping when we observe the race seems to be the most reliable
+ // option we have.
+ //
+ // http://golang.org/issue/3001
+ //
+ if err != nil && nbusy < 3 && strings.Contains(err.Error(), "text file busy") {
+ time.Sleep(100 * time.Millisecond << uint(nbusy))
+ nbusy++
+ continue
+ }
+
+ return buf.Bytes(), err
+ }
+ panic("unreachable")
}
// mkdir makes the named directory.
コアとなるコードの解説
変更された builder.runOut 関数は、コマンド実行の堅牢性を高めるために、ETXTBSY エラーに対するリトライメカニズムを実装しています。
import "time"の追加:time.Sleep関数を使用するために、timeパッケージがインポートされています。- ループの導入:
forループが導入され、コマンドの実行が複数回試行される可能性があります。 nbusyカウンタ:nbusy変数は、ETXTBSYエラーによるリトライの回数を追跡します。最大3回のリトライが許可されます。- エラーチェックとリトライ条件:
err != nil: コマンド実行がエラーを返した場合。nbusy < 3: リトライ回数が3回未満である場合。strings.Contains(err.Error(), "text file busy"): エラーメッセージに"text file busy"という文字列が含まれている場合。これはETXTBSYエラーを識別するためのヒューリスティックな方法です。
- 待機とリトライ: 上記の条件がすべて満たされた場合、以下の処理が行われます。
time.Sleep(100 * time.Millisecond << uint(nbusy)): 指数バックオフ戦略で待機します。nbusyが0の場合は100ms、1の場合は200ms、2の場合は400ms待機します。これにより、連続するリトライの間隔が徐々に長くなり、競合状態が解消される可能性が高まります。nbusy++: リトライカウンタをインクリメントします。continue: ループの次のイテレーションに進み、コマンドを再実行します。
- 正常終了または非リトライエラー: リトライ条件が満たされない場合(コマンドが成功したか、リトライ対象外のエラーが発生したか、リトライ回数が上限に達した場合)、
buf.Bytes(), errが返され、関数が終了します。 panic("unreachable"): ループの最後にpanic("unreachable")が追加されています。これは、ループが無限に続くことはないという保証を示すもので、通常は到達しないコードパスであることを示します。
この変更により、cgo のようなバイナリのビルドと実行が連続して行われるようなシナリオで、一時的なファイルロックによるエラーが原因でビルドが失敗するのを防ぎ、Goのビルドシステムの堅牢性が向上しました。
関連リンク
- Go Issue 3001: https://github.com/golang/go/issues/3001 (このコミットが修正したIssue)
- Go CL 5845044: https://golang.org/cl/5845044 (このコミットのChange List)
参考にした情報源リンク
ETXTBSYエラーに関する一般的な情報- Unixの
fork()とexec()システムコールに関する情報 - Go言語の
os/execパッケージのドキュメント - Go言語のIssueトラッカー (Issue 3001の内容確認)
- Go言語のChange List (CL 5845044の内容確認)
- 指数バックオフ (Exponential Backoff) 戦略に関する情報