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

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

このコミットは、Go言語の net パッケージにおけるWindows固有のファイルディスクリプタ (fd) 処理に関するバグ修正です。具体的には、ソケット作成や関連する操作が失敗した場合に、適切にソケットがクローズされないというメモリリークやリソースリークにつながる可能性のある問題を修正しています。

コミット

commit 87b315a78f91447f072a1a37a4b40ebb38475d01
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Wed Dec 5 15:13:03 2012 +1100

    net: add missing close socket code
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/6868067

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

https://github.com/golang/go/commit/87b315a78f91447f072a1a37a4b40ebb38475d01

元コミット内容

net: add missing close socket code

このコミットは、Go言語の net パッケージにおいて、ソケットのクローズ処理が不足している箇所を追加するものです。特にWindows環境でのソケット操作において、エラー発生時にソケットリソースが適切に解放されない問題を解決します。

変更の背景

Go言語の net パッケージは、ネットワーク通信を抽象化し、クロスプラットフォームで動作するように設計されています。しかし、OS固有のシステムコールを扱う際には、それぞれのOSの特性に合わせた細かな調整が必要です。Windows環境では、ソケットはファイルディスクリプタとは異なるハンドルとして扱われ、closesocket という専用のAPIで明示的にクローズする必要があります。

このコミット以前のコードでは、netFD (ネットワークファイルディスクリプタ) の accept メソッド内で新しいソケットが作成された際、その後の処理(例えば CreateIoCompletionPortSetsockopt)が失敗した場合に、作成されたソケットが適切にクローズされないという問題がありました。これにより、エラーパスを通った場合にソケットリソースがリークし、システムのリソースを枯渇させる可能性がありました。

この変更は、このようなリソースリークを防ぎ、ネットワーク操作の堅牢性を向上させることを目的としています。また、timeout_test.godefer ln.Close() が追加されていることから、テストコードにおいてもリソースの適切な解放が意識されていることが伺えます。

前提知識の解説

Go言語の net パッケージ

Go言語の net パッケージは、TCP/IP、UDP、Unixドメインソケットなど、様々なネットワークプロトコルを扱うための基本的なインターフェースを提供します。このパッケージは、OS固有のネットワークAPIを抽象化し、開発者がプラットフォームの違いを意識せずにネットワークアプリケーションを記述できるようにします。

syscall パッケージ

syscall パッケージは、GoプログラムからOSのシステムコールを直接呼び出すためのインターフェースを提供します。これにより、Goの標準ライブラリでは提供されていない低レベルのOS機能にアクセスしたり、OS固有の動作を制御したりすることが可能になります。ネットワーク操作においては、ソケットの作成、バインド、リスン、接続、送受信など、多くの操作が内部的に syscall パッケージを介して行われます。

Windowsにおけるソケットと closesocket

Windowsでは、ソケットはファイルディスクリプタではなく、SOCKET 型のハンドルとして扱われます。Unix系OSの close() 関数とは異なり、Windowsではソケットを閉じるために closesocket() という専用のAPI関数を使用する必要があります。この関数は、ソケットに関連付けられたリソースを解放し、ソケットハンドルを無効にします。

OpError

net.OpError は、Go言語の net パッケージで定義されているエラー型の一つです。ネットワーク操作中に発生したエラーを詳細に表現するために使用されます。このエラー型は、操作の種類 (Op)、ネットワークの種類 (Net)、ローカルアドレス (Source)、リモートアドレス (Addr)、そして基となるエラー (Err) の情報を含みます。これにより、エラーの原因とコンテキストをより詳細に把握することができます。

IOCP (I/O Completion Port)

IOCPは、Windowsにおける高性能な非同期I/Oメカニズムです。複数のI/O操作を効率的に処理するために使用され、特に多数の同時接続を扱うサーバーアプリケーションでその真価を発揮します。ソケットがIOCPに関連付けられると、I/O操作の完了はスレッドプールに通知され、効率的なスレッド利用が可能になります。

技術的詳細

このコミットの主要な変更は、src/pkg/net/fd_windows.go ファイル内の netFD.accept メソッドに集中しています。このメソッドは、新しいネットワーク接続を受け入れる際に呼び出されます。

変更前は、新しいソケット ssyscall.Socket によって作成された後、いくつかのエラーパスで closesocket(s) が呼び出されていませんでした。

  1. syscall.CreateIoCompletionPort の失敗時: 新しいソケット s が作成された後、syscall.CreateIoCompletionPort を呼び出してソケットをIOCPに関連付けようとします。この操作が失敗した場合、変更前は単にエラーを返していましたが、作成されたソケット s はクローズされずにリークしていました。 変更後、if _, err := syscall.CreateIoCompletionPort(...) のエラーチェックの直後に closesocket(s) が追加され、ソケットが適切に解放されるようになりました。

  2. syscall.Setsockopt の失敗時: AcceptEx のコンテキストを更新するために syscall.Setsockopt が呼び出されます。この操作が失敗した場合も、変更前はソケット s がクローズされずにリークしていました。 変更後、if err != nil のエラーチェックの直後に closesocket(s) が追加され、ソケットが適切に解放されるようになりました。

また、エラーを返す際に、単に return nil, err としていた箇所が return nil, &OpError{...} に変更されています。これは、より詳細なエラー情報を提供する net.OpError を使用することで、デバッグやエラーハンドリングを容易にするための改善です。

