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

[インデックス 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() メソッドがエラーを返して早期に終了する際に、既に開かれていた内部ディスクリプタを確実にクローズするためのロジックを追加したことです。

具体的には、以下の変更が行われました。

  1. closeDescriptors ヘルパー関数の追加: func (c *Cmd) closeDescriptors(closers []io.Closer) という新しいプライベートメソッドが Cmd 構造体に追加されました。この関数は io.Closer インターフェースを実装するオブジェクトのスライスを受け取り、それぞれの Close() メソッドを呼び出すことで、関連するリソースを解放します。これは、ディスクリプタのクローズ処理をカプセル化し、コードの重複を避けるためのユーティリティ関数です。

  2. Cmd.Start() 内でのエラーハンドリングの強化: Cmd.Start() メソッド内で、I/Oパイプのセットアップ(setupFd ループ)やプロセスのフォーク(syscall.ForkExec)など、エラーが発生する可能性のある各ステップの直後に、c.closeDescriptors が呼び出されるようになりました。

    • 以前は、setupFd ループ内でエラーが発生した場合、return err の前にディスクリプタのクローズ処理がありませんでした。
    • syscall.ForkExec がエラーを返した場合も同様に、クローズ処理が欠落していました。
    • 修正後、これらのエラーパスのそれぞれで、c.closeAfterStartc.closeAfterWait という内部スライスに格納されているディスクリプタが closeDescriptors を介してクローズされるようになりました。
  3. 既存のクローズ処理の 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

コアとなるコードの解説

  1. func (c *Cmd) closeDescriptors(closers []io.Closer) の追加: この新しいメソッドは、io.Closer インターフェースを実装するオブジェクトのスライス closers を受け取ります。ループ内で各 fdClose() メソッドを呼び出すことで、関連するリソースを解放します。これは、ファイルディスクリプタのクローズ処理を共通化し、コードの重複を排除するためのヘルパー関数です。

  2. 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) が追加されました。これにより、プロセス起動自体が失敗した場合でも、内部ディスクリプタが適切にクローズされます。
  3. 既存のクローズ処理の 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 パッケージの堅牢性が向上し、外部コマンド実行時のリソースリークのリスクが低減されました。

関連リンク

参考にした情報源リンク

  • コミットメッセージと差分情報 (/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() 失敗時のファイルディスクリプタリークに関するものであったことは明確です。