[インデックス 14559] ファイルの概要
このコミットは、Go言語の net
パッケージにおけるWindows固有のファイルディスクリプタ (fd) 処理に関するバグ修正です。具体的には、ソケット作成や関連する操作が失敗した場合に、適切にソケットがクローズされないというメモリリークやリソースリークにつながる可能性のある問題を修正しています。
コミット
commit 87b315a78f91447f072a1a37a4b40ebb38475d01
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Wed Dec 5 15:13:03 2012 +1100
net: add missing close socket code
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/6868067
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/87b315a78f91447f072a1a37a4b40ebb38475d01
元コミット内容
net: add missing close socket code
このコミットは、Go言語の net
パッケージにおいて、ソケットのクローズ処理が不足している箇所を追加するものです。特にWindows環境でのソケット操作において、エラー発生時にソケットリソースが適切に解放されない問題を解決します。
変更の背景
Go言語の net
パッケージは、ネットワーク通信を抽象化し、クロスプラットフォームで動作するように設計されています。しかし、OS固有のシステムコールを扱う際には、それぞれのOSの特性に合わせた細かな調整が必要です。Windows環境では、ソケットはファイルディスクリプタとは異なるハンドルとして扱われ、closesocket
という専用のAPIで明示的にクローズする必要があります。
このコミット以前のコードでは、netFD
(ネットワークファイルディスクリプタ) の accept
メソッド内で新しいソケットが作成された際、その後の処理(例えば CreateIoCompletionPort
や Setsockopt
)が失敗した場合に、作成されたソケットが適切にクローズされないという問題がありました。これにより、エラーパスを通った場合にソケットリソースがリークし、システムのリソースを枯渇させる可能性がありました。
この変更は、このようなリソースリークを防ぎ、ネットワーク操作の堅牢性を向上させることを目的としています。また、timeout_test.go
に defer ln.Close()
が追加されていることから、テストコードにおいてもリソースの適切な解放が意識されていることが伺えます。
前提知識の解説
Go言語の net
パッケージ
Go言語の net
パッケージは、TCP/IP、UDP、Unixドメインソケットなど、様々なネットワークプロトコルを扱うための基本的なインターフェースを提供します。このパッケージは、OS固有のネットワークAPIを抽象化し、開発者がプラットフォームの違いを意識せずにネットワークアプリケーションを記述できるようにします。
syscall
パッケージ
syscall
パッケージは、GoプログラムからOSのシステムコールを直接呼び出すためのインターフェースを提供します。これにより、Goの標準ライブラリでは提供されていない低レベルのOS機能にアクセスしたり、OS固有の動作を制御したりすることが可能になります。ネットワーク操作においては、ソケットの作成、バインド、リスン、接続、送受信など、多くの操作が内部的に syscall
パッケージを介して行われます。
Windowsにおけるソケットと closesocket
Windowsでは、ソケットはファイルディスクリプタではなく、SOCKET
型のハンドルとして扱われます。Unix系OSの close()
関数とは異なり、Windowsではソケットを閉じるために closesocket()
という専用のAPI関数を使用する必要があります。この関数は、ソケットに関連付けられたリソースを解放し、ソケットハンドルを無効にします。
OpError
net.OpError
は、Go言語の net
パッケージで定義されているエラー型の一つです。ネットワーク操作中に発生したエラーを詳細に表現するために使用されます。このエラー型は、操作の種類 (Op
)、ネットワークの種類 (Net
)、ローカルアドレス (Source
)、リモートアドレス (Addr
)、そして基となるエラー (Err
) の情報を含みます。これにより、エラーの原因とコンテキストをより詳細に把握することができます。
IOCP (I/O Completion Port)
IOCPは、Windowsにおける高性能な非同期I/Oメカニズムです。複数のI/O操作を効率的に処理するために使用され、特に多数の同時接続を扱うサーバーアプリケーションでその真価を発揮します。ソケットがIOCPに関連付けられると、I/O操作の完了はスレッドプールに通知され、効率的なスレッド利用が可能になります。
技術的詳細
このコミットの主要な変更は、src/pkg/net/fd_windows.go
ファイル内の netFD.accept
メソッドに集中しています。このメソッドは、新しいネットワーク接続を受け入れる際に呼び出されます。
変更前は、新しいソケット s
が syscall.Socket
によって作成された後、いくつかのエラーパスで closesocket(s)
が呼び出されていませんでした。
-
syscall.CreateIoCompletionPort
の失敗時: 新しいソケットs
が作成された後、syscall.CreateIoCompletionPort
を呼び出してソケットをIOCPに関連付けようとします。この操作が失敗した場合、変更前は単にエラーを返していましたが、作成されたソケットs
はクローズされずにリークしていました。 変更後、if _, err := syscall.CreateIoCompletionPort(...)
のエラーチェックの直後にclosesocket(s)
が追加され、ソケットが適切に解放されるようになりました。 -
syscall.Setsockopt
の失敗時:AcceptEx
のコンテキストを更新するためにsyscall.Setsockopt
が呼び出されます。この操作が失敗した場合も、変更前はソケットs
がクローズされずにリークしていました。 変更後、if err != nil
のエラーチェックの直後にclosesocket(s)
が追加され、ソケットが適切に解放されるようになりました。
また、エラーを返す際に、単に return nil, err
としていた箇所が return nil, &OpError{...}
に変更されています。これは、より詳細なエラー情報を提供する net.OpError
を使用することで、デバッグやエラーハンドリングを容易にするための改善です。
src/pkg/net/timeout_test.go
の変更は、テストの堅牢性を高めるためのものです。TestReadWriteDeadline
関数内で net.ListenTCP
で作成されたリスナー ln
に対して defer ln.Close()
が追加されました。これにより、テストが終了する際にリスナーソケットが確実にクローズされるようになり、テスト実行後のリソースリークを防ぎます。これは、本質的なバグ修正とは直接関係ありませんが、リソース管理のベストプラクティスに従った改善です。
コアとなるコードの変更箇所
src/pkg/net/fd_windows.go
--- a/src/pkg/net/fd_windows.go
+++ b/src/pkg/net/fd_windows.go
@@ -557,7 +557,7 @@ func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (*netFD, error) {
s, err := syscall.Socket(fd.family, fd.sotype, 0)
if err != nil {
syscall.ForkLock.RUnlock()
- return nil, err
+ return nil, &OpError{"socket", fd.net, fd.laddr, err}
}
syscall.CloseOnExec(s)
syscall.ForkLock.RUnlock()
@@ -565,6 +565,7 @@ func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (*netFD, error) {
// Associate our new socket with IOCP.
onceStartServer.Do(startServer)
if _, err := syscall.CreateIoCompletionPort(s, resultsrv.iocp, 0, 0); err != nil {
+ closesocket(s)
return nil, &OpError{"CreateIoCompletionPort", fd.net, fd.laddr, err}
}
@@ -582,7 +583,7 @@ func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) {\n err = syscall.Setsockopt(s, syscall.SOL_SOCKET, syscall.SO_UPDATE_ACCEPT_CONTEXT, (*byte)(unsafe.Pointer(&fd.sysfd)), int32(unsafe.Sizeof(fd.sysfd)))\n if err != nil {\n closesocket(s)\n- return nil, err\n+ return nil, &OpError{"Setsockopt", fd.net, fd.laddr, err}\n }\n
// Get local and peer addr out of AcceptEx buffer.
src/pkg/net/timeout_test.go
--- a/src/pkg/net/timeout_test.go
+++ b/src/pkg/net/timeout_test.go
@@ -332,6 +332,7 @@ func TestReadWriteDeadline(t *testing.T) {
if err != nil {
t.Fatalf("ListenTCP on :0: %v", err)
}\n+ defer ln.Close()\n
lnquit := make(chan bool)\n
コアとなるコードの解説
src/pkg/net/fd_windows.go
の変更点
-
syscall.Socket
エラーハンドリングの改善: 変更前:s, err := syscall.Socket(fd.family, fd.sotype, 0) if err != nil { syscall.ForkLock.RUnlock() return nil, err }
変更後:
s, err := syscall.Socket(fd.family, fd.sotype, 0) if err != nil { syscall.ForkLock.RUnlock() return nil, &OpError{"socket", fd.net, fd.laddr, err} }
syscall.Socket
が失敗した場合に、より詳細なエラー情報を含むnet.OpError
を返すようになりました。これにより、エラー発生時のコンテキスト(どの操作で、どのネットワークタイプで、どのローカルアドレスでエラーが発生したか)が明確になります。 -
syscall.CreateIoCompletionPort
エラー時のソケットクローズ: 変更前は、syscall.CreateIoCompletionPort
が失敗した場合にソケットs
がクローズされていませんでした。 変更後:if _, err := syscall.CreateIoCompletionPort(s, resultsrv.iocp, 0, 0); err != nil { closesocket(s) // ★追加された行 return nil, &OpError{"CreateIoCompletionPort", fd.net, fd.laddr, err} }
closesocket(s)
が追加され、IOCPへの関連付けが失敗した場合でも、新しく作成されたソケットs
が適切にクローズされるようになりました。これにより、ソケットリソースのリークが防止されます。 -
syscall.Setsockopt
エラー時のソケットクローズ: 変更前は、syscall.Setsockopt
が失敗した場合にソケットs
がクローズされていませんでした。 変更後:err = syscall.Setsockopt(s, syscall.SOL_SOCKET, syscall.SO_UPDATE_ACCEPT_CONTEXT, (*byte)(unsafe.Pointer(&fd.sysfd)), int32(unsafe.Sizeof(fd.sysfd))) if err != nil { closesocket(s) // ★追加された行 return nil, &OpError{"Setsockopt", fd.net, fd.laddr, err} }
同様に
closesocket(s)
が追加され、Setsockopt
が失敗した場合でもソケットs
が適切にクローズされるようになりました。また、エラー型もnet.OpError
に変更されています。
src/pkg/net/timeout_test.go
の変更点
func TestReadWriteDeadline(t *testing.T) {
// ...
ln, err := net.ListenTCP("tcp", &net.TCPAddr{Port: 0})
if err != nil {
t.Fatalf("ListenTCP on :0: %v", err)
}
defer ln.Close() // ★追加された行
// ...
}
defer ln.Close()
が追加されたことで、TestReadWriteDeadline
関数が終了する際に、net.ListenTCP
で作成されたリスナーソケット ln
が確実にクローズされるようになりました。defer
ステートメントは、関数がリターンする直前に指定された関数を実行するため、エラーが発生した場合でも、正常終了した場合でも、確実にリソースが解放されます。これは、テストのクリーンアップとリソース管理のベストプラクティスです。
関連リンク
- Go言語の
net
パッケージドキュメント: https://pkg.go.dev/net - Go言語の
syscall
パッケージドキュメント: https://pkg.go.dev/syscall - Microsoft Docs:
closesocket
function: https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-closesocket - Microsoft Docs: I/O Completion Ports: https://learn.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports
参考にした情報源リンク
- Go言語の公式ドキュメント
- Windows API ドキュメント
- Go言語のソースコード (特に
src/pkg/net
ディレクトリ) - 一般的なネットワークプログラミングに関する知識