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

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

このコミットは、Go言語のsyscallパッケージ内のcreds_test.goファイルにおけるテストの信頼性を向上させるための修正です。具体的には、os.Fileオブジェクトが適切にクローズされず、ガベージコレクション(GC)のファイナライザによって二重にファイルディスクリプタがクローズされる問題に対処しています。これにより、後続のテストで同じファイルディスクリプタが再利用された際に発生する可能性のある「bad file descriptor」エラーを防ぎます。

コミット

commit 5611e8b59fe338ca8dcb6790d569e77c2a78785f
Author: Ian Lance Taylor <iant@golang.org>
Date:   Fri Oct 26 10:31:03 2012 -0700

    syscall: fix creds_test to reliably close os.File
    
    Before this patch the test would close the file descriptor but
    not the os.File.  When the os.File was GC'ed, the finalizer
    would close the file descriptor again.  That would cause
    problems if the same file descriptor were returned by a later
    call to open in another test.
    
    On my system:
    
    > GOGC=30 go test
    --- FAIL: TestPassFD (0.04 seconds)
    passfd_test.go:62:      FileConn: dup: bad file descriptor
    FAIL
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6776053

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

https://github.com/golang/go/commit/5611e8b59fe338ca8dcb6790d569e77c2a78785f

元コミット内容

このコミットの元のメッセージは以下の通りです。

「syscall: creds_testをos.Fileを確実にクローズするように修正

このパッチ以前は、テストはファイルディスクリプタをクローズしていましたが、os.Fileはクローズしていませんでした。os.FileがGCされた際、ファイナライザがファイルディスクリプタを再度クローズしていました。これは、後続のテストでopenの呼び出しによって同じファイルディスクリプタが返された場合に問題を引き起こす可能性がありました。

私のシステムでは:

GOGC=30 go test --- FAIL: TestPassFD (0.04 seconds) passfd_test.go:62: FileConn: dup: bad file descriptor FAIL

R=golang-dev, rsc CC=golang-dev https://golang.org/cl/6776053」

変更の背景

この変更の背景には、Go言語のテスト環境におけるファイルディスクリプタの管理に関する潜在的な競合状態がありました。syscallパッケージのテスト、特にcreds_test.go内のTestSCMCredentials関数では、プロセス間でファイルディスクリプタを渡す機能(UnixドメインソケットのSCM_RIGHTSメッセージなど)をテストしていました。

問題は、テスト内でos.NewFileを使ってファイルディスクリプタから*os.Fileオブジェクトを作成した後、その*os.Fileオブジェクトが明示的にクローズされていなかった点にありました。Goの*os.Fileオブジェクトは、その基となるファイルディスクリプタを管理し、ガベージコレクションによってオブジェクトが回収される際に、関連付けられたファイルディスクリプタを自動的にクローズするためのファイナライザを持っています。

しかし、テストコードでは、net.FileConnに渡すためにos.NewFile*os.Fileを作成し、そのnet.Connをクローズすることでファイルディスクリプタ自体はクローズされていました。このため、*os.Fileオブジェクト自体はまだメモリ上に存在し、GCの対象となるまでファイナライザが実行されませんでした。

結果として、*os.FileオブジェクトがGCによって回収されると、既にクローズされているはずのファイルディスクリプタに対してファイナライザが再度close()を呼び出してしまい、二重クローズが発生していました。この二重クローズ自体は通常エラーになりませんが、問題は、そのファイルディスクリプタ番号がOSによって再利用され、別のプロセスやテストで新しいファイルが開かれた際に同じディスクリプタ番号が割り当てられる可能性があったことです。

もし、二重クローズされたディスクリプタ番号が別の有効なファイルディスクリプタとして再利用された直後に、古い*os.Fileのファイナライザが実行されると、その新しい有効なファイルディスクリプタが意図せずクローズされてしまうという競合状態が発生しました。これが、コミットメッセージにある「bad file descriptor」エラーの原因でした。特にGOGC=30のようにGCの頻度を上げる設定でテストを実行すると、この問題が顕在化しやすくなりました。

この修正は、テストの信頼性を高め、このような非決定的なエラーを防ぐことを目的としています。

前提知識の解説

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

Unix系OSにおいて、ファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数値です。プロセスはファイルディスクリプタを通じてこれらのリソースにアクセスします。

2. os.Fileとファイルディスクリプタ

Go言語のosパッケージの*os.File型は、OSのファイルディスクリプタを抽象化したものです。os.NewFile(fd uintptr, name string)関数は、既存のファイルディスクリプタ(uintptr型)から*os.Fileオブジェクトを作成します。*os.Fileオブジェクトは、そのライフサイクル中に基となるファイルディスクリプタを管理し、Close()メソッドが呼び出されるか、オブジェクトがガベージコレクションによって回収される際に、関連付けられたファイルディスクリプタをクローズする責任を持ちます。

