[インデックス 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.Conn
のClose()
メソッドが呼び出されると、基となるファイルディスクリプタはクローズされます。
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.File
をnet.FileConn
に渡していました。その後、net.FileConn
から得られたnet.Conn
オブジェクトに対してdefer srv.Close()
のようにClose()
を呼び出していました。net.Conn
のClose()
メソッドは、基となるファイルディスクリプタをクローズします。
しかし、ここで重要なのは、net.Conn
のClose()
がクローズするのは「ファイルディスクリプタ」であり、os.NewFile
で作成された「*os.File
オブジェクト自体」ではないという点です。*os.File
オブジェクトはまだメモリ上に存在し、ガベージコレクタによって回収されるのを待っています。
*os.File
オブジェクトには、そのオブジェクトがGCによって回収される際に、関連付けられたファイルディスクリプタをクローズするためのファイナライザが設定されています。したがって、net.Conn
のClose()
によってファイルディスクリプタが既にクローズされているにもかかわらず、後になって*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をクローズする
-
srvFile := os.NewFile(uintptr(fds[0]), "server")
: ファイルディスクリプタfds[0]
から*os.File
オブジェクトを作成し、srvFile
という変数に格納しています。第2引数に"server"
という名前が与えられていますが、これはデバッグ目的でファイルに名前を付けるもので、機能的な意味はありません。 -
defer srvFile.Close()
: これが最も重要な変更点です。defer
ステートメントを使用することで、現在の関数(TestSCMCredentials
)が終了する直前にsrvFile.Close()
が確実に呼び出されるようにスケジュールされます。*os.File
のClose()
メソッドは、そのオブジェクトが管理しているファイルディスクリプタをクローズし、関連するリソースを解放します。これにより、*os.File
オブジェクトのファイナライザが実行される前に、ファイルディスクリプタが明示的にクローズされることが保証されます。また、*os.File
オブジェクト自体も適切にクリーンアップされます。 -
srv, err := net.FileConn(srvFile)
: 新しく作成されたsrvFile
(*os.File
)をnet.FileConn
に渡して、ネットワーク接続srv
を作成します。 -
defer srv.Close()
: この行は変更されていません。これは引き続きnet.Conn
インターフェースのClose()
メソッドを呼び出し、基となるファイルディスクリプタをクローズします。
この修正により、srvFile.Close()
がnet.Conn
のClose()
よりも先に(またはほぼ同時に)実行されることで、*os.File
オブジェクトのファイナライザが二重にファイルディスクリプタをクローズしようとする競合状態が解消されます。*os.File
オブジェクトがGCされる際には、既にClose()
が呼び出されているため、ファイナライザは何もせず安全に終了します。
クライアント側(cli
)の接続についても同様の修正が適用されており、両方のファイルディスクリプタが適切に管理されるようになっています。
この変更は、Go言語におけるリソース管理のベストプラクティス、特に*os.File
のようなOSリソースをラップするオブジェクトの明示的なクローズの重要性を示しています。defer
を使用することで、エラーパスや複数のリターンパスがある場合でも、リソースの解放を確実に実行できるため、堅牢なコードを書く上で非常に有効です。
関連リンク
- Go言語の
os
パッケージ: https://pkg.go.dev/os - Go言語の
net
パッケージ: https://pkg.go.dev/net - Go言語の
syscall
パッケージ: https://pkg.go.dev/syscall - Go言語のガベージコレクション: https://go.dev/doc/gc-guide
- Go言語の
defer
ステートメント: https://go.dev/tour/flowcontrol/12
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(特に
os
パッケージとruntime
パッケージのファイナライザ関連の実装) - Go言語のガベージコレクションに関する技術記事
- Unix系OSのファイルディスクリプタに関する一般的な情報
- Goのコードレビューシステム (Gerrit) の該当コミットページ: https://golang.org/cl/6776053 (コミットメッセージに記載されているリンク)
- Go言語の
GOGC
環境変数に関するドキュメントや記事