[インデックス 16903] ファイルの概要
src/pkg/net/fd_windows.go
は、Go言語の標準ライブラリである net
パッケージの一部であり、特にWindowsオペレーティングシステムにおけるファイルディスクリプタ(ソケット)の管理とネットワークI/O処理を担当するファイルです。Goのネットワーク処理は、OS固有のシステムコールを抽象化し、クロスプラットフォームで一貫したAPIを提供していますが、その内部では各OSの特性に合わせた実装が行われています。このファイルは、WindowsのWinsock APIとGoのランタイムが連携して、効率的かつ安全にネットワーク接続を管理するための基盤を提供しています。具体的には、ソケットの作成、クローズ、I/O操作(読み書き)、および非同期I/Oのためのポーリングメカニズム(netpoll)との連携などが定義されています。
コミット
このコミットは、Windows環境におけるGoの net
パッケージでのメモリリークを修正することを目的としています。主な変更点は、ソケットが閉じられる際に、関連する netpoll
ディスクリプタも確実に閉じられるようにすること、およびエラーパスにおいてもこれらのディスクリプタが適切にクローズされるようにすることです。これにより、リソースの解放が確実に行われ、メモリリークが防止されます。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b8734748b6b151a7fd724fc41e2555e6cd34385f
元コミット内容
commit b8734748b6b151a7fd724fc41e2555e6cd34385f
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Jul 29 20:01:13 2013 +0400
net: fix memory leaks on windows
Close netpoll descriptor along with socket.
Ensure that error paths close the descriptor as well.
R=golang-dev, mikioh.mikioh, alex.brainman
CC=golang-dev
https://golang.org/cl/11987043
変更の背景
Goの net
パッケージは、内部的に非同期I/Oを効率的に処理するために「netpoll」と呼ばれるメカニズムを使用しています。これは、OSが提供するI/O多重化機能(Linuxのepoll、macOSのkqueue、WindowsのI/O Completion Ports (IOCP) など)を抽象化したものです。ソケットが作成されると、通常、このnetpollメカニズムに登録され、I/Oイベントの準備ができたときにGoランタイムに通知されます。
このコミットが行われる前のWindows環境では、ソケットが閉じられる際に、そのソケットに関連付けられた netpoll
ディスクリプタが適切に解放されないケースがありました。特に、エラーが発生してソケットが早期にクローズされるようなパスでは、この netpoll
ディスクリプタがリークし、システムリソースを消費し続ける可能性がありました。これは、Goアプリケーションが長時間稼働したり、多数のネットワーク接続を頻繁に開閉したりする場合に、メモリ使用量の増加やリソース枯渇につながるメモリリークとして顕在化します。
この問題は、ソケット自体は closesocket
システムコールによって閉じられても、Goランタイムが内部的に管理している netpoll
関連のリソースが解放されないために発生していました。このコミットは、ソケットのライフサイクルと netpoll
ディスクリプタのライフサイクルを同期させ、両者が確実に解放されるようにすることで、このメモリリークを解決することを目的としています。
前提知識の解説
1. Goの net
パッケージと netFD
Goの net
パッケージは、TCP/UDPソケット、IP接続、Unixドメインソケットなど、様々なネットワーク通信機能を提供します。内部的には、OS固有のソケットを抽象化するために netFD
(network file descriptor) という構造体を使用しています。netFD
は、実際のOSのソケットハンドル(Windowsでは syscall.Handle
)や、そのソケットに関連するメタデータ(アドレス情報、I/O状態など)をカプセル化しています。
2. netpoll
メカニズム
Goランタイムは、ノンブロッキングI/Oとゴルーチンのスケジューリングを効率的に連携させるために netpoll
という内部メカニズムを使用しています。これは、多数のソケットからのI/Oイベントを単一のスレッドで効率的に監視するためのものです。Windowsでは、この netpoll
の実装にI/O Completion Ports (IOCP) が利用されています。ソケットが netpoll
に登録されると、I/O操作が完了した際にランタイムに通知が送られ、対応するゴルーチンが再開されます。netpoll
ディスクリプタ(fd.pd
)は、この netpoll
メカニズムにおけるソケットの登録情報を管理するオブジェクトです。
3. runtime.SetFinalizer
runtime.SetFinalizer
は、Goのガベージコレクタ(GC)がオブジェクトを回収する直前に実行される関数を設定するためのメカニズムです。これは、Goのヒープ上に割り当てられたオブジェクトが、OSのリソース(ファイルディスクリプタ、ネットワークソケット、C言語ライブラリによって割り当てられたメモリなど)を保持している場合に特に有用です。GCがオブジェクトを回収する際に、SetFinalizer
で設定された関数が呼び出され、その関数内でOSリソースの解放処理を行うことで、リソースリークを防ぐことができます。ただし、ファイナライザはGCのタイミングに依存するため、即座のリソース解放が保証されるわけではありません。
4. WindowsのソケットAPI (closesocket
)
Windowsでは、ソケットを閉じるために closesocket
というAPI関数を使用します。これは、ソケットに関連付けられたシステムリソースを解放し、ソケットハンドルを無効化します。しかし、closesocket
はソケット自体を閉じるだけであり、Goランタイムが内部的に管理している netpoll
ディスクリプタのような高レベルのリソースは自動的には解放しません。
技術的詳細
このメモリリークは、主に以下の2つのシナリオで発生していました。
-
runtime.SetFinalizer
の不適切な使用: 変更前は、netFD
オブジェクトのファイナライザとして(*netFD).closesocket
が直接設定されていました。closesocket
はOSのソケットハンドルを閉じるだけなので、netFD
に関連付けられたnetpoll
ディスクリプタ (fd.pd
) は解放されませんでした。GCがnetFD
を回収し、closesocket
が呼び出されても、fd.pd
はリークしたままになっていました。 -
エラーパスでの不完全なクリーンアップ:
netFD.accept
メソッドのような、新しいソケットを作成する処理において、エラーが発生した場合にclosesocket(s)
が直接呼び出されていました。このs
は新しいソケットハンドルですが、もしこのソケットが一時的にでもnetpoll
に登録されていた場合、closesocket
だけではnetpoll
ディスクリプタが閉じられず、リークが発生していました。
このコミットによる修正は、これらの問題を包括的に解決しています。
-
runtime.SetFinalizer
の変更:runtime.SetFinalizer(fd, (*netFD).closesocket)
からruntime.SetFinalizer(fd, (*netFD).Close)
へと変更されました。これは非常に重要な変更です。netFD.Close
メソッドは、ソケットを閉じるだけでなく、netFD
に関連するすべてのリソース(特にnetpoll
ディスクリプタ)を適切に解放する責任を持つように設計されています。これにより、netFD
オブジェクトがGCによって回収される際に、ソケットとnetpoll
ディスクリプタの両方が確実にクリーンアップされるようになりました。 -
netFD.decref()
内でのfd.pd.Close()
の追加:netFD.decref()
メソッドは、netFD
の参照カウントがゼロになり、ソケットが実際に閉じられる直前に呼び出される内部メソッドです。このメソッド内でfd.pd.Close()
がclosesocket(fd.sysfd)
の前に呼び出されるようになりました。コメントにもあるように、「Poller may want to unregister fd in readiness notification mechanism, so this must be executed before closesocket.」(ポーラーは準備完了通知メカニズムからfdの登録を解除したい場合があるため、これはclosesocketの前に実行されなければならない)という理由で、netpoll
ディスクリプタのクローズがソケット自体のクローズよりも先に行われることが保証されます。これにより、ポーラーが既に閉じられたソケットにアクセスしようとする競合状態を防ぎ、安全なリソース解放を保証します。 -
netFD.accept()
エラーパスでのnetfd.Close()
の使用:netFD.accept()
メソッド内のエラーパスで、以前はclosesocket(s)
が直接呼び出されていましたが、これがnetfd.Close()
に変更されました。ここでnetfd
は、新しく作成されたnetFD
オブジェクトを指します。これにより、エラーが発生して新しいソケットが破棄される場合でも、そのソケットに関連付けられたnetpoll
ディスクリプタを含むすべてのリソースがnetFD.Close()
の完全なクリーンアップロジックを通じて適切に解放されることが保証されます。
これらの変更により、Goの net
パッケージはWindows環境でより堅牢になり、ソケットと netpoll
ディスクリプタのライフサイクル管理が改善され、メモリリークが効果的に防止されるようになりました。
コアとなるコードの変更箇所
--- a/src/pkg/net/fd_windows.go
+++ b/src/pkg/net/fd_windows.go
@@ -288,7 +288,7 @@ func newFD(fd syscall.Handle, family, sotype int, net string) (*netFD, error) {
func (fd *netFD) setAddr(laddr, raddr Addr) {
fd.laddr = laddr
fd.raddr = raddr
- runtime.SetFinalizer(fd, (*netFD).closesocket)
+ runtime.SetFinalizer(fd, (*netFD).Close)
}
// Make new connection.
@@ -366,6 +366,9 @@ func (fd *netFD) decref() {
fd.sysmu.Lock()
fd.sysref--
if fd.closing && fd.sysref == 0 && fd.sysfd != syscall.InvalidHandle {
+\t\t// Poller may want to unregister fd in readiness notification mechanism,
+\t\t// so this must be executed before closesocket.
+\t\tfd.pd.Close()
\t\tclosesocket(fd.sysfd)
\t\tfd.sysfd = syscall.InvalidHandle
\t\t// no need for a finalizer anymore
@@ -409,10 +412,6 @@ func (fd *netFD) CloseWrite() error {\n \treturn fd.shutdown(syscall.SHUT_WR)\n }\n \n-func (fd *netFD) closesocket() error {\n-\treturn closesocket(fd.sysfd)\n-}\n-\n // Read from network.\n \n type readOp struct {\n@@ -585,14 +584,14 @@ func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (*netFD, error) {\n \to.newsock = s\n \t_, err = iosrv.ExecIO(&o)\n \tif err != nil {\n-\t\tclosesocket(s)\n+\t\tnetfd.Close()\n \t\treturn nil, err\n \t}\n \n \t// Inherit properties of the listening socket.\n \terr = syscall.Setsockopt(s, syscall.SOL_SOCKET, syscall.SO_UPDATE_ACCEPT_CONTEXT, (*byte)(unsafe.Pointer(&fd.sysfd)), int32(unsafe.Sizeof(fd.sysfd)))\n \tif err != nil {\n-\t\tclosesocket(s)\n+\t\tnetfd.Close()\n \t\treturn nil, &OpError{\"Setsockopt\", fd.net, fd.laddr, err}\n \t}\n \n```
## コアとなるコードの解説
### 1. `runtime.SetFinalizer` の変更
```go
// 変更前
// runtime.SetFinalizer(fd, (*netFD).closesocket)
// 変更後
runtime.SetFinalizer(fd, (*netFD).Close)
この変更は、netFD
オブジェクトがガベージコレクションによって回収される際に呼び出されるファイナライザ関数を (*netFD).closesocket
から (*netFD).Close
に変更しています。
- 変更前:
(*netFD).closesocket
は、単にOSのclosesocket
システムコールを呼び出すだけの関数でした。これはソケットハンドル自体は閉じますが、netFD
内部で管理されているnetpoll
ディスクリプタ (fd.pd
) のような他のリソースは解放しませんでした。 - 変更後:
(*netFD).Close
は、netFD
の公開されたクローズメソッドであり、ソケットだけでなく、関連するnetpoll
ディスクリプタを含むすべてのリソースを適切に解放するロジックを含んでいます。これにより、netFD
オブジェクトが不要になった際に、すべての関連リソースが確実にクリーンアップされるようになりました。
2. netFD.decref()
内での fd.pd.Close()
の追加
func (fd *netFD) decref() {
fd.sysmu.Lock()
fd.sysref--
if fd.closing && fd.sysref == 0 && fd.sysfd != syscall.InvalidHandle {
// Poller may want to unregister fd in readiness notification mechanism,
// so this must be executed before closesocket.
fd.pd.Close() // <-- 追加された行
closesocket(fd.sysfd)
fd.sysfd = syscall.InvalidHandle
// no need for a finalizer anymore
runtime.SetFinalizer(fd, nil) // ファイナライザを解除
}
fd.sysmu.Unlock()
}
netFD.decref()
は、netFD
の内部参照カウントを減らすメソッドです。参照カウントがゼロになり、かつ fd.closing
が真の場合(つまり、ソケットが閉じられる準備ができていて、もう参照されていない場合)、実際のソケットクローズ処理が行われます。
- 追加された
fd.pd.Close()
は、ソケットのnetpoll
ディスクリプタを閉じます。この行がclosesocket(fd.sysfd)
の前に配置されていることが重要です。これは、netpoll
メカニズムがソケットのクローズを認識し、それに関連する内部状態を更新する機会を確保するためです。もしソケットが先に閉じられてしまうと、netpoll
が無効なソケットハンドルにアクセスしようとして問題が発生する可能性があります。
3. func (fd *netFD) closesocket() error
の削除
// 削除された関数
// func (fd *netFD) closesocket() error {
// return closesocket(fd.sysfd)
// }
この関数は、runtime.SetFinalizer
の変更に伴い不要になったため削除されました。netFD
のクローズ処理はすべて (*netFD).Close
メソッドに集約されることになります。
4. netFD.accept()
エラーパスでの netfd.Close()
の使用
// 変更前
// if err != nil {
// closesocket(s)
// return nil, err
// }
// 変更後
if err != nil {
netfd.Close() // <-- 変更された行
return nil, err
}
// ...
// 変更前
// if err != nil {
// closesocket(s)
// return nil, &OpError{"Setsockopt", fd.net, fd.laddr, err}
// }
// 変更後
if err != nil {
netfd.Close() // <-- 変更された行
return nil, &OpError{"Setsockopt", fd.net, fd.laddr, err}
}
netFD.accept()
メソッドは、新しい接続を受け入れる際に新しいソケットを作成します。この処理中にエラーが発生した場合、以前は新しく作成されたソケットハンドル s
を直接 closesocket(s)
で閉じていました。
- 変更後:
netfd.Close()
が呼び出されるようになりました。ここでnetfd
は、新しく作成されたnetFD
オブジェクトを指します。これにより、エラーが発生して接続が確立されなかった場合でも、そのソケットに関連付けられたnetpoll
ディスクリプタを含むすべてのリソースがnetFD.Close()
の完全なクリーンアップロジックを通じて適切に解放されることが保証されます。これは、エラーパスにおけるリソースリークを防ぐ上で非常に重要です。
これらの変更は、Goのネットワークスタックにおけるリソース管理の堅牢性を高め、特にWindows環境でのメモリリーク問題を解決する上で不可欠なものです。
関連リンク
- Goの
net
パッケージのドキュメント: https://pkg.go.dev/net - Goの
runtime.SetFinalizer
のドキュメント: https://pkg.go.dev/runtime#SetFinalizer - Goの
netpoll
に関する議論(一般的な概念): Goのソースコードや設計ドキュメントに散見されますが、特定の公式ドキュメントは少ないです。Goの内部実装に関するブログ記事やカンファレンストークが参考になる場合があります。
参考にした情報源リンク
- Goのコミット履歴: https://github.com/golang/go/commits/master
- Goのコードレビューシステム (Gerrit): https://go.dev/cl/11987043 (元のCL (Change-list) へのリンク)
- Windows Sockets (Winsock) API のドキュメント (Microsoft Learn): https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-2-start-page
- I/O Completion Ports (IOCP) の概念 (Microsoft Learn): https://learn.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports
- Goのガベージコレクションとファイナライザに関する一般的な情報源(例: Go公式ブログ、技術ブログなど)