3. ガベージコレクション (Garbage Collection, GC) とファイナライザ (Finalizer)

Go言語には自動メモリ管理のためのガベージコレクタがあります。ファイナライザは、オブジェクトがガベージコレクタによって回収される直前に実行される関数です。runtime.SetFinalizer関数を使ってオブジェクトにファイナライザを設定できます。*os.Fileオブジェクトには、その基となるファイルディスクリプタをクローズするためのファイナライザが内部的に設定されています。これにより、プログラマが明示的にClose()を呼び忘れた場合でも、リソースリークを防ぐことができます。

4. net.FileConn

netパッケージのFileConn(f *os.File)関数は、*os.Fileオブジェクトからnet.Connインターフェースを実装するネットワーク接続を作成します。この関数は、*os.Fileが表すファイルディスクリプタをネットワーク接続として扱えるようにします。net.ConnClose()メソッドが呼び出されると、基となるファイルディスクリプタはクローズされます。

5. deferステートメント

Go言語のdeferステートメントは、そのdeferが書かれた関数がリターンする直前に実行される関数呼び出しをスケジュールします。これは、リソースの解放(ファイルやネットワーク接続のクローズ、ロックの解除など)を確実に行うための非常に便利な機能です。deferはLIFO(後入れ先出し)の順序で実行されます。

6. GOGC環境変数

GOGCはGoランタイムのガベージコレクタの動作を制御する環境変数です。デフォルト値は100で、これはヒープサイズが前回のGC後のヒープサイズの100%増加したときにGCが実行されることを意味します。GOGC=30のように値を小さくすると、GCがより頻繁に実行されるようになります。これにより、メモリ使用量を抑えることができますが、GCのオーバーヘッドが増加する可能性があります。このコミットの背景では、GOGC=30を設定することで、*os.Fileオブジェクトのファイナライザがより早く実行され、問題が顕在化しやすくなったことを示しています。

技術的詳細

このコミットが解決しようとしている問題は、Goの*os.Fileオブジェクトのライフサイクル管理と、それに付随するファイナライザの動作に関するものです。

従来のコードでは、os.NewFile(uintptr(fds[0]), "")のようにファイルディスクリプタから*os.Fileオブジェクトを作成し、その*os.Filenet.FileConnに渡していました。その後、net.FileConnから得られたnet.Connオブジェクトに対してdefer srv.Close()のようにClose()を呼び出していました。net.ConnClose()メソッドは、基となるファイルディスクリプタをクローズします。

しかし、ここで重要なのは、net.ConnClose()がクローズするのは「ファイルディスクリプタ」であり、os.NewFileで作成された「*os.Fileオブジェクト自体」ではないという点です。*os.Fileオブジェクトはまだメモリ上に存在し、ガベージコレクタによって回収されるのを待っています。

*os.Fileオブジェクトには、そのオブジェクトがGCによって回収される際に、関連付けられたファイルディスクリプタをクローズするためのファイナライザが設定されています。したがって、net.ConnClose()によってファイルディスクリプタが既にクローズされているにもかかわらず、後になって*os.Fileオブジェクトのファイナライザが実行されると、既にクローズされたファイルディスクリプタに対して再度close()システムコールが発行されることになります。

この「二重クローズ」自体は、通常はエラーになりません。しかし、問題は、OSがファイルディスクリプタ番号を再利用することにあります。ファイルディスクリプタがクローズされると、その番号は空きとなり、OSは新しいファイルやソケットが開かれた際にその番号を再割り当てする可能性があります。

もし、テスト中にファイルディスクリプタが二重クローズされ、その直後に別のテストやシステムプロセスが新しいファイルを開き、たまたま同じディスクリプタ番号が割り当てられたとします。その直後に、まだGCされていない古い*os.Fileオブジェクトのファイナライザが実行されると、そのファイナライザは「古い」ファイルディスクリプタ番号(しかし現在は「新しい」ファイルに割り当てられている)をクローズしようとします。これにより、意図せず「新しい」ファイルがクローズされてしまい、その後の操作で「bad file descriptor」エラーが発生するという、非決定的な競合状態が発生していました。

この問題は、特にGOGC環境変数を小さく設定してGCの頻度を上げた場合に顕在化しやすくなります。GCが頻繁に実行されることで、*os.Fileオブジェクトのファイナライザがより早く実行される機会が増えるためです。

修正は、この二重クローズの問題を根本的に解決するために、*os.Fileオブジェクト自体も明示的にクローズするように変更しました。defer srvFile.Close()のように*os.Fileオブジェクトに対してClose()を呼び出すことで、*os.Fileのファイナライザが実行される前にファイルディスクリプタがクローズされ、かつ*os.Fileオブジェクトが適切に解放されるようになります。これにより、ファイナライザによる二重クローズの試みがなくなり、テストの信頼性が向上します。

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

