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

[インデックス 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つのシナリオで発生していました。

  1. runtime.SetFinalizer の不適切な使用: 変更前は、netFD オブジェクトのファイナライザとして (*netFD).closesocket が直接設定されていました。closesocket はOSのソケットハンドルを閉じるだけなので、netFD に関連付けられた netpoll ディスクリプタ (fd.pd) は解放されませんでした。GCが netFD を回収し、closesocket が呼び出されても、fd.pd はリークしたままになっていました。

  2. エラーパスでの不完全なクリーンアップ: 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の内部実装に関するブログ記事やカンファレンストークが参考になる場合があります。

参考にした情報源リンク