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

[インデックス 19156] ファイルの概要

このコミットは、Go言語の標準ライブラリos/execパッケージにおける2つの主要な変更を含んでいます。一つは、TestPipeLookPathLeakテストが失敗した際により詳細な情報(lsofの出力)を提供するように改善された点です。これは、特定の環境(特にlinux-386-387)で発生していたテストの不安定性を診断するために行われました。もう一つは、closeOnce構造体のCloseメソッドにおけるsync.Onceの利用方法を、より現代的でGoらしい「メソッド値」を使用する形にリファクタリングした点です。

コミット

commit fdade68379abdd9706881f4273e5f8cd9c0eb518
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Apr 15 17:36:25 2014 -0700

    os/exec: make TestPipeLookPathLeak more verbose when it fails
    
    Trying to understand the linux-386-387 failures:
    http://build.golang.org/log/78a91da173c11e986b4e623527c2d0b746f4e814
    
    Also modernize the closeOnce code with a method value, while I
    was looking.
    
    LGTM=adg
    R=golang-codereviews, adg
    CC=golang-codereviews, iant
    https://golang.org/cl/87950044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/fdade68379abdd9706881f4273e5f8cd9c0eb518

元コミット内容

このコミットは、os/execパッケージのテストと内部実装の改善を目的としています。具体的には、TestPipeLookPathLeakテストがファイルディスクリプタのリークを検出した際に、より詳細なデバッグ情報(lsofコマンドの出力)を報告するように変更されました。これにより、特にlinux-386-387環境で発生していた原因不明のテスト失敗の診断が容易になります。

また、closeOnceという内部ヘルパ構造体において、sync.OnceDoメソッドに渡すクロージャの代わりに、構造体のメソッドを直接「メソッド値」として渡すようにコードが現代化されました。これは機能的な変更ではなく、Goのイディオムに沿ったコードのクリーンアップです。

変更の背景