変更はsrc/pkg/syscall/creds_test.goファイルに集中しています。

--- a/src/pkg/syscall/creds_test.go
+++ b/src/pkg/syscall/creds_test.go
@@ -31,14 +31,18 @@ func TestSCMCredentials(t *testing.T) {
 		t.Fatalf("SetsockoptInt: %v", err)
 	}
 
-	srv, err := net.FileConn(os.NewFile(uintptr(fds[0]), ""))
+	srvFile := os.NewFile(uintptr(fds[0]), "server")
+	defer srvFile.Close()
+	srv, err := net.FileConn(srvFile)
 	if err != nil {
 		t.Errorf("FileConn: %v", err)
 		return
 	}
 	defer srv.Close()
 
-	cli, err := net.FileConn(os.NewFile(uintptr(fds[1]), ""))
+	cliFile := os.NewFile(uintptr(fds[1]), "client")
+	defer cliFile.Close()
+	cli, err := net.FileConn(cliFile)
 	if err != nil {
 		t.Errorf("FileConn: %v", err)
 		return

コアとなるコードの解説

変更は主に2つの箇所で行われています。それぞれsrv(サーバー側)とcli(クライアント側)の接続を確立する部分です。

変更前:

	srv, err := net.FileConn(os.NewFile(uintptr(fds[0]), ""))
	// ...
	defer srv.Close() // これがファイルディスクリプタをクローズする

このコードでは、os.NewFile*os.Fileオブジェクトを作成し、それを直接net.FileConnに渡しています。srv.Close()が呼び出されると、基となるファイルディスクリプタはクローズされます。しかし、os.NewFileで作成された*os.Fileオブジェクト自体は明示的にクローズされていませんでした。この*os.Fileオブジェクトは、GCによって回収されるまでメモリに残り、そのファイナライザが後で実行される可能性がありました。

変更後:

	srvFile := os.NewFile(uintptr(fds[0]), "server")
	defer srvFile.Close() // ここでos.Fileオブジェクトを明示的にクローズする
	srv, err := net.FileConn(srvFile)
	// ...
	defer srv.Close() // これは引き続きnet.Connをクローズする
  1. srvFile := os.NewFile(uintptr(fds[0]), "server"): ファイルディスクリプタfds[0]から*os.Fileオブジェクトを作成し、srvFileという変数に格納しています。第2引数に"server"という名前が与えられていますが、これはデバッグ目的でファイルに名前を付けるもので、機能的な意味はありません。

  2. defer srvFile.Close(): これが最も重要な変更点です。deferステートメントを使用することで、現在の関数(TestSCMCredentials)が終了する直前にsrvFile.Close()が確実に呼び出されるようにスケジュールされます。 *os.FileClose()メソッドは、そのオブジェクトが管理しているファイルディスクリプタをクローズし、関連するリソースを解放します。これにより、*os.Fileオブジェクトのファイナライザが実行される前に、ファイルディスクリプタが明示的にクローズされることが保証されます。また、*os.Fileオブジェクト自体も適切にクリーンアップされます。

  3. srv, err := net.FileConn(srvFile): 新しく作成されたsrvFile*os.File)をnet.FileConnに渡して、ネットワーク接続srvを作成します。

  4. defer srv.Close(): この行は変更されていません。これは引き続きnet.ConnインターフェースのClose()メソッドを呼び出し、基となるファイルディスクリプタをクローズします。

この修正により、srvFile.Close()net.ConnClose()よりも先に(またはほぼ同時に)実行されることで、*os.Fileオブジェクトのファイナライザが二重にファイルディスクリプタをクローズしようとする競合状態が解消されます。*os.FileオブジェクトがGCされる際には、既にClose()が呼び出されているため、ファイナライザは何もせず安全に終了します。

クライアント側(cli)の接続についても同様の修正が適用されており、両方のファイルディスクリプタが適切に管理されるようになっています。

この変更は、Go言語におけるリソース管理のベストプラクティス、特に*os.FileのようなOSリソースをラップするオブジェクトの明示的なクローズの重要性を示しています。deferを使用することで、エラーパスや複数のリターンパスがある場合でも、リソースの解放を確実に実行できるため、堅牢なコードを書く上で非常に有効です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(特にosパッケージとruntimeパッケージのファイナライザ関連の実装)
  • Go言語のガベージコレクションに関する技術記事
  • Unix系OSのファイルディスクリプタに関する一般的な情報
  • Goのコードレビューシステム (Gerrit) の該当コミットページ: https://golang.org/cl/6776053 (コミットメッセージに記載されているリンク)
  • Go言語のGOGC環境変数に関するドキュメントや記事