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

[インデックス 13487] ファイルの概要

このコミットは、Go言語の標準ライブラリである net パッケージにおけるファイルディスクリプタのリーク(資源漏洩)を修正するものです。具体的には、FileListenerFileConnFilePacketConn といった、既存のファイルディスクリプタからネットワーク接続やリスナーを生成する機能において発生していたリークが対象です。

コミット

commit d380a9775093cb99e9fb8103955f39b8a15bf60a
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Sun Jul 22 01:48:15 2012 +0900

    net: fix file descriptor leak on FileListener, FileConn and FilePacketConn
    
    R=golang-dev, dave, r
    CC=golang-dev
    https://golang.org/cl/6430062

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d380a9775093cb99e9fb8103955f39b8a15bf60a

元コミット内容

net: fix file descriptor leak on FileListener, FileConn and FilePacketConn

このコミットは、net パッケージ内の FileListenerFileConn、および FilePacketConn の各機能において発生していたファイルディスクリプタのリークを修正することを目的としています。

変更の背景

ファイルディスクリプタ(File Descriptor, FD)のリークは、プログラムがファイルやソケットなどのリソースをオープンした後に適切にクローズしない場合に発生します。これにより、システムが利用可能なファイルディスクリプタの最大数に達し、新たなファイルやネットワーク接続を開けなくなるという問題を引き起こします。これは、アプリケーションの安定性や可用性に深刻な影響を与える可能性があります。

このコミットの背景には、Go言語の net パッケージが提供する os.File からネットワークオブジェクト(net.Listenernet.Conn)を再構築する機能、具体的には net.FileListenernet.FileConnnet.FilePacketConn の実装に潜在的なバグが存在していたことがあります。これらの関数は、既存の os.File オブジェクト(通常はソケットを表すファイルディスクリプタをラップしたもの)を受け取り、それに対応するネットワークオブジェクトを生成します。

問題は、newFileFD 関数内で、syscall.Dup によってファイルディスクリプタが複製された後、その後の処理(syscall.GetsockoptIntnewFD の呼び出し)がエラーになった場合に、複製されたファイルディスクリプタが適切にクローズされないまま残ってしまう点にありました。これにより、エラーパスを通るたびにファイルディスクリプタがリークし、システムリソースを枯渇させる可能性がありました。

前提知識の解説

ファイルディスクリプタ (File Descriptor, FD)

ファイルディスクリプタは、Unix系オペレーティングシステムにおいて、プロセスが開いているファイルやソケット、パイプなどのI/Oリソースを識別するために使用される整数値です。プログラムがファイルを開いたり、ネットワーク接続を確立したりすると、カーネルは対応するファイルディスクリプタをプロセスに割り当てます。これらのリソースは、使用後に明示的にクローズ(解放)される必要があります。クローズされないまま放置されると、ファイルディスクリプタがシステム内で消費され続け、最終的にはシステム全体または特定のプロセスが新たなリソースを開けなくなる「ファイルディスクリプタの枯渇」状態に陥ります。

syscall パッケージ

Go言語の syscall パッケージは、オペレーティングシステムの低レベルなシステムコールへのアクセスを提供します。これにより、GoプログラムからOSのカーネル機能(ファイル操作、ネットワーク操作、プロセス管理など)を直接呼び出すことができます。

syscall.Dup

syscall.Dup は、既存のファイルディスクリプタを複製するシステムコールです。複製されたファイルディスクリプタは、元のファイルディスクリプタと同じファイル記述エントリを参照します。このコミットの文脈では、os.File から得られたファイルディスクリプタを複製し、その複製をネットワーク操作に使用しようとしていました。

syscall.GetsockoptInt

syscall.GetsockoptInt は、ソケットのオプションを取得するためのシステムコールです。第一引数にソケットのファイルディスクリプタ、第二引数にオプションのレベル(例: syscall.SOL_SOCKET)、第三引数にオプション名(例: syscall.SO_TYPE)を指定します。この関数は、指定されたオプションの整数値を返します。

