[インデックス 13487] ファイルの概要
このコミットは、Go言語の標準ライブラリである net
パッケージにおけるファイルディスクリプタのリーク(資源漏洩)を修正するものです。具体的には、FileListener
、FileConn
、FilePacketConn
といった、既存のファイルディスクリプタからネットワーク接続やリスナーを生成する機能において発生していたリークが対象です。
コミット
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
パッケージ内の FileListener
、FileConn
、および FilePacketConn
の各機能において発生していたファイルディスクリプタのリークを修正することを目的としています。
変更の背景
ファイルディスクリプタ(File Descriptor, FD)のリークは、プログラムがファイルやソケットなどのリソースをオープンした後に適切にクローズしない場合に発生します。これにより、システムが利用可能なファイルディスクリプタの最大数に達し、新たなファイルやネットワーク接続を開けなくなるという問題を引き起こします。これは、アプリケーションの安定性や可用性に深刻な影響を与える可能性があります。
このコミットの背景には、Go言語の net
パッケージが提供する os.File
からネットワークオブジェクト(net.Listener
や net.Conn
)を再構築する機能、具体的には net.FileListener
、net.FileConn
、net.FilePacketConn
の実装に潜在的なバグが存在していたことがあります。これらの関数は、既存の os.File
オブジェクト(通常はソケットを表すファイルディスクリプタをラップしたもの)を受け取り、それに対応するネットワークオブジェクトを生成します。
問題は、newFileFD
関数内で、syscall.Dup
によってファイルディスクリプタが複製された後、その後の処理(syscall.GetsockoptInt
や newFD
の呼び出し)がエラーになった場合に、複製されたファイルディスクリプタが適切にクローズされないまま残ってしまう点にありました。これにより、エラーパスを通るたびにファイルディスクリプタがリークし、システムリソースを枯渇させる可能性がありました。
前提知識の解説
ファイルディスクリプタ (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_SOCKET
と syscall.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.Listener
、net.Conn
、net.PacketConn
インターフェースを実装するオブジェクトを生成します。これにより、外部から渡されたファイルディスクリプタをGoのネットワークI/Oシステムに統合することが可能になります。
技術的詳細
このコミットが修正する問題は、src/pkg/net/file.go
内の newFileFD
関数にありました。この関数は、os.File
オブジェクトから netFD
オブジェクトを生成する役割を担っています。
newFileFD
関数は以下の手順で処理を進めます。
- 入力として与えられた
os.File
から、その基となるファイルディスクリプタを取得します。 syscall.Dup
を呼び出して、このファイルディスクリプタを複製します。これは、元のos.File
がGoのガベージコレクタによってクローズされる可能性があるため、複製されたディスクリプタをnetFD
が独立して管理できるようにするためです。- 複製されたファイルディスクリプタ (
fd
) を使用して、ソケットのタイプ(SOCK_STREAM
,SOCK_DGRAM
など)を取得するためにsyscall.GetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_TYPE)
を呼び出します。 - ソケットのアドレスファミリー(
AF_INET
,AF_INET6
,AF_UNIX
など)を特定し、適切なアドレス変換関数 (toAddr
) を設定します。 - 最後に、複製されたファイルディスクリプタ (
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)
の呼び出しを追加したことです。
-
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
に変更され、ソケットタイプをより明確に表すようになりました。 -
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
関数の呼び出しが失敗した場合にも、複製されたファイルディスクリプタfd
がclosesocket
によって閉じられるようになりました。これにより、netFD
オブジェクトの生成に失敗した場合のリークも防止されます。
これらの変更は、newFileFD
関数が正常に netFD
オブジェクトを返すことができない場合に、関数内で開かれた(複製された)ファイルディスクリプタが確実にクリーンアップされるようにするためのものです。これにより、ファイルディスクリプタのリークという深刻なバグが修正されました。
関連リンク
- Go CL (Code Review) ページ: https://golang.org/cl/6430062
参考にした情報源リンク
- Go言語の
net
パッケージのドキュメント - Unix系システムにおけるファイルディスクリプタとソケットプログラミングに関する一般的な情報
syscall.Dup
およびsyscall.GetsockoptInt
のGo言語ドキュメント- ファイルディスクリプタリークに関する一般的な情報源 (例: Wikipedia, 技術ブログなど)