[インデックス 16926] ファイルの概要
このコミットは、Go言語のnet
パッケージにおけるUnix環境でのメモリリークを修正するものです。具体的には、netFD
オブジェクトがファイナライザによって閉じられた際に、関連するランタイムのネットポールディスクリプタが適切に解放されない問題に対処しています。
コミット
commit 3b6de5e847bb8bf12f2299e2026ebf35c2026463
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Jul 30 19:47:16 2013 +0400
net: fix memory leak on unix
If netFD is closed by finalizer, runtime netpoll descriptor is not freed.
R=golang-dev, dave, alex.brainman
CC=golang-dev
https://golang.org/cl/12037043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3b6de5e847bb8bf12f2299e2026ebf35c2026463
元コミット内容
net: fix memory leak on unix
If netFD is closed by finalizer, runtime netpoll descriptor is not freed.
変更の背景
Goのネットワーク操作は、内部的にファイルディスクリプタ(FD)を扱います。これらのFDは、ソケット通信などのI/O操作のためにOSによって管理されるリソースです。Goランタイムは、効率的なI/O多重化のために「ネットポール」(netpoll)メカニズムを使用しており、これはUnix系システムではepoll
やkqueue
といったOSの機能を利用して、多数のFDからのイベントを効率的に監視します。
netFD
構造体は、Goのnet
パッケージにおけるネットワーク接続の抽象化を表し、その中に実際のOSのファイルディスクリプタ(sysfd
)と、ネットポールに関連するpoll.FD
(pd
)が含まれています。
問題は、netFD
オブジェクトがGoのガベージコレクタによって回収される際に、そのオブジェクトに設定されたファイナライザ(runtime.SetFinalizer
)が呼び出されることにありました。ファイナライザは、オブジェクトがメモリから解放される直前に特定のクリーンアップ処理を実行するために使用されます。この場合、netFD
のファイナライザはnetFD.Close()
メソッドを呼び出すように設定されていました。
しかし、既存の実装では、ファイナライザ経由でnetFD.Close()
が呼び出された際に、netpoll
に関連するディスクリプタ(fd.pd
)が適切に解放されないケースがありました。具体的には、fd.sysfile
という*os.File
型のフィールドが存在する場合としない場合でクリーンアップロジックが分岐しており、ファイナライザが呼び出すパスではfd.pd.Close()
が実行されない可能性がありました。これにより、netFD
オブジェクト自体はGCによって回収されても、対応するネットポールディスクリプタがOSレベルで解放されず、結果としてメモリリークやリソースリークが発生していました。
このコミットは、このファイナライザ経由でのクリーンアップパスにおけるnetpoll
ディスクリプタのリークを修正することを目的としています。
前提知識の解説
-
ファイルディスクリプタ (File Descriptor, FD): Unix系OSにおいて、ファイル、ソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。ネットワーク通信では、ソケットがFDとして扱われます。
-
Goの
net
パッケージ: Go言語の標準ライブラリで、TCP/IP、UDP、UnixドメインソケットなどのネットワークI/O機能を提供します。内部的にはOSのシステムコールをラップして、クロスプラットフォームで統一されたインターフェースを提供します。 -
netFD
構造体:net
パッケージ内部で使用される構造体で、ネットワーク接続の基本的な情報をカプセル化します。これには、OSのファイルディスクリプタ(sysfd
)、ネットワークタイプ、アドレス情報、そしてGoランタイムのネットポールメカニズムと連携するためのpoll.FD
(pd
)などが含まれます。 -
Goのネットポール (Netpoll): Goランタイムが提供する非同期I/Oメカニズムです。多数のネットワーク接続を効率的に扱うために、OSのI/O多重化機能(Linuxの
epoll
、macOS/BSDのkqueue
など)を利用します。これにより、GoのgoroutineはブロッキングI/O操作を行うことなく、多数のネットワークイベントを同時に処理できます。poll.FD
は、このネットポールメカニズムに登録されるファイルディスクリプタを抽象化したものです。 -
runtime.SetFinalizer
: Goのruntime
パッケージが提供する関数で、オブジェクトがガベージコレクタによってメモリから回収される直前に実行される関数(ファイナライザ)を設定します。ファイナライザは、オブジェクトが保持していたOSリソース(ファイルディスクリプタ、ネットワーク接続など)を解放するために使用されることがあります。ただし、ファイナライザの実行タイミングは保証されず、またファイナライザ内で新たな参照が作られるとオブジェクトが回収されなくなるなど、注意深く使用する必要があります。 -
os.File
とclosesocket
:os.File
: Goのos
パッケージが提供するファイルオブジェクトで、OSのファイルディスクリプタをラップします。os.NewFile
で既存のFDから作成できます。Close()
メソッドは、関連するOSのFDを閉じます。closesocket
: Unix系システムにおけるソケットを閉じるためのシステムコール(またはそのラッパー関数)です。Goの内部では、syscall.Close
などがこれに相当します。
技術的詳細
このコミットの核心は、netFD
オブジェクトのライフサイクル管理、特にガベージコレクションとファイナライザの相互作用におけるリソースリークの防止にあります。
以前の実装では、netFD
がruntime.SetFinalizer
によって(*netFD).Close
をファイナライザとして登録していました。netFD.Close()
メソッドは、ネットワーク接続を閉じるための主要なクリーンアップロジックを含んでいます。このメソッド内には、fd.pd.Close()
(ネットポールディスクリプタの解放)と、fd.sysfd
(OSのファイルディスクリプタ)の解放ロジックが含まれていました。
しかし、netFD.decref()
(netFD.Close()
から呼び出される)の内部で、fd.sysfile
という*os.File
型のフィールドの有無によって、sysfd
を閉じる方法が分岐していました。
// 変更前
if fd.sysfile != nil {
fd.sysfile.Close()
fd.sysfile = nil
} else {
closesocket(fd.sysfd)
}
このロジックの問題点は、fd.sysfile
がnil
でない場合にfd.sysfile.Close()
が呼び出される一方で、fd.pd.Close()
がその条件分岐の外にあり、かつfd.sysfile
がnil
でないパスではfd.pd.Close()
が実行されない可能性があったことです。コミットメッセージによると、「If netFD is closed by finalizer, runtime netpoll descriptor is not freed.」とあるため、ファイナライザ経由でClose
が呼ばれた際に、このfd.sysfile != nil
のパスが選択され、fd.pd.Close()
がスキップされていたと考えられます。fd.pd.Close()
は、GoランタイムのネットポールメカニズムからこのFDを登録解除するために不可欠な処理です。これがスキップされると、OSのFD自体は閉じられても、ランタイム内部のネットポール関連のデータ構造がクリーンアップされず、結果としてメモリリークや、最悪の場合、同じFD番号が再利用された際に誤ったイベントが通知されるなどの問題を引き起こす可能性がありました。
このコミットでは、以下の2つの主要な変更によってこの問題を解決しています。
-
fd.sysfile
フィールドの削除:netFD
構造体からsysfile *os.File
フィールドが削除されました。これにより、netFD
がos.File
オブジェクトを内部的に保持するという二重管理が解消されます。sysfd
(OSのファイルディスクリプタ番号)が直接管理されるようになります。 -
ファイナライザの設定とクリーンアップロジックの簡素化:
netFD.setAddr
メソッド内で、runtime.SetFinalizer(fd, (*netFD).Close)
が設定されるようになりました。これは、netFD
オブジェクトがGCによって回収される際にnetFD.Close()
が呼び出されることを保証します。netFD.decref()
(netFD.Close()
から呼び出される)内のクリーンアップロジックが簡素化されました。fd.sysfile
の有無による分岐がなくなり、常にfd.pd.Close()
が呼び出され、その後closesocket(fd.sysfd)
が呼び出されるようになりました。- さらに、
fd.decref()
の最後にruntime.SetFinalizer(fd, nil)
が追加されました。これは、netFD
が明示的に閉じられた場合(つまり、ファイナライザが不要になった場合)に、ファイナライザを解除することで、ファイナライザが二重に実行されたり、不要な参照が残ったりするのを防ぎます。
これらの変更により、netFD
がファイナライザによって閉じられる場合でも、明示的に閉じられる場合でも、fd.pd.Close()
が確実に呼び出され、ネットポールディスクリプタが適切に解放されるようになりました。これにより、Unix環境におけるnet
パッケージのメモリリークが修正されます。
コアとなるコードの変更箇所
src/pkg/net/fd_unix.go
--- a/src/pkg/net/fd_unix.go
+++ b/src/pkg/net/fd_unix.go
@@ -9,6 +9,7 @@ package net
import (
"io"
"os"
+ "runtime"
"sync"
"syscall"
"time"
@@ -29,7 +30,6 @@ type netFD struct {
family int
sotype int
isConnected bool
- sysfile *os.File
net string
laddr Addr
raddr Addr
@@ -70,7 +70,7 @@ func newFD(fd, family, sotype int, net string) (*netFD, error) {
func (fd *netFD) setAddr(laddr, raddr Addr) {
fd.laddr = laddr
fd.raddr = raddr
- fd.sysfile = os.NewFile(uintptr(fd.sysfd), fd.net)
+ runtime.SetFinalizer(fd, (*netFD).Close)
}
func (fd *netFD) name() string {
@@ -129,15 +129,11 @@ func (fd *netFD) decref() {
fd.sysref--
if fd.closing && fd.sysref == 0 {
// Poller may want to unregister fd in readiness notification mechanism,
-\t\t// so this must be executed before sysfile.Close().
+\t\t// so this must be executed before closesocket.
\t\tfd.pd.Close()
-\t\tif fd.sysfile != nil {\n-\t\t\tfd.sysfile.Close()\n-\t\t\tfd.sysfile = nil\n-\t\t} else {\n-\t\t\tclosesocket(fd.sysfd)\n-\t\t}\n+\t\tclosesocket(fd.sysfd)
\t\tfd.sysfd = -1
+\t\truntime.SetFinalizer(fd, nil)
}
fd.sysmu.Unlock()
}
コアとなるコードの解説
-
import "runtime"
の追加:runtime.SetFinalizer
関数を使用するために、runtime
パッケージがインポートされました。 -
netFD
構造体からのsysfile
フィールド削除:- sysfile *os.File
netFD
構造体からsysfile *os.File
フィールドが削除されました。これにより、netFD
がOSのファイルディスクリプタをsysfd
として直接管理し、os.File
オブジェクトを介した間接的な管理を廃止しました。これは、クリーンアップロジックの複雑さを軽減し、一貫性を保つ上で重要です。 -
netFD.setAddr
でのファイナライザ設定:- fd.sysfile = os.NewFile(uintptr(fd.sysfd), fd.net) + runtime.SetFinalizer(fd, (*netFD).Close)
netFD.setAddr
メソッドは、netFD
オブジェクトが初期化され、アドレスが設定されるタイミングで呼び出されます。変更前はここでos.NewFile
を使ってsysfile
を初期化していましたが、変更後はruntime.SetFinalizer
を呼び出し、netFD
オブジェクトがガベージコレクタによって回収される際に(*netFD).Close
メソッドが呼び出されるように設定しています。これにより、明示的にClose
が呼び出されなかった場合でも、リソースが解放される機会が提供されます。 -
netFD.decref
内のクリーンアップロジックの変更:- // Poller may want to unregister fd in readiness notification mechanism, - // so this must be executed before sysfile.Close(). - fd.pd.Close() - if fd.sysfile != nil { - fd.sysfile.Close() - fd.sysfile = nil - } else { - closesocket(fd.sysfd) - } + // 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 = -1 + runtime.SetFinalizer(fd, nil)
fd.decref()
は、netFD
の参照カウントが減少し、最終的に0になった(つまり、閉じられるべき)場合に呼び出される内部メソッドです。- コメントが
sysfile.Close()
からclosesocket
に変更され、fd.pd.Close()
がclosesocket
の前に実行されるべきであることが明確にされました。これは、ネットポールからの登録解除がOSのFDを閉じる前に行われるべきという順序の重要性を示しています。 fd.sysfile != nil
による条件分岐が削除され、常にfd.pd.Close()
が呼び出されるようになりました。これにより、ファイナライザ経由でClose
が呼び出された場合でも、ネットポールディスクリプタが確実に解放されることが保証されます。closesocket(fd.sysfd)
が直接呼び出されるようになり、os.File
を介した間接的なクローズ処理がなくなりました。runtime.SetFinalizer(fd, nil)
が追加されました。これは、netFD
が明示的に閉じられた場合、ファイナライザが不要になるため、ファイナライザを解除します。これにより、オブジェクトが既にクリーンアップされているにもかかわらず、GCによって再度ファイナライザが呼び出されるといった不必要な処理や潜在的な問題を防ぎます。
- コメントが
これらの変更により、netFD
のクリーンアップパスが簡素化され、netpoll
ディスクリプタのリークが効果的に防止されるようになりました。
関連リンク
- Go Issue: https://golang.org/cl/12037043 (このコミットのChange-ID)
- Go
net
パッケージのドキュメント: https://pkg.go.dev/net - Go
runtime
パッケージのドキュメント: https://pkg.go.dev/runtime
参考にした情報源リンク
- Go言語のソースコード (特に
src/pkg/net/fd_unix.go
の変更履歴) - Go言語のガベージコレクションとファイナライザに関する公式ドキュメントやブログ記事
- Unix系OSにおけるファイルディスクリプタとI/O多重化(epoll/kqueue)に関する一般的な情報
- Goのネットポールメカニズムに関する技術記事や解説