syscall.SOL_SOCKETsyscall.SO_TYPE

  • syscall.SOL_SOCKET: ソケットレベルのオプションを指定するための定数です。
  • syscall.SO_TYPE: ソケットのタイプ(例: SOCK_STREAM (TCP), SOCK_DGRAM (UDP), SOCK_RAW (RAWソケット))を取得するためのオプションです。

closesocket (または syscall.Close)

closesocket は、Windowsにおけるソケットのクローズ関数ですが、Unix系システムでは一般的に close システムコール(Goの syscall パッケージでは syscall.Close)が使用されます。このコミットの文脈では、net パッケージ内部でソケットのファイルディスクリプタをクローズするためのヘルパー関数として closesocket が定義されている可能性があります。ファイルディスクリプタをクローズすることは、それが占有していたシステムリソースを解放し、リークを防ぐために不可欠です。

netFD

Go言語の net パッケージ内部で使用される構造体で、ネットワーク接続を表すファイルディスクリプタ(ソケット)を抽象化したものです。netFD は、ソケットのファイルディスクリプタ、ネットワークタイプ、アドレス情報などを管理し、GoのネットワークI/O操作の基盤となります。

FileListener, FileConn, FilePacketConn

これらは net パッケージが提供する関数で、既存の os.File オブジェクト(通常はソケットを表すファイルディスクリプタ)から、それぞれ net.Listenernet.Connnet.PacketConn インターフェースを実装するオブジェクトを生成します。これにより、外部から渡されたファイルディスクリプタをGoのネットワークI/Oシステムに統合することが可能になります。

技術的詳細

このコミットが修正する問題は、src/pkg/net/file.go 内の newFileFD 関数にありました。この関数は、os.File オブジェクトから netFD オブジェクトを生成する役割を担っています。

newFileFD 関数は以下の手順で処理を進めます。

  1. 入力として与えられた os.File から、その基となるファイルディスクリプタを取得します。
  2. syscall.Dup を呼び出して、このファイルディスクリプタを複製します。これは、元の os.File がGoのガベージコレクタによってクローズされる可能性があるため、複製されたディスクリプタを netFD が独立して管理できるようにするためです。
  3. 複製されたファイルディスクリプタ (fd) を使用して、ソケットのタイプ(SOCK_STREAM, SOCK_DGRAM など)を取得するために syscall.GetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_TYPE) を呼び出します。
  4. ソケットのアドレスファミリー(AF_INET, AF_INET6, AF_UNIX など)を特定し、適切なアドレス変換関数 (toAddr) を設定します。
  5. 最後に、複製されたファイルディスクリプタ (fd) と取得した情報(アドレスファミリー、ソケットタイプなど)を用いて newFD 関数を呼び出し、実際の netFD オブジェクトを生成します。

問題は、ステップ3またはステップ5でエラーが発生した場合に、ステップ2で複製されたファイルディスクリプタ (fd) がクローズされないまま残ってしまうことでした。例えば、syscall.GetsockoptInt が何らかの理由で失敗した場合、関数はエラーを返して終了しますが、複製された fd は閉じられず、リークが発生していました。同様に、newFD が失敗した場合も、複製された fd はリークしていました。

このコミットは、これらのエラーパスに closesocket(fd) の呼び出しを追加することで、このリークを解消しています。これにより、newFileFD 関数が途中でエラーを返して終了する際にも、複製されたファイルディスクリプタが確実に解放されるようになります。

また、proto という変数名が sotype に変更されていますが、これは機能的な変更ではなく、ソケットタイプ(socket type)を表す変数であることをより明確にするためのリファクタリングです。

コアとなるコードの変更箇所

src/pkg/net/file.go ファイルの変更点です。

--- a/src/pkg/net/file.go
+++ b/src/pkg/net/file.go
@@ -17,8 +17,9 @@ func newFileFD(f *os.File) (*netFD, error) {
 	return nil, os.NewSyscallError("dup", err)
 }
 
-	proto, err := syscall.GetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_TYPE)
+	sotype, err := syscall.GetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_TYPE)
 	if err != nil {
+		closesocket(fd)
 		return nil, os.NewSyscallError("getsockopt", err)
 	}
 
