[インデックス 16422] ファイルの概要
このコミットは、Go言語の標準ライブラリ os/exec
パッケージ内のテストにおけるハング(処理が停止する現象)を修正するものです。具体的には、テストが誤って重要なファイルディスクリプタを閉じてしまうことで発生していた問題を解決します。
コミット
commit 4d6bfcf24504bb2de0bf63bf43ad703ba808a3e9
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue May 28 21:09:27 2013 +0400
os/exec: fix test hang
Currently the test closes random files descriptors,
which leads to hang (in particular if netpoll fd is closed).
Try to open only fd 3, since the parent process expects it to be fd 3 anyway.
Fixes #5571.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/9778048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4d6bfcf24504bb2de0bf63bf43ad703ba808a3e9
元コミット内容
os/exec: fix test hang
Currently the test closes random files descriptors,
which leads to hang (in particular if netpoll fd is closed).
Try to open only fd 3, since the parent process expects it to be fd 3 anyway.
Fixes #5571.
変更の背景
このコミットは、os/exec
パッケージのテスト TestHelperProcess
内で発生していたハング問題を解決するために導入されました。元のコードでは、describefiles
というヘルパープロセスが、ファイルディスクリプタ (fd) の3番から24番までを順に試行し、それぞれを os.NewFile
でファイルとして開き、さらに net.FileListener
でネットワークリスナーとして解釈しようとしていました。
この処理の中で、テストが意図せず重要なファイルディスクリプタを閉じてしまう可能性がありました。特に、Goランタイムが内部的に使用する netpoll
(ネットワークI/Oの多重化を効率的に行うためのメカニズム) に関連するファイルディスクリプタが閉じられた場合、親プロセスがネットワークイベントを適切に処理できなくなり、結果としてテストがハングするという問題が発生していました。
コミットメッセージにある #5571
は、この問題が報告された内部的な課題トラッカーのIDであると考えられます。
前提知識の解説
ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix系オペレーティングシステムにおいて、プロセスが開いているファイルやソケット、パイプなどのI/Oリソースを識別するために使用される整数値です。各プロセスは、独自のファイルディスクリプタテーブルを持っています。
- 標準入出力: 通常、ファイルディスクリプタ0は標準入力 (stdin)、1は標準出力 (stdout)、2は標準エラー出力 (stderr) に割り当てられます。
- 継承: 子プロセスは、親プロセスからファイルディスクリプタを継承することが一般的です。これにより、親プロセスが確立したI/Oチャネルを子プロセスも利用できます。
- クローズ: ファイルディスクリプタをクローズすると、そのディスクリプタが指すリソースへのアクセスが解放されます。誤って重要なディスクリプタを閉じると、システムやアプリケーションの動作に深刻な影響を与える可能性があります。
netpoll
netpoll
は、GoランタイムがネットワークI/Oを効率的に処理するために使用する内部メカニズムです。これは、Linuxの epoll
、macOS/BSDの kqueue
、Windowsの IOCP
など、OSが提供するI/O多重化APIの抽象化レイヤーとして機能します。
netpoll
は、多数のネットワーク接続を同時に監視し、データが利用可能になったり、書き込みが可能になったりしたときに、対応するゴルーチンをスケジューリングします。このメカニズムは、Goの並行処理モデルと非同期I/Oの基盤となっており、Goアプリケーションの高いスケーラビリティとパフォーマンスに貢献しています。
netpoll
が使用するファイルディスクリプタが閉じられると、Goランタイムはネットワークイベントを検出できなくなり、ネットワークI/Oを待機しているゴルーチンが永久にブロックされ、結果としてアプリケーション全体がハングする可能性があります。
os/exec
パッケージ
os/exec
パッケージは、Goプログラムから外部コマンドを実行するための機能を提供します。このパッケージを使用すると、新しいプロセスを起動し、その標準入出力や環境変数を制御できます。テストにおいては、特定のシナリオをシミュレートするためにヘルパープロセスを起動することがよくあります。
os.NewFile
と net.FileListener
os.NewFile(fd uintptr, name string)
: 指定されたファイルディスクリプタ (fd) と名前から*os.File
オブジェクトを作成します。この関数は、既存のファイルディスクリプタをGoのファイルオブジェクトとしてラップするために使用されます。net.FileListener(f *os.File)
:*os.File
オブジェクトからnet.Listener
インターフェースを実装するオブジェクトを作成します。これは、親プロセスから継承されたソケットファイルディスクリプタを、Goのネットワークリスナーとして利用する際に使用されます。
技術的詳細
元の TestHelperProcess
の describefiles
ケースでは、以下のループがありました。
for fd := uintptr(3); fd < 25; fd++ {
f := os.NewFile(fd, fmt.Sprintf("fd-%d", fd))
ln, err := net.FileListener(f)
if err == nil {
fmt.Printf("fd%d: listener %s\\n", fd, ln.Addr())
ln.Close()
}
}
このコードは、ファイルディスクリプタ3から24までを順に試行し、それぞれを os.File
として開き、さらに net.FileListener
として解釈しようとします。
問題は、このループが「ランダムな」ファイルディスクリプタを閉じてしまうことにありました。Goランタイムは、内部的な netpoll
メカニズムのために特定のファイルディスクリプタを使用しています。もしこのループが netpoll
が使用しているファイルディスクリプタを ln.Close()
経由で閉じてしまった場合、Goランタイムはネットワークイベントを適切に処理できなくなり、テストプロセスがハングしてしまいます。
このコミットによる修正は、このループを削除し、代わりにファイルディスクリプタ3のみを明示的に開くように変更しました。
f := os.NewFile(3, fmt.Sprintf("fd3"))
ln, err := net.FileListener(f)
if err == nil {
fmt.Printf("fd3: listener %s\\n", ln.Addr())
ln.Close()
}
この変更の根拠は、コミットメッセージにある「親プロセスはfd 3であることを期待している」という点です。これは、テストのセットアップにおいて、特定のファイルディスクリプタ(この場合は3番)が意図的に子プロセスに渡され、それがネットワークリスナーとして機能することが期待されていることを示唆しています。他のファイルディスクリプタを試行する必要はなく、むしろ試行することで予期せぬ副作用(重要なFDを閉じること)が発生するリスクがありました。
この修正により、テストは必要なファイルディスクリプタのみを扱い、Goランタイムの内部的な netpoll
機構が使用するファイルディスクリプタを誤って閉じることを防ぎ、テストのハングを解消しました。
コアとなるコードの変更箇所
src/pkg/os/exec/exec_test.go
ファイルの TestHelperProcess
関数内の case "describefiles":
ブロックが変更されています。
--- a/src/pkg/os/exec/exec_test.go
+++ b/src/pkg/os/exec/exec_test.go
@@ -540,13 +540,11 @@ func TestHelperProcess(*testing.T) {
tn, _ := strconv.Atoi(args[0])
os.Exit(n)
case "describefiles":
- for fd := uintptr(3); fd < 25; fd++ {
- f := os.NewFile(fd, fmt.Sprintf("fd-%d", fd))
- ln, err := net.FileListener(f)
- if err == nil {
- fmt.Printf("fd%d: listener %s\\n", fd, ln.Addr())
- ln.Close()
- }
+ f := os.NewFile(3, fmt.Sprintf("fd3"))
+ ln, err := net.FileListener(f)
+ if err == nil {
+ fmt.Printf("fd3: listener %s\\n", ln.Addr())
+ ln.Close()
}
os.Exit(0)
case "extraFilesAndPipes":
コアとなるコードの解説
変更前は、ファイルディスクリプタ3から24までをループで処理していました。
for fd := uintptr(3); fd < 25; fd++ { // fd 3から24までをループ
f := os.NewFile(fd, fmt.Sprintf("fd-%d", fd)) // 各fdからファイルオブジェクトを作成
ln, err := net.FileListener(f) // ファイルオブジェクトをネットワークリスナーとして解釈を試みる
if err == nil { // ネットワークリスナーとして成功した場合
fmt.Printf("fd%d: listener %s\\n", fd, ln.Addr()) // アドレスを出力
ln.Close() // リスナーをクローズ(これにより基盤のfdもクローズされる)
}
}
このループは、テストの意図とは関係なく、システムが使用している可能性のある他のファイルディスクリプタ(特に netpoll
関連のFD)を閉じてしまうリスクがありました。
変更後は、ループが削除され、ファイルディスクリプタ3のみを明示的に処理するように簡素化されました。
f := os.NewFile(3, fmt.Sprintf("fd3")) // fd 3からファイルオブジェクトを作成
ln, err := net.FileListener(f) // ファイルオブジェクトをネットワークリスナーとして解釈を試みる
if err == nil { // ネットワークリスナーとして成功した場合
fmt.Printf("fd3: listener %s\\n", ln.Addr()) // アドレスを出力
ln.Close() // リスナーをクローズ
}
この修正により、テストは必要なファイルディスクリプタのみを操作し、他の重要なシステムリソースに影響を与える可能性がなくなりました。これにより、テストの信頼性が向上し、ハングが解消されました。
関連リンク
- Go言語の
os/exec
パッケージ: https://pkg.go.dev/os/exec - Go言語の
net
パッケージ: https://pkg.go.dev/net - Go言語の
os
パッケージ: https://pkg.go.dev/os
参考にした情報源リンク
- コミットメッセージと差分:
commit_data/16422.txt
の内容 - Go言語の公式ドキュメント (上記関連リンク)
- ファイルディスクリプタに関する一般的なUnix/Linuxの知識
- Goの
netpoll
メカニズムに関する一般的な知識 (Goの内部実装に関する情報源)