[インデックス 17419] ファイルの概要
このコミットは、Go言語の os/exec
パッケージにおける Cmd.StdinPipe
の挙動を修正し、Close
メソッドが Cmd.Wait
と競合する問題を解決します。具体的には、StdinPipe
から返される io.WriteCloser
が冪等な Close
操作を提供するように変更され、これによりユーザーが明示的に Close
を呼び出しても、Cmd.Wait
がパイプを閉じようとした際に競合状態が発生しないようになります。
コミット
commit 10e2ffdf2ca657567fc1708f6387fef69a8445b6
Author: Andrew Gerrand <adg@golang.org>
Date: Thu Aug 29 14:41:44 2013 +1000
os/exec: return idempotent Closer from StdinPipe
Before this fix, it was always an error to use the Close method on the
io.WriteCloser obtained from Cmd.StdinPipe, as it would race with the
Close performed by Cmd.Wait.
Fixes #6270.
R=golang-dev, r, remyoudompheng, bradfitz, dsymonds
CC=golang-dev
https://golang.org/cl/13329043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/10e2ffdf2ca657567fc1708f6387fef69a8445b6
元コミット内容
os/exec: return idempotent Closer from StdinPipe
この修正以前は、Cmd.StdinPipe
から取得した io.WriteCloser
の Close
メソッドを使用することは常にエラーでした。なぜなら、Cmd.Wait
によって実行される Close
と競合するためです。
このコミットは #6270 を修正します。
変更の背景
このコミットは、Go言語の os/exec
パッケージにおける Cmd.StdinPipe
の既知の競合状態(レースコンディション)を解決するために導入されました。具体的には、Issue #6270 で報告された問題に対応しています。
os/exec
パッケージは、外部コマンドを実行するための機能を提供します。Cmd.StdinPipe()
メソッドは、実行されるコマンドの標準入力に接続されるパイプ(io.WriteCloser
インターフェースを実装)を返します。このパイプを通じて、親プロセスは子プロセスにデータを書き込むことができます。
問題は、Cmd.StdinPipe()
から取得した io.WriteCloser
をユーザーが明示的に Close()
した際に発生しました。Goの os/exec
パッケージの内部実装では、Cmd.Wait()
メソッドが呼び出された際に、子プロセスの標準入力パイプも自動的に閉じられます。これは、子プロセスが標準入力からの読み込みを終了し、リソースが適切に解放されることを保証するためです。
しかし、ユーザーが StdinPipe
から取得した io.WriteCloser
を Cmd.Wait()
が呼び出される前に、または同時に Close()
した場合、同じファイルディスクリプタに対して二重に Close()
が試みられることになり、これが競合状態を引き起こしていました。二重クローズは、通常、syscall.EINVAL
(無効な引数) や syscall.EBADF
(不正なファイルディスクリプタ) などのエラーを引き起こし、プログラムの予期せぬ終了や不安定な動作につながる可能性があります。
この問題は、特に並行処理を行うGoプログラムにおいて顕著でした。例えば、別のゴルーチンで StdinPipe
にデータを書き込み、書き込み完了後に Close()
し、メインのゴルーチンで Cmd.Wait()
を呼び出すようなシナリオで発生しやすかったのです。
このコミットの目的は、この競合状態を排除し、StdinPipe
から返される io.WriteCloser
の Close
メソッドが安全かつ冪等に(何度呼び出しても同じ結果になるように)動作するようにすることでした。
前提知識の解説
このコミットを理解するためには、以下の概念が重要です。
os/exec
パッケージ: Go言語で外部コマンドを実行するための標準ライブラリです。Cmd
構造体を通じてコマンドの実行、標準入出力のリダイレクト、プロセスの待機などを行います。- パイプ (Pipe): プロセス間通信 (IPC) の一種で、一方のプロセスが書き込んだデータをもう一方のプロセスが読み込むことができる単方向のデータストリームです。
os/exec
では、子プロセスの標準入出力と親プロセスを接続するために内部的にパイプが使用されます。 io.WriteCloser
インターフェース: Goの標準ライブラリio
パッケージで定義されているインターフェースで、Write(p []byte) (n int, err error)
とClose() error
メソッドを持ちます。データを書き込み、その後リソースを閉じる機能を提供します。- 競合状態 (Race Condition): 複数のゴルーチン(またはスレッド)が共有リソースに同時にアクセスし、そのアクセス順序によって結果が変わってしまう状態を指します。このコミットのケースでは、
StdinPipe
のClose
とCmd.Wait
のClose
が共有リソース(パイプのファイルディスクリプタ)に同時にアクセスしようとすることで発生していました。 - 冪等性 (Idempotence): ある操作を複数回実行しても、1回実行した場合と同じ結果になる性質を指します。このコミットでは、
Close
メソッドが冪等になるように修正されています。つまり、一度閉じられたパイプに対して再度Close
を呼び出しても、エラーにならず、最初のClose
の結果が返されるようになります。 sync.Once
: Goのsync
パッケージで提供される型で、特定の関数が一度だけ実行されることを保証します。複数のゴルーチンから同時にDo
メソッドが呼び出されても、引数として渡された関数は一度だけ実行されます。これは、リソースの初期化や、今回のケースのように一度だけ実行されるべきクローズ処理の実装に非常に有用です。
技術的詳細
このコミットの核心は、os/exec
パッケージに closeOnce
という新しい内部型を導入し、Cmd.StdinPipe()
がこの closeOnce
のインスタンスを返すように変更した点です。
以前のコードでは、Cmd.StdinPipe()
は単に *os.File
型の書き込み側パイプ (pw
) を io.WriteCloser
として直接返していました。
// Before the fix
func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
// ...
c.closeAfterWait = append(c.closeAfterWait, pw) // pw is *os.File
return pw, nil
}
この pw
は *os.File
であり、その Close()
メソッドは冪等ではありませんでした。一度閉じられたファイルに対して再度 Close()
を呼び出すとエラーが発生します。
新しい実装では、pw
を直接返す代わりに、closeOnce
型のインスタンスでラップして返します。
// After the fix
func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
// ...
wc := &closeOnce{File: pw} // Wrap pw in closeOnce
c.closeAfterWait = append(c.closeAfterWait, wc)
return wc, nil
}
type closeOnce struct {
*os.File // Embeds *os.File, so it implicitly implements io.Writer and other *os.File methods
close sync.Once // Ensures Close() is called only once
closeErr error // Stores the error from the first Close() call
}
func (c *closeOnce) Close() error {
c.close.Do(func() {
c.closeErr = c.File.Close() // The actual Close() call on the underlying *os.File
})
return c.closeErr // Always returns the result of the first Close() call
}
closeOnce
型は以下の特徴を持ちます。
*os.File
の埋め込み:*os.File
を埋め込むことで、closeOnce
はio.Writer
やio.ReaderFrom
など、*os.File
が実装するすべてのメソッドを自動的に継承します。これにより、ユーザーはStdinPipe
から返されたオブジェクトを*os.File
としても扱うことができ、Fd()
などのメソッドにアクセスできます。sync.Once
の利用:sync.Once
フィールドclose
を使用して、Close()
メソッドが一度だけ実行されることを保証します。Close()
が複数回呼び出されても、c.close.Do()
に渡された匿名関数は最初の呼び出し時のみ実行されます。- エラーのキャッシュ:
closeErr
フィールドは、Close()
が最初に実行された際の結果(エラーまたはnil
)を保存します。これにより、その後のClose()
呼び出しは、実際のクローズ操作を再実行するのではなく、キャッシュされたエラー値を返すだけになります。
この設計により、ユーザーが StdinPipe
から取得した io.WriteCloser
を明示的に Close()
しても、Cmd.Wait()
が内部的に Close()
を呼び出しても、*os.File
の Close()
は一度しか実行されません。これにより、二重クローズによる競合状態が完全に解消され、Close()
メソッドが冪等になります。
テストケース TestStdinClose
が追加され、この修正が正しく機能することを確認しています。このテストでは、StdinPipe
を取得し、別のゴルーチンでデータをコピーして stdin.Close()
を呼び出し、メインゴルーチンで cmd.Wait()
を呼び出すという、競合状態が発生しやすかったシナリオを再現しています。修正後、このテストはエラーなくパスするようになります。
コアとなるコードの変更箇所
src/pkg/os/exec/exec.go
の変更点:
import "sync"
が追加されました。Cmd.StdinPipe()
メソッド内で、pw
(*os.File
) を直接返す代わりに、&closeOnce{File: pw}
でラップして返すように変更されました。closeOnce
という新しい構造体と、そのClose()
メソッドが追加されました。
--- a/src/pkg/os/exec/exec.go
+++ b/src/pkg/os/exec/exec.go
@@ -13,6 +13,7 @@ import (
"io"
"os"
"strconv"
+ "sync" // 追加
"syscall"
)
@@ -357,8 +358,23 @@ func (c *Cmd) CombinedOutput() ([]byte, error) {
// StdinPipe returns a pipe that will be connected to the command's
// standard input when the command starts.
+// If the returned WriteCloser is not closed before Wait is called,
+// Wait will close it.
func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
if c.Stdin != nil {
return nil, errors.New("exec: Stdin already set")
}
pr, pw, err := os.Pipe()
if err != nil {
return nil, err
}
c.Stdin = pr
c.closeAfterStart = append(c.closeAfterStart, pr)
- c.closeAfterWait = append(c.closeAfterWait, pw)
- return pw, nil
+ wc := &closeOnce{File: pw} // 変更: pwをcloseOnceでラップ
+ c.closeAfterWait = append(c.closeAfterWait, wc)
+ return wc, nil
+}
+
+type closeOnce struct { // 追加
+ *os.File
+
+ close sync.Once
+ closeErr error
+}
+
+func (c *closeOnce) Close() error { // 追加
+ c.close.Do(func() {
+ c.closeErr = c.File.Close()
+ })
+ return c.closeErr
}
// StdoutPipe returns a pipe that will be connected to the command's
src/pkg/os/exec/exec_test.go
の変更点:
stdinCloseTestString
定数が追加されました。TestStdinClose
という新しいテスト関数が追加されました。このテストは、StdinPipe
のClose
がCmd.Wait
と競合しないことを検証します。TestHelperProcess
関数にstdinClose
ケースが追加され、テストヘルパーとして機能します。
--- a/src/pkg/os/exec/exec_test.go
+++ b/src/pkg/os/exec/exec_test.go
@@ -152,6 +152,34 @@ func TestPipes(t *testing.T) {
check("Wait", err)
}
+const stdinCloseTestString = "Some test string." // 追加
+
+// Issue 6270. // 追加
+func TestStdinClose(t *testing.T) { // 追加
+ check := func(what string, err error) {
+ if err != nil {
+ t.Fatalf("%s: %v", what, err)
+ }
+ }
+ cmd := helperCommand("stdinClose")
+ stdin, err := cmd.StdinPipe()
+ check("StdinPipe", err)
+ // Check that we can access methods of the underlying os.File.`
+ if _, ok := stdin.(interface {
+ Fd() uintptr
+ }); !ok {
+ t.Error("can't access methods of underlying *os.File")
+ }
+ check("Start", cmd.Start())
+ go func() {
+ _, err := io.Copy(stdin, strings.NewReader(stdinCloseTestString))
+ check("Copy", err)
+ // Before the fix, this next line would race with cmd.Wait.
+ check("Close", stdin.Close())
+ }()
+ check("Wait", cmd.Wait())
+}
+
// Issue 5071
func TestPipeLookPathLeak(t *testing.T) {
fd0 := numOpenFDS(t)
@@ -507,6 +535,17 @@ func TestHelperProcess(*testing.T) {
os.Exit(1)
}
}
+ case "stdinClose": // 追加
+ b, err := ioutil.ReadAll(os.Stdin)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v\\n", err)
+ os.Exit(1)
+ }
+ if s := string(b); s != stdinCloseTestString {
+ fmt.Fprintf(os.Stderr, "Error: Read %q, want %q", s, stdinCloseTestString)
+ os.Exit(1)
+ }
+ os.Exit(0)
case "read3": // read fd 3
fd3 := os.NewFile(3, "fd3")
bs, err := ioutil.ReadAll(fd3)
コアとなるコードの解説
このコミットの最も重要な変更は、os/exec
パッケージに導入された closeOnce
型です。
type closeOnce struct {
*os.File
close sync.Once
closeErr error
}
func (c *closeOnce) Close() error {
c.close.Do(func() {
c.closeErr = c.File.Close()
})
return c.closeErr
}
-
type closeOnce struct { ... }
:- この構造体は、
*os.File
を匿名フィールドとして埋め込んでいます。これにより、closeOnce
は*os.File
が実装するすべてのメソッド(Read
,Write
,Fd
など)を自動的に継承します。これは、StdinPipe
がio.WriteCloser
を返すだけでなく、必要に応じて基盤となる*os.File
の機能にもアクセスできるようにするために重要です。 close sync.Once
: これはsync.Once
型のフィールドです。sync.Once
は、Do
メソッドに渡された関数がプログラムの実行中に一度だけ実行されることを保証するために使用されます。複数のゴルーチンが同時にDo
を呼び出しても、関数は一度しか実行されず、他のゴルーチンはその完了を待ちます。closeErr error
: これは、Close()
メソッドが最初に実行された際のエラーを格納するためのフィールドです。
- この構造体は、
-
func (c *closeOnce) Close() error { ... }
:- このメソッドは
io.Closer
インターフェースのClose()
メソッドを実装しています。 c.close.Do(func() { c.closeErr = c.File.Close() })
: ここがこの修正の肝です。sync.Once
のDo
メソッドを使用することで、c.File.Close()
(つまり、基盤となる*os.File
のClose()
メソッド)が一度だけ実行されることが保証されます。Close()
が複数回呼び出されても、この匿名関数は最初の呼び出し時のみ実行されます。return c.closeErr
:Close()
が呼び出されるたびに、closeErr
にキャッシュされた値が返されます。これにより、最初のClose()
の結果が常に返され、その後のClose()
呼び出しはエラーを発生させずに、最初のクローズ操作の結果を報告します。
- このメソッドは
この closeOnce
型を Cmd.StdinPipe()
が返すことで、ユーザーが明示的にパイプを閉じても、Cmd.Wait()
が内部的にパイプを閉じても、基盤となるファイルディスクリプタは一度しか閉じられず、競合状態が解消されます。
関連リンク
- Go Issue #6270: https://github.com/golang/go/issues/6270
- Go CL 13329043: https://golang.org/cl/13329043 (Gerrit Code Review)
参考にした情報源リンク
- Go言語の公式ドキュメント (
os/exec
パッケージ): https://pkg.go.dev/os/exec - Go言語の公式ドキュメント (
io
パッケージ): https://pkg.go.dev/io - Go言語の公式ドキュメント (
sync
パッケージ): https://pkg.go.dev/sync - Go言語における競合状態と
sync.Once
の利用に関する一般的な情報源 (例: Go Concurrency Patterns, Go by Example:sync.Once
)