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

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

このコミットは、src/pkg/net/fd_unix.go ファイルに対して行われた変更です。このファイルは、Go言語の標準ライブラリである net パッケージの一部であり、Unix系システムにおけるネットワークファイルディスクリプタ(FD)の管理と、それに関連する低レベルのネットワーク操作(ソケットの接続など)を扱っています。

コミット

commit 672525a56e7d326ec986bc330a7accd8ec0395f4
Author: Ian Lance Taylor <iant@golang.org>
Date:   Sat Dec 28 09:37:54 2013 -0800

    net: work around Solaris connect issue when server closes socket
    
    On Solaris, if you do a in-progress connect, and then the
    server accepts and closes the socket, the client's later
    attempt to complete the connect will fail with EINVAL.  Handle
    this case by assuming that the connect succeeded.  This code
    is weird enough that it is implemented as Solaris-only so that
    it doesn't hide a real error on a different OS.
    
    Update #6828
    
    R=golang-codereviews, bradfitz, dave
    CC=golang-codereviews
    https://golang.org/cl/46160043

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

https://github.com/golang/go/commit/672525a56e7d326ec986bc330a7accd8ec0395f4

元コミット内容

このコミットは、Go言語の net パッケージにおいて、Solarisオペレーティングシステム特有の connect システムコールに関する問題を回避するための修正を導入しています。具体的には、クライアントがノンブロッキング connect を実行中に、サーバーが接続を受け入れた直後にソケットを閉じてしまうと、クライアント側で EINVAL (Invalid argument) エラーが発生する問題に対応しています。この修正では、このような特定の状況下で EINVAL エラーが発生した場合、接続が成功したとみなすことで問題を回避しています。この回避策はSolarisに限定されており、他のOSでの予期せぬエラーを隠蔽しないように配慮されています。

変更の背景

この変更の背景には、GoプログラムがSolaris上でネットワーク接続を確立する際に直面していた特定のエッジケースがあります。通常、connect システムコールは、接続が確立されるか、エラーが発生するまでブロックするか、ノンブロッキングモードの場合は EINPROGRESSEALREADY といったエラーを返して処理を継続します。

しかし、Solaris環境では、以下のような特殊なシーケンスで問題が発生していました。

  1. クライアントがノンブロッキングモードで connect システムコールを発行し、接続処理が進行中となる(EINPROGRESS などが返される)。
  2. その間にサーバーがクライアントからの接続要求を受け入れ(accept)、直後にそのソケットを閉じてしまう
  3. クライアントが後から connect の完了を試みる(例えば、selectpoll でソケットが書き込み可能になったことを検知した後、再度 connect を呼び出すか、ソケットの状態を確認する)。

このシナリオにおいて、Solarisの connect は予期せず EINVAL エラーを返していました。一般的な EINVAL は引数の不正を示しますが、このケースではソケットの状態が原因で発生しており、Goの net パッケージが期待するエラーハンドリングのフローから外れていました。この問題は、GoのIssue #6828として報告され、このコミットはその解決策として実装されました。

この問題は、特にサーバーが短命な接続を扱う場合や、接続確立直後に何らかの理由でソケットを閉じるようなアプリケーションで顕在化する可能性がありました。Goの net パッケージは、様々なOSで一貫したネットワークI/Oの抽象化を提供することを目指しているため、このようなOS固有の挙動の違いを吸収する必要がありました。

前提知識の解説

connect システムコール

connect は、クライアントがサーバーへの接続を確立するために使用するシステムコールです。

  • ブロッキングモード: デフォルトでは、接続が確立されるかエラーが発生するまで呼び出し元をブロックします。
  • ノンブロッキングモード: ソケットがノンブロッキングモードに設定されている場合、connect はすぐに戻り、接続が進行中であることを示す EINPROGRESS (または EALREADY) エラーを返します。実際の接続完了は、後でソケットが書き込み可能になったことを selectpoll などのI/O多重化メカニズムで検知し、再度ソケットの状態を確認することで判断します。

エラーコード

  • EINPROGRESS: ノンブロッキングソケットで接続がすぐに完了せず、処理が進行中であることを示します。
  • EALREADY: ノンブロッキングソケットで既に接続が進行中であることを示します。
  • EINVAL: 一般的には、システムコールに渡された引数が無効であることを示します。しかし、OSや特定の状況によっては、より複雑なソケットの状態変化を示すために使用されることがあります。このコミットのケースがまさにそれにあたります。
  • EISCONN: ソケットが既に接続されていることを示します。

Solarisオペレーティングシステム

Solarisは、Sun Microsystems(現在はOracleが所有)によって開発されたUnix系オペレーティングシステムです。そのネットワークスタックやシステムコールの実装には、他のUnix系OS(LinuxやBSDなど)とは異なる独自の挙動やセマンティクスが存在することがあります。このコミットで扱われている EINVAL エラーの挙動は、まさにその一例です。

Go言語の net パッケージ

Goの net パッケージは、TCP/IP、UDP、Unixドメインソケットなど、様々なネットワークプロトコルを扱うための高レベルなインターフェースを提供します。内部的には、OSの提供する低レベルなシステムコール(socket, bind, listen, accept, connect など)を抽象化し、Goの並行処理モデル(goroutineとchannel)と統合して、効率的で使いやすいネットワークプログラミングを可能にしています。

fd_unix.go ファイル

