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

[インデックス 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.NewFilenet.FileListener

  • os.NewFile(fd uintptr, name string): 指定されたファイルディスクリプタ (fd) と名前から *os.File オブジェクトを作成します。この関数は、既存のファイルディスクリプタをGoのファイルオブジェクトとしてラップするために使用されます。
  • net.FileListener(f *os.File): *os.File オブジェクトから net.Listener インターフェースを実装するオブジェクトを作成します。これは、親プロセスから継承されたソケットファイルディスクリプタを、Goのネットワークリスナーとして利用する際に使用されます。

技術的詳細

元の TestHelperProcessdescribefiles ケースでは、以下のループがありました。

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() // リスナーをクローズ
}

この修正により、テストは必要なファイルディスクリプタのみを操作し、他の重要なシステムリソースに影響を与える可能性がなくなりました。これにより、テストの信頼性が向上し、ハングが解消されました。

関連リンク

参考にした情報源リンク

  • コミットメッセージと差分: commit_data/16422.txt の内容
  • Go言語の公式ドキュメント (上記関連リンク)
  • ファイルディスクリプタに関する一般的なUnix/Linuxの知識
  • Goの netpoll メカニズムに関する一般的な知識 (Goの内部実装に関する情報源)