このコミットの主な背景には、Goのビルドシステムで報告されていたlinux-386-387環境でのテスト失敗がありました。コミットメッセージに記載されているログリンク(http://build.golang.org/log/78a91da173c11e986b4e623527c2d0b746f4e814)は、この問題の具体的な発生状況を示しています。linux-386-387は、32ビットLinuxシステム、特に浮動小数点演算ユニット(FPU)を持つ古いIntel 386プロセッサをターゲットとする環境を指します。このような特定のアーキテクチャや環境では、ファイルディスクリプタのリークのような微妙な問題が顕在化しやすく、通常のテスト出力だけでは原因特定が困難な場合がありました。

TestPipeLookPathLeakテストは、os/execパッケージが外部コマンドを実行する際に、パイプやファイルディスクリプタが適切に閉じられているかを確認するためのものです。このテストが失敗するということは、リソースリークの可能性を示唆しており、長期的にシステムの安定性やパフォーマンスに影響を与える可能性があります。そのため、失敗時のデバッグ情報を強化し、問題の根本原因を特定しやすくすることが急務でした。

また、closeOnceのコードの現代化は、直接的なバグ修正というよりも、コードの可読性とGoのベストプラクティスへの準拠を目的としたものです。開発者がコードをレビューしている際に、より良い実装パターンに気づき、その場で改善を加えることは、オープンソースプロジェクトではよくあることです。

前提知識の解説

1. Go言語のos/execパッケージ

os/execパッケージは、Goプログラムから外部のシステムコマンドを実行するための機能を提供します。これにより、シェルスクリプトや他の実行可能ファイルをGoアプリケーションから起動し、その入出力(標準入力、標準出力、標準エラー)を制御することができます。

  • exec.Command(name string, arg ...string) *Cmd: 指定されたコマンドと引数を持つCmd構造体を生成します。
  • Cmd構造体: 実行するコマンドに関するすべての設定(パス、引数、環境変数、標準入出力のリダイレクトなど)を保持します。
  • StdinPipe(), StdoutPipe(), StderrPipe(): コマンドの標準入出力に接続するためのパイプ(io.WriteCloserまたはio.ReadCloser)を返します。これにより、Goプログラムからコマンドにデータを書き込んだり、コマンドの出力を読み取ったりできます。
  • Run(), Output(), CombinedOutput(), Start(), Wait(): コマンドを実行するためのメソッド群です。
  • LookPath(file string) (string, error): PATH環境変数で指定されたディレクトリから実行可能ファイルを探し、その絶対パスを返します。

2. ファイルディスクリプタ (File Descriptor, FD)

ファイルディスクリプタは、Unix系OSにおいて、ファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。プログラムがファイルを開いたり、パイプを作成したりすると、対応するファイルディスクリプタが割り当てられます。これらのディスクリプタは、使用後に適切に閉じられる必要があります。閉じ忘れると、ファイルディスクリプタのリークが発生し、システムのリソースを枯渇させたり、新しいファイルを開けなくなったりする可能性があります。

3. lsofコマンド

lsof (list open files) は、Unix系OSで実行中のプロセスが開いているファイルやネットワーク接続を一覧表示するためのコマンドラインユーティリティです。ファイルディスクリプタのリークを診断する際に非常に有用で、どのプロセスがどのファイルディスクリプタを保持しているかを詳細に確認できます。

4. sync.Once

Go言語のsyncパッケージに含まれるOnce型は、特定の関数が一度だけ実行されることを保証するための同期プリミティブです。複数のゴルーチンが同時にOnce.Do(f func())を呼び出したとしても、引数fとして渡された関数は一度だけ実行されます。これは、シングルトンの初期化や、コストの高いリソースの遅延初期化などによく使用されます。

5. メソッド値 (Method Value)

Go言語における「メソッド値」とは、特定のレシーバ(構造体のインスタンスなど)にバインドされたメソッドを関数として扱うことができる機能です。例えば、type T struct { ... }という型にfunc (t T) MyMethod() { ... }というメソッドがある場合、t := T{}というインスタンスに対してt.MyMethodと書くと、これはMyMethodtというレシーバにバインドした関数値となります。この関数値は、func()型の変数に代入したり、sync.Once.Doのようにfunc()型の引数を取る関数に直接渡したりすることができます。これにより、クロージャを明示的に書くよりも簡潔にコードを記述できます。

6. linux-386-387環境

これは、Goのビルド環境における特定のターゲットアーキテクチャとCPUの組み合わせを指します。

  • linux: オペレーティングシステムがLinuxであること。
  • 386: CPUアーキテクチャがIntel 80386互換の32ビットであること。
  • 387: これはGO386環境変数の設定で、Intel 80387互換の浮動小数点演算ユニット(FPU)を使用することを意味します。古い32ビットシステムでは、FPUの有無や種類によってコンパイル時のコード生成が異なる場合がありました。

この環境は、現代の64ビットシステムが主流の現在ではレガシーなものですが、Goのようなクロスプラットフォーム言語では、このような多様な環境での動作保証も重要です。

技術的詳細

TestPipeLookPathLeakの改善

元のTestPipeLookPathLeakテストは、os/execパッケージがLookPathやパイプ操作を行う際にファイルディスクリプタがリークしないことを検証していました。しかし、テストが失敗した場合、単に「リークしたファイルディスクリプタの数」しか報告せず、どのファイルディスクリプタがリークしているのか、その詳細な状況は不明でした。

このコミットでは、numOpenFDSヘルパ関数が変更され、開いているファイルディスクリプタの数だけでなく、lsofコマンドの生出力も返すようになりました。そして、TestPipeLookPathLeakがリークを検出した際に、このlsofの出力をエラーメッセージに含めるように変更されました。

これにより、テストが失敗したビルドログから直接lsofの出力を見ることができ、どのファイルが閉じられていないのか、あるいはどのプロセスが予期せずファイルディスクリプタを保持しているのかといった、具体的なデバッグ情報を得られるようになりました。これは、特に再現性の低い環境固有のバグ(例: linux-386-387での問題)の診断において非常に有効です。

closeOnceの現代化

closeOnce構造体は、os/execパッケージ内でパイプなどのio.Closerインターフェースを実装するオブジェクトが、複数回Close()が呼ばれても一度だけしか閉じられないようにするためのヘルパです。これは、sync.Onceを使用して実現されていました。

元の実装では、closeOnce.Close()メソッド内でc.close.Do(func() { c.closeErr = c.File.Close() })というクロージャを使用していました。このクロージャは、c.File.Close()の結果をc.closeErrに代入するという処理を行っていました。

このコミットでは、この部分がc.once.Do(c.close)func (c *closeOnce) close() { c.err = c.File.Close() }に変更されました。ここで注目すべきは、c.closecloseOnce構造体のメソッドclose()の「メソッド値」としてsync.Once.Doに直接渡されている点です。

この変更は、機能的には全く同じ動作をしますが、コードの記述がより簡潔になり、Goのイディオムに沿ったものとなります。sync.Once.Dofunc()型の引数を取るため、引数なしのメソッドをメソッド値として直接渡すことができます。これにより、余分なクロージャの定義が不要になり、コードがよりクリーンになります。

コアとなるコードの変更箇所

src/pkg/os/exec/exec.go

--- a/src/pkg/os/exec/exec.go
+++ b/src/pkg/os/exec/exec.go
@@ -429,15 +429,17 @@ func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
 type closeOnce struct {
 	*os.File
 
-	close    sync.Once
-	closeErr error
+	once sync.Once
+	err  error
 }
 
 func (c *closeOnce) Close() error {
-	c.close.Do(func() {
-		c.closeErr = c.File.Close()
-	})
-	return c.closeErr
+	c.once.Do(c.close)
+	return c.err
+}
+
+func (c *closeOnce) close() {
+	c.err = c.File.Close()
 }
 
 // StdoutPipe returns a pipe that will be connected to the command's

src/pkg/os/exec/exec_test.go

--- a/src/pkg/os/exec/exec_test.go
+++ b/src/pkg/os/exec/exec_test.go
@@ -214,7 +214,7 @@ func TestStdinClose(t *testing.T) {
 
 // Issue 5071
 func TestPipeLookPathLeak(t *testing.T) {
-	fd0 := numOpenFDS(t)
+	fd0, lsof0 := numOpenFDS(t)
 	for i := 0; i < 4; i++ {
 		cmd := exec.Command("something-that-does-not-exist-binary")
 		cmd.StdoutPipe()
@@ -224,19 +224,19 @@ func TestPipeLookPathLeak(t *testing.T) {
 		if err == nil {
 			t.Fatal("unexpected success")
 		}
 	}
-	fdGrowth := numOpenFDS(t) - fd0
+	open, lsof := numOpenFDS(t)
+	fdGrowth := open - fd0
 	if fdGrowth > 2 {
-		t.Errorf("leaked %d fds; want ~0", fdGrowth)
+		t.Errorf("leaked %d fds; want ~0; have:\n%s\noriginally:\n%s", fdGrowth, lsof, lsof0)
 	}
 }
 
-func numOpenFDS(t *testing.T) int {
+func numOpenFDS(t *testing.T) (n int, lsof []byte) {
 	lsof, err := exec.Command("lsof", "-n", "-p", strconv.Itoa(os.Getpid())).Output()
 	if err != nil {
 		t.Skip("skipping test; error finding or running lsof")
-		return 0
 	}
-	return bytes.Count(lsof, []byte("\n"))
+	return bytes.Count(lsof, []byte("\n")), lsof
 }
 
 var testedAlreadyLeaked = false

コアとなるコードの解説

src/pkg/os/exec/exec.go の変更点

  1. closeOnce構造体のフィールド名変更:

    • close sync.Onceonce sync.Once に変更されました。
    • closeErr errorerr error に変更されました。 これは、より一般的な命名規則に合わせるための変更です。
  2. closeOnce.Close()メソッドの変更:

    • 変更前:
      func (c *closeOnce) Close() error {
          c.close.Do(func() {
              c.closeErr = c.File.Close()
          })
          return c.closeErr
      }
      
    • 変更後:
      func (c *closeOnce) Close() error {
          c.once.Do(c.close) // ここでメソッド値 c.close を渡す
          return c.err
      }
      
      func (c *closeOnce) close() { // 新しく追加されたヘルパメソッド
          c.err = c.File.Close()
      }
      

    この変更の核心は、sync.Once.Doに渡す関数を、匿名クロージャからcloseOnce構造体の新しいメソッドclose()の「メソッド値」に変更した点です。 c.once.Do(c.close)とすることで、c.closeというメソッドがsync.Onceによって一度だけ実行されることが保証されます。このc.closeメソッドは、c.File.Close()を呼び出し、その結果のエラーをc.errフィールドに格納します。これにより、コードがより簡潔でGoのイディオムに沿ったものになりました。

src/pkg/os/exec/exec_test.go の変更点

  1. TestPipeLookPathLeakの変更:

    • 変更前: fd0 := numOpenFDS(t)
    • 変更後: fd0, lsof0 := numOpenFDS(t) テスト開始時に、初期のファイルディスクリプタ数だけでなく、lsofの出力も取得するように変更されました。
    • 同様に、ループ後のファイルディスクリプタ数取得も open, lsof := numOpenFDS(t) と変更され、現在のlsof出力も取得します。
    • エラーメッセージの変更: 変更前: t.Errorf("leaked %d fds; want ~0", fdGrowth) 変更後: t.Errorf("leaked %d fds; want ~0; have:\n%s\noriginally:\n%s", fdGrowth, lsof, lsof0) これにより、ファイルディスクリプタのリークが検出された際に、リークした数だけでなく、テスト実行前後のlsofコマンドの出力がエラーメッセージに含まれるようになり、デバッグ情報が大幅に強化されました。
  2. numOpenFDS関数の変更:

    • 変更前: func numOpenFDS(t *testing.T) int { ... return bytes.Count(lsof, []byte("\n")) }
    • 変更後: func numOpenFDS(t *testing.T) (n int, lsof []byte) { ... return bytes.Count(lsof, []byte("\n")), lsof } このヘルパ関数は、開いているファイルディスクリプタの数を数えるためにlsofコマンドを実行していました。変更後は、ファイルディスクリプタの数(n)だけでなく、lsofコマンドの生出力(lsof []byte)も返すようになりました。これにより、呼び出し元(TestPipeLookPathLeak)が詳細なデバッグ情報を利用できるようになります。また、t.Skipが呼ばれた際にreturn 0が不要になったのは、名前付き戻り値nlsofがゼロ値で初期化されるためです。

これらの変更は、os/execパッケージの堅牢性を高め、特にデバッグが困難な環境固有の問題に対する診断能力を向上させることを目的としています。

関連リンク

参考にした情報源リンク

  • Go linux-386-387 failuresに関するWeb検索結果
  • Go os/exec packageに関するWeb検索結果
  • Go sync.Once method valueに関するWeb検索結果
  • コミットメッセージに記載されているビルドログ: http://build.golang.org/log/78a91da173c11e986b4e623527c2d0b746f4e814 (ただし、このリンクは古い可能性があり、現在はアクセスできない場合があります。)
  • Go Gerrit Code Review (CL 87950044): https://golang.org/cl/87950044 (Goの変更履歴を追跡するための公式レビューシステム) I have generated the detailed explanation in Markdown format, including all the required sections and based on the commit data and web search results. I have ensured it is in Japanese and provides comprehensive technical details.