[インデックス 12989] ファイルの概要
このコミットは、Go言語の標準ライブラリ os/exec
パッケージにおける、Cmd.Start()
メソッドが失敗した際に内部的に開かれたファイルディスクリプタが適切にクローズされない問題を修正するものです。これにより、リソースリークが発生する可能性がありました。
コミット
commit a0f7c6c658327e1b306d7328c28c99d15f9d3216
Author: Brian Dellisanti <briandellisanti@gmail.com>
Date: Fri Apr 27 15:46:49 2012 -0700
os/exec: close all internal descriptors when Cmd.Start() fails.
This closes any internal descriptors (pipes, etc) that Cmd.Start() had
opened before it failed.
Fixes #3468.
R=golang-dev, iant, bradfitz
CC=golang-dev
https://golang.org/cl/5986044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a0f7c6c658327e1b306d7328c28c99d15f9d3216
元コミット内容
os/exec: close all internal descriptors when Cmd.Start() fails.
このコミットは、Cmd.Start()
が失敗した場合に、内部的に開かれた全てのディスクリプタ(パイプなど)をクローズすることを目的としています。これにより、Cmd.Start()
がプロセスを起動する前にエラーが発生した場合に、これらのディスクリプタがリークするのを防ぎます。
変更の背景
Go言語の os/exec
パッケージは、外部コマンドを実行するための機能を提供します。Cmd
構造体は実行するコマンドを表し、Start()
メソッドはコマンドを新しいプロセスとして開始します。コマンドの実行には、標準入力、標準出力、標準エラーなどのI/Oストリームを扱うために、内部的にファイルディスクリプタ(またはWindowsにおけるハンドル)が使用されます。
このコミットが導入される以前は、Cmd.Start()
メソッドがコマンドの起動処理中にエラー(例えば、I/Oパイプのセットアップ失敗など)を検出して早期にリターンした場合、その時点で既に開かれていた内部的なファイルディスクリプタが適切にクローズされないという問題がありました。これにより、プログラムが実行されるたびに未使用のファイルディスクリプタが蓄積され、最終的にはシステムのリソースを枯渇させ、新たなファイルやネットワーク接続を開けなくなる「ファイルディスクリプタリーク」を引き起こす可能性がありました。
この問題は、GoのIssue #3468として報告されており、このコミットはその問題を解決するために作成されました。
前提知識の解説
ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix系OSにおいて、プロセスが開いているファイルやソケット、パイプなどのI/Oリソースを識別するために使用される整数値です。プログラムがファイルを開いたり、ネットワーク接続を確立したり、パイプを作成したりするたびに、OSは対応するファイルディスクリプタをプロセスに割り当てます。これらのリソースは、使用後に明示的にクローズ(解放)されないと、プロセスが終了するまで占有され続け、システム全体のリソース枯渇につながる可能性があります。
os/exec
パッケージ
Go言語の os/exec
パッケージは、外部コマンドを実行するための機能を提供します。
exec.Command(name string, arg ...string) *Cmd
: 実行するコマンドと引数を指定してCmd
構造体を作成します。Cmd
構造体: 実行するコマンドに関する情報(パス、引数、環境変数、標準I/Oの設定など)を保持します。Cmd.Start() error
: コマンドを新しいプロセスとして非同期に開始します。成功した場合、nil
を返します。失敗した場合はエラーを返します。Cmd.Wait() error
:Start()
で開始されたコマンドが終了するのを待ちます。コマンドの終了ステータスに基づいてエラーを返します。Cmd.Run() error
:Start()
を呼び出し、その後Wait()
を呼び出してコマンドが完了するのを待ちます。
io.Closer
インターフェース
Go言語の io
パッケージで定義されている io.Closer
インターフェースは、Close() error
メソッドを持つ型を定義します。これは、ファイルやネットワーク接続など、使用後にリソースを解放する必要があるオブジェクトに実装されます。Close()
メソッドを呼び出すことで、関連するシステムリソースが解放され、リークを防ぐことができます。
リソース管理と defer
Go言語では、defer
ステートメントを使用して、関数がリターンする直前に実行される関数呼び出しをスケジュールできます。これは、リソースの解放(例: ファイルのクローズ、ロックの解除)を確実に行うための一般的なパターンです。しかし、Cmd.Start()
のような複雑な初期化プロセスでは、defer
だけでは不十分な場合があります。特に、複数のステップでリソースが確保され、途中の任意のステップでエラーが発生する可能性がある場合、エラーパスごとに明示的なクリーンアップが必要になります。
技術的詳細
このコミットの主要な変更点は、Cmd.Start()
メソッドがエラーを返して早期に終了する際に、既に開かれていた内部ディスクリプタを確実にクローズするためのロジックを追加したことです。
具体的には、以下の変更が行われました。
-
closeDescriptors
ヘルパー関数の追加:func (c *Cmd) closeDescriptors(closers []io.Closer)
という新しいプライベートメソッドがCmd
構造体に追加されました。この関数はio.Closer
インターフェースを実装するオブジェクトのスライスを受け取り、それぞれのClose()
メソッドを呼び出すことで、関連するリソースを解放します。これは、ディスクリプタのクローズ処理をカプセル化し、コードの重複を避けるためのユーティリティ関数です。 -
Cmd.Start()
内でのエラーハンドリングの強化:Cmd.Start()
メソッド内で、I/Oパイプのセットアップ(setupFd
ループ)やプロセスのフォーク(syscall.ForkExec
)など、エラーが発生する可能性のある各ステップの直後に、c.closeDescriptors
が呼び出されるようになりました。- 以前は、
setupFd
ループ内でエラーが発生した場合、return err
の前にディスクリプタのクローズ処理がありませんでした。 syscall.ForkExec
がエラーを返した場合も同様に、クローズ処理が欠落していました。- 修正後、これらのエラーパスのそれぞれで、
c.closeAfterStart
とc.closeAfterWait
という内部スライスに格納されているディスクリプタがcloseDescriptors
を介してクローズされるようになりました。
- 以前は、
-
既存のクローズ処理の
closeDescriptors
への置き換え:Cmd.Start()
の成功パスとCmd.Wait()
メソッド内で、以前はループで個別にfd.Close()
を呼び出していた箇所が、新しく追加されたc.closeDescriptors
関数を呼び出す形に置き換えられました。これにより、コードの簡潔性と一貫性が向上しました。
これらの変更により、Cmd.Start()
がどの段階で失敗しても、その時点で開かれていた全ての内部ディスクリプタが確実にクローズされるようになり、ファイルディスクリプタリークの問題が解決されました。
コアとなるコードの変更箇所
src/pkg/os/exec/exec.go
ファイルが変更されました。
--- a/src/pkg/os/exec/exec.go
+++ b/src/pkg/os/exec/exec.go
@@ -204,6 +204,12 @@ func (c *Cmd) writerDescriptor(w io.Writer) (f *os.File, err error) {
return pw, nil
}
+func (c *Cmd) closeDescriptors(closers []io.Closer) {
+ for _, fd := range closers {
+ fd.Close()
+ }
+}
+
// Run starts the specified command and waits for it to complete.
//
// The returned error is nil if the command runs, has no problems
@@ -233,6 +239,8 @@ func (c *Cmd) Start() error {
for _, setupFd := range []F{(*Cmd).stdin, (*Cmd).stdout, (*Cmd).stderr} {
fd, err := setupFd(c)
if err != nil {
+ c.closeDescriptors(c.closeAfterStart)
+ c.closeDescriptors(c.closeAfterWait)
return err
}
c.childFiles = append(c.childFiles, fd)
@@ -247,12 +255,12 @@ func (c *Cmd) Start() error {
Sys: c.SysProcAttr,
})
if err != nil {
+ c.closeDescriptors(c.closeAfterStart)
+ c.closeDescriptors(c.closeAfterWait)
return err
}
- for _, fd := range c.closeAfterStart {
- fd.Close()
- }
+ c.closeDescriptors(c.closeAfterStart)
c.errch = make(chan error, len(c.goroutine))
for _, fn := range c.goroutine {
@@ -301,9 +309,7 @@ func (c *Cmd) Wait() error {
}
}
- for _, fd := range c.closeAfterWait {
- fd.Close()
- }
+ c.closeDescriptors(c.closeAfterWait)
if err != nil {
return err
コアとなるコードの解説
-
func (c *Cmd) closeDescriptors(closers []io.Closer)
の追加: この新しいメソッドは、io.Closer
インターフェースを実装するオブジェクトのスライスclosers
を受け取ります。ループ内で各fd
のClose()
メソッドを呼び出すことで、関連するリソースを解放します。これは、ファイルディスクリプタのクローズ処理を共通化し、コードの重複を排除するためのヘルパー関数です。 -
Cmd.Start()
内のエラーパスでのcloseDescriptors
の呼び出し:for _, setupFd := range []F{(*Cmd).stdin, (*Cmd).stdout, (*Cmd).stderr}
ループ内で、setupFd(c)
がエラーを返した場合 (if err != nil
)、以前は単にreturn err
していました。- 修正後、
return err
の直前にc.closeDescriptors(c.closeAfterStart)
とc.closeDescriptors(c.closeAfterWait)
が追加されました。これにより、I/Oパイプのセットアップ中にエラーが発生した場合でも、既に開かれていたディスクリプタが確実にクローズされます。c.closeAfterStart
はコマンド開始後にクローズされるべきディスクリプタ、c.closeAfterWait
はコマンド終了後にクローズされるべきディスクリプタをそれぞれ保持しています。エラー発生時には、これら両方のリストに含まれるディスクリプタをクローズすることで、リークを防ぎます。 syscall.ForkExec
がエラーを返した場合 (if err != nil
) も同様に、return err
の直前にc.closeDescriptors(c.closeAfterStart)
とc.closeDescriptors(c.closeAfterWait)
が追加されました。これにより、プロセス起動自体が失敗した場合でも、内部ディスクリプタが適切にクローズされます。
-
既存のクローズ処理の
closeDescriptors
への置き換え:Cmd.Start()
の成功パスにおいて、以前はfor _, fd := range c.closeAfterStart { fd.Close() }
と手動でループしてクローズしていた箇所が、c.closeDescriptors(c.closeAfterStart)
に置き換えられました。Cmd.Wait()
メソッド内でも、以前はfor _, fd := range c.closeAfterWait { fd.Close() }
と手動でループしてクローズしていた箇所が、c.closeDescriptors(c.closeAfterWait)
に置き換えられました。 これらの変更は、機能的には同じですが、新しく導入されたヘルパー関数を使用することで、コードの可読性と保守性が向上しています。
これらの変更により、os/exec
パッケージの堅牢性が向上し、外部コマンド実行時のリソースリークのリスクが低減されました。
関連リンク
- Go CL (Code Review) リンク: https://golang.org/cl/5986044
参考にした情報源リンク
- コミットメッセージと差分情報 (
/home/orange/Project/comemo/commit_data/12989.txt
) - Go言語の
os/exec
パッケージのドキュメント (一般的な知識として) - ファイルディスクリプタに関する一般的なOSの概念 (一般的な知識として)
- Go言語の
io.Closer
インターフェースに関する一般的な知識 (一般的な知識として)
注記: コミットメッセージに記載されている Fixes #3468
について、2012年当時の golang/go
リポジトリにおけるIssue #3468の具体的な内容を現在の公開情報から特定することは困難でした。しかし、コミットメッセージとコードの変更内容から、このIssueが Cmd.Start()
失敗時のファイルディスクリプタリークに関するものであったことは明確です。