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

[インデックス 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系システムではepollkqueueといったOSの機能を利用して、多数のFDからのイベントを効率的に監視します。

netFD構造体は、Goのnetパッケージにおけるネットワーク接続の抽象化を表し、その中に実際のOSのファイルディスクリプタ(sysfd)と、ネットポールに関連するpoll.FDpd)が含まれています。

問題は、netFDオブジェクトがGoのガベージコレクタによって回収される際に、そのオブジェクトに設定されたファイナライザ(runtime.SetFinalizer)が呼び出されることにありました。ファイナライザは、オブジェクトがメモリから解放される直前に特定のクリーンアップ処理を実行するために使用されます。この場合、netFDのファイナライザはnetFD.Close()メソッドを呼び出すように設定されていました。

しかし、既存の実装では、ファイナライザ経由でnetFD.Close()が呼び出された際に、netpollに関連するディスクリプタ(fd.pd)が適切に解放されないケースがありました。具体的には、fd.sysfileという*os.File型のフィールドが存在する場合としない場合でクリーンアップロジックが分岐しており、ファイナライザが呼び出すパスではfd.pd.Close()が実行されない可能性がありました。これにより、netFDオブジェクト自体はGCによって回収されても、対応するネットポールディスクリプタがOSレベルで解放されず、結果としてメモリリークやリソースリークが発生していました。

このコミットは、このファイナライザ経由でのクリーンアップパスにおけるnetpollディスクリプタのリークを修正することを目的としています。

前提知識の解説

  1. ファイルディスクリプタ (File Descriptor, FD): Unix系OSにおいて、ファイル、ソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。ネットワーク通信では、ソケットがFDとして扱われます。

  2. Goのnetパッケージ: Go言語の標準ライブラリで、TCP/IP、UDP、UnixドメインソケットなどのネットワークI/O機能を提供します。内部的にはOSのシステムコールをラップして、クロスプラットフォームで統一されたインターフェースを提供します。

  3. netFD構造体: netパッケージ内部で使用される構造体で、ネットワーク接続の基本的な情報をカプセル化します。これには、OSのファイルディスクリプタ(sysfd)、ネットワークタイプ、アドレス情報、そしてGoランタイムのネットポールメカニズムと連携するためのpoll.FDpd)などが含まれます。

  4. Goのネットポール (Netpoll): Goランタイムが提供する非同期I/Oメカニズムです。多数のネットワーク接続を効率的に扱うために、OSのI/O多重化機能(Linuxのepoll、macOS/BSDのkqueueなど)を利用します。これにより、GoのgoroutineはブロッキングI/O操作を行うことなく、多数のネットワークイベントを同時に処理できます。poll.FDは、このネットポールメカニズムに登録されるファイルディスクリプタを抽象化したものです。

  5. runtime.SetFinalizer: Goのruntimeパッケージが提供する関数で、オブジェクトがガベージコレクタによってメモリから回収される直前に実行される関数(ファイナライザ)を設定します。ファイナライザは、オブジェクトが保持していたOSリソース(ファイルディスクリプタ、ネットワーク接続など)を解放するために使用されることがあります。ただし、ファイナライザの実行タイミングは保証されず、またファイナライザ内で新たな参照が作られるとオブジェクトが回収されなくなるなど、注意深く使用する必要があります。

  6. os.Fileclosesocket:

    • os.File: Goのosパッケージが提供するファイルオブジェクトで、OSのファイルディスクリプタをラップします。os.NewFileで既存のFDから作成できます。Close()メソッドは、関連するOSのFDを閉じます。
    • closesocket: Unix系システムにおけるソケットを閉じるためのシステムコール(またはそのラッパー関数)です。Goの内部では、syscall.Closeなどがこれに相当します。

技術的詳細

このコミットの核心は、netFDオブジェクトのライフサイクル管理、特にガベージコレクションとファイナライザの相互作用におけるリソースリークの防止にあります。

以前の実装では、netFDruntime.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.sysfilenilでない場合にfd.sysfile.Close()が呼び出される一方で、fd.pd.Close()がその条件分岐の外にあり、かつfd.sysfilenilでないパスでは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つの主要な変更によってこの問題を解決しています。

  1. fd.sysfileフィールドの削除: netFD構造体からsysfile *os.Fileフィールドが削除されました。これにより、netFDos.Fileオブジェクトを内部的に保持するという二重管理が解消されます。sysfd(OSのファイルディスクリプタ番号)が直接管理されるようになります。

  2. ファイナライザの設定とクリーンアップロジックの簡素化:

    • 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()
 }

コアとなるコードの解説

  1. import "runtime" の追加: runtime.SetFinalizer関数を使用するために、runtimeパッケージがインポートされました。

  2. netFD構造体からのsysfileフィールド削除:

    -	sysfile     *os.File
    

    netFD構造体からsysfile *os.Fileフィールドが削除されました。これにより、netFDがOSのファイルディスクリプタをsysfdとして直接管理し、os.Fileオブジェクトを介した間接的な管理を廃止しました。これは、クリーンアップロジックの複雑さを軽減し、一貫性を保つ上で重要です。

  3. 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が呼び出されなかった場合でも、リソースが解放される機会が提供されます。

  4. 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言語のソースコード (特にsrc/pkg/net/fd_unix.goの変更履歴)
  • Go言語のガベージコレクションとファイナライザに関する公式ドキュメントやブログ記事
  • Unix系OSにおけるファイルディスクリプタとI/O多重化(epoll/kqueue)に関する一般的な情報
  • Goのネットポールメカニズムに関する技術記事や解説