[インデックス 11554] ファイルの概要
このコミットは、Go言語の os/exec
パッケージのテストコードにおける、ファイルディスクリプタがGoのガベージコレクタによって意図せず早期に閉じられてしまう問題を修正するものです。具体的には、リークしたファイルディスクリプタのテストにおいて、テスト対象のファイルディスクリプタがガベージコレクションの対象となり、その結果としてファイルが閉じられ、テストが失敗する可能性があった事象に対処しています。
コミット
commit 2b8d5be55f0201c3d4047ea8510b168eb37c5074
Author: Ian Lance Taylor <iant@golang.org>
Date: Wed Feb 1 16:37:02 2012 -0800
os/exec: make sure file is not closed early in leaked fd test
Without this change, fd3 can be collected by the garbage
collector and finalized, which causes the file descriptor to
be closed, which causes the call to os.Open to return 3 rather
than the expected descriptor number.
R=golang-dev, gri, bradfitz, bradfitz, iant
CC=golang-dev
https://golang.org/cl/5607056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2b8d5be55f0201c3d4047ea8510b168eb37c5074
元コミット内容
os/exec: make sure file is not closed early in leaked fd test
Without this change, fd3 can be collected by the garbage
collector and finalized, which causes the file descriptor to
be closed, which causes the call to os.Open to return 3 rather
than the expected descriptor number.
R=golang-dev, gri, bradfitz, bradfitz, iant
CC=golang-dev
https://golang.org/cl/5607056
変更の背景
このコミットは、Go言語の os/exec
パッケージ内のテスト exec_test.go
において、ファイルディスクリプタのリークを検出するテストの信頼性を向上させるために行われました。
問題の根源は、Goのガベージコレクタ (GC) の動作にありました。GoのGCは、もはや参照されなくなったオブジェクトを自動的に解放します。ファイルディスクリプタのようなOSリソースは、Goの os.File
オブジェクトによってラップされており、このオブジェクトがGCによって回収される際に、関連するファイルディスクリプタが自動的に閉じられる(ファイナライズされる)仕組みになっています。
リークしたファイルディスクリプタをテストするシナリオでは、特定のファイルディスクリプタ(このケースでは fd3
)が意図的に開かれたままにされ、その後の os.Open
呼び出しが期待されるディスクリプタ番号(通常は3より大きい番号)を返すことを検証します。しかし、テストコード内で fd3
への参照が一時的に失われると、GCが fd3
を回収し、その結果として fd3
に対応するファイルディスクリプタが早期に閉じられてしまう可能性がありました。
ファイルディスクリプタが早期に閉じられると、次に os.Open
が呼び出された際に、閉じられた fd3
の番号(この場合は3)が再利用されてしまい、テストが期待する結果(リークしたディスクリプタが残っているため、新しいディスクリプタ番号が割り当てられる)と異なる結果を返すことになります。これにより、テストが誤って失敗する、あるいはリークを検出できないという問題が発生していました。
このコミットは、fd3
への参照をテストコード内で明示的に保持し続けることで、GCによる早期のファイナライズを防ぎ、テストの安定性と正確性を確保することを目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
-
ファイルディスクリプタ (File Descriptor, FD): Unix系OSにおいて、ファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。プロセスがファイルを開くと、カーネルは一意のファイルディスクリプタを返し、プロセスはこのディスクリプタを使ってそのファイルに対する読み書きなどの操作を行います。標準入力 (stdin) は0、標準出力 (stdout) は1、標準エラー出力 (stderr) は2という固定のディスクリプタ番号が割り当てられています。
-
Go言語の
os/exec
パッケージ: Go言語の標準ライブラリの一部で、外部コマンドを実行するための機能を提供します。os/exec
パッケージを使用すると、新しいプロセスを生成し、その標準入出力や環境変数を設定し、実行結果を取得することができます。このパッケージは、特にシェルスクリプトや他のプログラムをGoアプリケーションから呼び出す際に不可欠です。 -
Go言語のガベージコレクション (Garbage Collection, GC): Goは自動メモリ管理(ガベージコレクション)を採用しています。プログラマが明示的にメモリを解放する必要はなく、GCが不要になったメモリ領域を自動的に回収します。GoのGCは、到達可能性 (reachability) に基づいて動作します。つまり、プログラムから到達可能なオブジェクトは「生きている」と判断され、到達不可能なオブジェクトは「死んでいる」と判断されて回収の対象となります。 GoのGCは、オブジェクトが回収される際に、そのオブジェクトに紐付けられたファイナライザ (finalizer) を実行することができます。
os.File
オブジェクトの場合、そのファイナライザは対応するファイルディスクリプタを閉じる役割を担っています。 -
ファイナライザ (Finalizer): Goにおいて、
runtime.SetFinalizer
関数を使って、特定のオブジェクトがガベージコレクションによって回収される直前に実行される関数を設定できます。os.File
型のオブジェクトには、内部的にこのファイナライザが設定されており、オブジェクトがGCされると、関連するファイルディスクリプタが自動的に閉じられます。これは、プログラマがf.Close()
を呼び忘れた場合でもリソースリークを防ぐための重要なメカニズムです。 -
テストにおけるファイルディスクリプタのリーク: プログラムがファイルディスクリプタを適切に閉じない場合、ディスクリプタが「リーク」します。OSがプロセスに割り当てられるファイルディスクリプタの数には上限があるため、リークが続くと最終的に新しいファイルを開けなくなり、アプリケーションがクラッシュする可能性があります。そのため、ファイルディスクリプタのリークを検出するテストは、堅牢なシステムを構築する上で非常に重要です。
技術的詳細
このコミットが修正している問題は、Goのガベージコレクタとファイルディスクリプタのライフサイクル管理の間の微妙な相互作用に起因します。
os.File
オブジェクトは、内部的にOSのファイルディスクリプタへの参照を保持しています。Goのランタイムは、os.File
オブジェクトがガベージコレクションの対象となり、メモリから解放される際に、そのオブジェクトに設定されたファイナライザを実行します。このファイナライザは、対応するOSのファイルディスクリプタを閉じる役割を担っています。これは通常、リソースリークを防ぐための望ましい動作です。
しかし、exec_test.go
の TestHelperProcess
関数内の特定のテストシナリオでは、この自動的なファイナライズが問題を引き起こしていました。このテストは、子プロセスが親プロセスからファイルディスクリプタを継承し、それが適切に閉じられているか(リークしていないか)を検証するものです。
テストコードの一部で、fd3
という変数に os.File
オブジェクトが代入され、その後、その fd3
への直接的な参照がテストコードの実行パスから一時的に失われる可能性がありました。GoのGCは、参照がなくなったオブジェクトをいつでも回収する可能性があるため、テストの実行中に fd3
オブジェクトがGCの対象となり、そのファイナライザが実行されて fd3
に対応するファイルディスクリプタが閉じられてしまうことがありました。
ファイルディスクリプタが閉じられると、OSは閉じられたディスクリプタ番号を再利用可能な状態にします。このテストでは、os.Open
を呼び出して新しいファイルディスクリプタを取得し、その番号が期待される値(リークしたディスクリプタが存在しないため、通常は3より大きい番号)であることを検証していました。しかし、fd3
がGCによって早期に閉じられてしまうと、次に os.Open
が呼び出された際に、閉じられた fd3
の番号である 3
が再利用されてしまい、テストが誤って成功(リークがないと判断)してしまうか、あるいは期待と異なるディスクリプタ番号が返されることでテストが失敗していました。
この修正は、fd3
オブジェクトへの参照をテストコードの関連する部分で明示的に保持し続けることで、GCが fd3
を早期に回収することを防ぎます。具体的には、fd3.Close()
を呼び出すことで、fd3
オブジェクトがまだ参照されていることをGCに示し、テストの目的である「リークしたディスクリプタが存在するかどうか」の検証が正確に行われるようにします。fd3.Close()
自体は、テストの目的上、そのファイルディスクリプタを閉じることになりますが、重要なのはその呼び出しが fd3
オブジェクトへの参照を保持し、GCによる意図しない早期のファイナライズを防ぐ点です。これにより、テストのタイミングに依存しない、より堅牢なテストが実現されました。
コアとなるコードの変更箇所
変更は src/pkg/os/exec/exec_test.go
ファイルの TestHelperProcess
関数内で行われています。
--- a/src/pkg/os/exec/exec_test.go
+++ b/src/pkg/os/exec/exec_test.go
@@ -309,6 +309,12 @@ func TestHelperProcess(*testing.T) {
f.Close()
}
}
+ // Referring to fd3 here ensures that it is not
+ // garbage collected, and therefore closed, while
+ // executing the wantfd loop above. It doesn't matter
+ // what we do with fd3 as long as we refer to it;
+ // closing it is the easy choice.
+ fd3.Close()
os.Stderr.Write(bs)
case "exit":
n, _ := strconv.Atoi(args[0])
追加されたコードは以下の6行です。
// Referring to fd3 here ensures that it is not
// garbage collected, and therefore closed, while
// executing the wantfd loop above. It doesn't matter
// what we do with fd3 as long as we refer to it;
// closing it is the easy choice.
fd3.Close()
コアとなるコードの解説
追加された fd3.Close()
の呼び出しとそれに付随するコメントが、このコミットの核心です。
fd3
は、テストの初期段階で os.Open
によって開かれたファイルを表す *os.File
型の変数です。このテストの目的は、子プロセスが親プロセスから継承したファイルディスクリプタが適切に閉じられているか(リークしていないか)を検証することです。
変更前のコードでは、fd3
が開かれた後、その *os.File
オブジェクトへの直接的な参照が、テストの特定の実行パスにおいて一時的に失われる可能性がありました。Goのガベージコレクタは、参照されなくなったオブジェクトをいつでも回収する可能性があるため、fd3
オブジェクトがGCによって回収され、そのファイナライザ(ファイルディスクリプタを閉じる処理)が実行されてしまうことがありました。これにより、fd3
に対応するファイルディスクリプタが、テストが意図するよりも早く閉じられてしまい、テストの信頼性が損なわれていました。
追加された fd3.Close()
は、以下の2つの重要な役割を果たします。
-
GCによる早期回収の防止:
fd3.Close()
を呼び出すことで、fd3
オブジェクトへの参照がこの行まで確実に保持されることになります。GoのGCは、到達可能なオブジェクトを回収しないため、この呼び出しが存在することで、fd3
オブジェクトがwantfd
ループの実行中にGCによって回収され、ファイルディスクリプタが早期に閉じられることを防ぎます。コメントにもあるように、「fd3
をここで参照することで、wantfd
ループの実行中にガベージコレクションされ、したがって閉じられることがないようにする」という意図があります。 -
ファイルディスクリプタの明示的なクローズ:
fd3.Close()
は、その名の通り、ファイルディスクリプタを閉じます。テストの文脈では、このファイルディスクリプタは最終的に閉じられるべきものです。この修正の主な目的はGCによる早期クローズを防ぐことですが、Close()
を呼び出すことは、リソースを適切に解放するという観点からも正しい振る舞いです。コメントが示唆するように、「fd3
を参照する限り、何をするかは重要ではない。閉じるのが簡単な選択肢だ」という考え方です。つまり、fd3
への参照を保持し続けるための最もシンプルで適切な方法としてClose()
が選ばれたということです。
この変更により、TestHelperProcess
は、ガベージコレクタの動作に左右されずに、ファイルディスクリプタのリークテストを安定して実行できるようになりました。
関連リンク
- Go言語の
os/exec
パッケージのドキュメント: https://pkg.go.dev/os/exec - Go言語のガベージコレクションに関する公式ブログ記事 (古いものですが概念は共通): https://go.dev/blog/go15gc
- Go言語の
runtime.SetFinalizer
のドキュメント: https://pkg.go.dev/runtime#SetFinalizer - Go言語の
os.File
のドキュメント: https://pkg.go.dev/os#File
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のガベージコレクションに関する一般的な情報源
- Unix系OSにおけるファイルディスクリプタに関する情報源
- Go言語のソースコード (特に
os
およびruntime
パッケージ) - このコミットのGo Gerritレビューページ: https://golang.org/cl/5607056 (コミットメッセージに記載されているリンク)
- このレビューページには、コミットに関する議論や背景情報が含まれている可能性があります。