src/pkg/net/fd_unix.go は、Goの net パッケージ内で、Unix系システム(Linux, macOS, FreeBSD, Solarisなど)におけるファイルディスクリプタ(FD)の管理と、それらを通じたネットワークI/O操作を実装しているファイルです。netFD 構造体は、ネットワーク接続を表すファイルディスクリプタをラップし、connect メソッドのような低レベルな操作をカプセル化しています。このファイルは、OS固有のシステムコールを直接呼び出す syscall パッケージと密接に連携しています。

技術的詳細

このコミットが対処している技術的な問題は、Solarisの connect システムコールが、特定の競合状態において標準的ではない EINVAL エラーを返すという点にあります。

Goの net パッケージの connect メソッド(fd_unix.go 内)は、ノンブロッキング接続を処理するためにループを使用しています。このループは、connect システムコールを呼び出し、返されたエラーコードに基づいて処理を分岐します。

一般的なフローは以下の通りです。

  1. connect を呼び出す。
  2. エラーが nil または syscall.EISCONN であれば、接続成功とみなしループを抜ける。
  3. エラーが syscall.EINPROGRESS または syscall.EALREADY であれば、接続が進行中なので、I/O多重化メカニズム(poll など)でソケットが書き込み可能になるのを待つ。
  4. その他のエラーであれば、接続失敗としてエラーを返す。

Solarisの特殊なケースでは、サーバーが接続を受け入れた直後にソケットを閉じてしまうと、クライアントが connect の完了を試みた際に、EINPROGRESSEALREADY ではなく、syscall.EINVAL が返されていました。Goの既存のロジックでは EINVAL は一般的なエラーとして扱われ、接続失敗と判断されていました。しかし、この特定のシナリオでは、EINVAL は実際には接続が一時的に確立された後にサーバー側で切断されたことを示唆しており、クライアント側から見れば「接続は試みられたが、すぐに終了した」という状態に近いものでした。

このコミットの解決策は、このSolaris特有の EINVAL を「成功」とみなすことです。なぜなら、この状況で EINVAL が返された場合、ソケットは実際には接続可能な状態になっており、その後のソケットへの書き込み操作は即座にEOF(End Of File)を返すことが期待されるからです。これは、接続が確立された直後に相手が切断した場合の一般的な挙動と一致します。

この回避策がSolarisに限定されているのは非常に重要です。他のOSで EINVAL が返された場合、それは本当に引数の不正や他の深刻な問題を示している可能性が高く、それを成功とみなしてしまうと、本来検出されるべきエラーが隠蔽されてしまうためです。runtime.GOOS == "solaris" というチェックを入れることで、この特殊な挙動がSolaris環境でのみ適用されるようにしています。

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

変更は src/pkg/net/fd_unix.go ファイルの func (fd *netFD) connect(la, ra syscall.Sockaddr) error メソッド内で行われています。

--- a/src/pkg/net/fd_unix.go
+++ b/src/pkg/net/fd_unix.go
@@ -80,6 +80,16 @@ func (fd *netFD) connect(la, ra syscall.Sockaddr) error {
 		if err == nil || err == syscall.EISCONN {
 			break
 		}
+
+		// On Solaris we can see EINVAL if the socket has
+		// already been accepted and closed by the server.
+		// Treat this as a successful connection--writes to
+		// the socket will see EOF.  For details and a test
+		// case in C see http://golang.org/issue/6828.
+		if runtime.GOOS == "solaris" && err == syscall.EINVAL {
+			break
+		}
+
 		if err != syscall.EINPROGRESS && err != syscall.EALREADY && err != syscall.EINTR {
 			return err
 		}

コアとなるコードの解説

追加されたコードブロックは以下の通りです。

		// On Solaris we can see EINVAL if the socket has
		// already been accepted and closed by the server.
		// Treat this as a successful connection--writes to
		// the socket will see EOF.  For details and a test
		// case in C see http://golang.org/issue/6828.
		if runtime.GOOS == "solaris" && err == syscall.EINVAL {
			break
		}

このコードは、connect システムコールがエラーを返した場合の処理フローに挿入されています。

  1. runtime.GOOS == "solaris": これは、現在のGoプログラムがSolarisオペレーティングシステム上で実行されているかどうかをチェックする条件です。このチェックにより、この特殊な回避策がSolaris環境でのみ適用されることが保証されます。他のOSでは、このブロックはスキップされます。
  2. err == syscall.EINVAL: これは、connect システムコールが返したエラーが EINVAL であるかどうかをチェックする条件です。

この両方の条件が真である場合、つまり「Solaris上で connectEINVAL を返した」場合に、break ステートメントが実行されます。break は、現在の for ループ(connect の再試行ループ)を終了させます。これにより、Goランタイムは EINVAL エラーを「接続成功」として扱い、connect メソッドから正常にリターンします。

コメントにもあるように、この状況で接続を成功とみなす理由は、「ソケットへの書き込みがEOF(End Of File)を返す」ためです。これは、接続が確立された直後に相手側が接続を閉じた場合の一般的なネットワークプロトコルの挙動と一致します。クライアントは接続できたと認識し、データを送信しようとするとすぐに接続が切れていることを検知できるため、アプリケーションレベルでの適切なエラーハンドリングが可能になります。

この修正は、Solarisの connect の特殊な挙動を吸収し、Goの net パッケージが異なるOS間で一貫したネットワークI/Oインターフェースを提供するための重要なステップです。

関連リンク

参考にした情報源リンク

  • Go Issue #6828 (上記に同じ)
  • Go CL 46160043 (上記に同じ)
  • Web検索: "Solaris connect EINVAL issue golang issue 6828"