[インデックス 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, 技術ブログなど)