src/pkg/net/timeout_test.go の変更は、テストの堅牢性を高めるためのものです。TestReadWriteDeadline 関数内で net.ListenTCP で作成されたリスナー ln に対して defer ln.Close() が追加されました。これにより、テストが終了する際にリスナーソケットが確実にクローズされるようになり、テスト実行後のリソースリークを防ぎます。これは、本質的なバグ修正とは直接関係ありませんが、リソース管理のベストプラクティスに従った改善です。

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

src/pkg/net/fd_windows.go

--- a/src/pkg/net/fd_windows.go
+++ b/src/pkg/net/fd_windows.go
@@ -557,7 +557,7 @@ func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (*netFD, error) {
 	s, err := syscall.Socket(fd.family, fd.sotype, 0)
 	if err != nil {
 		syscall.ForkLock.RUnlock()
-		return nil, err
+		return nil, &OpError{"socket", fd.net, fd.laddr, err}
 	}
 	syscall.CloseOnExec(s)
 	syscall.ForkLock.RUnlock()
@@ -565,6 +565,7 @@ func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (*netFD, error) {
 	// Associate our new socket with IOCP.
 	onceStartServer.Do(startServer)
 	if _, err := syscall.CreateIoCompletionPort(s, resultsrv.iocp, 0, 0); err != nil {
+		closesocket(s)
 		return nil, &OpError{"CreateIoCompletionPort", fd.net, fd.laddr, err}
 	}
 
@@ -582,7 +583,7 @@ func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) {\n 	err = syscall.Setsockopt(s, syscall.SOL_SOCKET, syscall.SO_UPDATE_ACCEPT_CONTEXT, (*byte)(unsafe.Pointer(&fd.sysfd)), int32(unsafe.Sizeof(fd.sysfd)))\n 	if err != nil {\n 		closesocket(s)\n-		return nil, err\n+		return nil, &OpError{"Setsockopt", fd.net, fd.laddr, err}\n 	}\n 
 	// Get local and peer addr out of AcceptEx buffer.

src/pkg/net/timeout_test.go

--- a/src/pkg/net/timeout_test.go
+++ b/src/pkg/net/timeout_test.go
@@ -332,6 +332,7 @@ func TestReadWriteDeadline(t *testing.T) {
 	if err != nil {
 		t.Fatalf("ListenTCP on :0: %v", err)
 	}\n+	defer ln.Close()\n 
 	lnquit := make(chan bool)\n 

コアとなるコードの解説

src/pkg/net/fd_windows.go の変更点

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

    s, err := syscall.Socket(fd.family, fd.sotype, 0)
    if err != nil {
        syscall.ForkLock.RUnlock()
        return nil, err
    }
    

    変更後:

    s, err := syscall.Socket(fd.family, fd.sotype, 0)
    if err != nil {
        syscall.ForkLock.RUnlock()
        return nil, &OpError{"socket", fd.net, fd.laddr, err}
    }
    

    syscall.Socket が失敗した場合に、より詳細なエラー情報を含む net.OpError を返すようになりました。これにより、エラー発生時のコンテキスト(どの操作で、どのネットワークタイプで、どのローカルアドレスでエラーが発生したか)が明確になります。

  2. syscall.CreateIoCompletionPort エラー時のソケットクローズ: 変更前は、syscall.CreateIoCompletionPort が失敗した場合にソケット s がクローズされていませんでした。 変更後:

    if _, err := syscall.CreateIoCompletionPort(s, resultsrv.iocp, 0, 0); err != nil {
        closesocket(s) // ★追加された行
        return nil, &OpError{"CreateIoCompletionPort", fd.net, fd.laddr, err}
    }
    

    closesocket(s) が追加され、IOCPへの関連付けが失敗した場合でも、新しく作成されたソケット s が適切にクローズされるようになりました。これにより、ソケットリソースのリークが防止されます。

  3. syscall.Setsockopt エラー時のソケットクローズ: 変更前は、syscall.Setsockopt が失敗した場合にソケット s がクローズされていませんでした。 変更後:

    err = syscall.Setsockopt(s, syscall.SOL_SOCKET, syscall.SO_UPDATE_ACCEPT_CONTEXT, (*byte)(unsafe.Pointer(&fd.sysfd)), int32(unsafe.Sizeof(fd.sysfd)))
    if err != nil {
        closesocket(s) // ★追加された行
        return nil, &OpError{"Setsockopt", fd.net, fd.laddr, err}
    }
    

    同様に closesocket(s) が追加され、Setsockopt が失敗した場合でもソケット s が適切にクローズされるようになりました。また、エラー型も net.OpError に変更されています。

src/pkg/net/timeout_test.go の変更点

func TestReadWriteDeadline(t *testing.T) {
    // ...
    ln, err := net.ListenTCP("tcp", &net.TCPAddr{Port: 0})
    if err != nil {
        t.Fatalf("ListenTCP on :0: %v", err)
    }
    defer ln.Close() // ★追加された行

    // ...
}

defer ln.Close() が追加されたことで、TestReadWriteDeadline 関数が終了する際に、net.ListenTCP で作成されたリスナーソケット ln が確実にクローズされるようになりました。defer ステートメントは、関数がリターンする直前に指定された関数を実行するため、エラーが発生した場合でも、正常終了した場合でも、確実にリソースが解放されます。これは、テストのクリーンアップとリソース管理のベストプラクティスです。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Windows API ドキュメント
  • Go言語のソースコード (特に src/pkg/net ディレクトリ)
  • 一般的なネットワークプログラミングに関する知識