@@ -31,24 +32,24 @@ func newFileFD(f *os.File) (*netFD, error) {
 	return nil, syscall.EINVAL
 case *syscall.SockaddrInet4:
 	family = syscall.AF_INET
-		if proto == syscall.SOCK_DGRAM {
+		if sotype == syscall.SOCK_DGRAM {
 			toAddr = sockaddrToUDP
-		} else if proto == syscall.SOCK_RAW {
+		} else if sotype == syscall.SOCK_RAW {
 			toAddr = sockaddrToIP
 		}
 case *syscall.SockaddrInet6:
 	family = syscall.AF_INET6
-		if proto == syscall.SOCK_DGRAM {
+		if sotype == syscall.SOCK_DGRAM {
 			toAddr = sockaddrToUDP
-		} else if proto == syscall.SOCK_RAW {
+		} else if sotype == syscall.SOCK_RAW {
 			toAddr = sockaddrToIP
 		}
 case *syscall.SockaddrUnix:
 	family = syscall.AF_UNIX
 	toAddr = sockaddrToUnix
-		if proto == syscall.SOCK_DGRAM {
+		if sotype == syscall.SOCK_DGRAM {
 			toAddr = sockaddrToUnixgram
-		} else if proto == syscall.SOCK_SEQPACKET {
+		} else if sotype == syscall.SOCK_SEQPACKET {
 			toAddr = sockaddrToUnixpacket
 		}
 	}
@@ -56,8 +57,9 @@ func newFileFD(f *os.File) (*netFD, error) {
 	sa, _ = syscall.Getpeername(fd)
 	raddr := toAddr(sa)
 
-	netfd, err := newFD(fd, family, proto, laddr.Network())
+	netfd, err := newFD(fd, family, sotype, laddr.Network())
 	if err != nil {
+		closesocket(fd)
 		return nil, err
 	}
 	netfd.setAddr(laddr, raddr)

コアとなるコードの解説

このコミットの主要な変更点は、newFileFD 関数内の2つのエラーパスに closesocket(fd) の呼び出しを追加したことです。

  1. syscall.GetsockoptInt のエラーハンドリングの改善: 変更前:

    proto, err := syscall.GetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_TYPE)
    if err != nil {
        return nil, os.NewSyscallError("getsockopt", err)
    }
    

    変更後:

    sotype, err := syscall.GetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_TYPE)
    if err != nil {
        closesocket(fd) // ここが追加された
        return nil, os.NewSyscallError("getsockopt", err)
    }
    

    この変更により、syscall.GetsockoptInt の呼び出しが失敗した場合、syscall.Dup で複製されたファイルディスクリプタ fd が即座に closesocket によって閉じられるようになりました。これにより、このエラーパスでのファイルディスクリプタのリークが防止されます。また、変数名が proto から sotype に変更され、ソケットタイプをより明確に表すようになりました。

  2. newFD のエラーハンドリングの改善: 変更前:

    netfd, err := newFD(fd, family, proto, laddr.Network())
    if err != nil {
        return nil, err
    }
    

    変更後:

    netfd, err := newFD(fd, family, sotype, laddr.Network()) // proto も sotype に変更
    if err != nil {
        closesocket(fd) // ここが追加された
        return nil, err
    }
    

    同様に、newFD 関数の呼び出しが失敗した場合にも、複製されたファイルディスクリプタ fdclosesocket によって閉じられるようになりました。これにより、netFD オブジェクトの生成に失敗した場合のリークも防止されます。

これらの変更は、newFileFD 関数が正常に netFD オブジェクトを返すことができない場合に、関数内で開かれた(複製された)ファイルディスクリプタが確実にクリーンアップされるようにするためのものです。これにより、ファイルディスクリプタのリークという深刻なバグが修正されました。

関連リンク

参考にした情報源リンク

  • Go言語の net パッケージのドキュメント
  • Unix系システムにおけるファイルディスクリプタとソケットプログラミングに関する一般的な情報
  • syscall.Dup および syscall.GetsockoptInt のGo言語ドキュメント
  • ファイルディスクリプタリークに関する一般的な情報源 (例: Wikipedia, 技術ブログなど)