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

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

このコミットは、Go言語の標準ライブラリ net パッケージ内の src/pkg/net/fd_unix.go ファイルに対する変更です。このファイルは、Unix系システムにおけるネットワークファイルディスクリプタ(netFD)の低レベルな操作、特にソケットの接続(connectシステムコール)に関するロジックを扱っています。GoのネットワークスタックがOSのシステムコールとどのように連携するかを定義する重要な部分です。

コミット

このコミットは、Unix系OSにおける syscall.Connect の複数回呼び出しを回避することを目的としています。特に、以前の修正(CL 69340044)がまだ残していた可能性のある問題を解決し、DragonFly BSDなどのカーネルがストリームベースのトランスポート層プロトコルソケット上で予測不能な非同期接続確立を実行するのを防ぎます。これにより、ネットワーク接続の安定性と信頼性が向上します。

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

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

元コミット内容

net: avoid multiple calling of syscall connect on Unix variants

The previous fix CL 69340044 still leaves a possibility of it.
This CL prevents the kernel, especially DragonFly BSD, from
performing unpredictable asynchronous connection establishment
on stream-based transport layer protocol sockets.

Update #7541
Update #7474

LGTM=jsing
R=jsing
CC=golang-codereviews
https://golang.org/cl/75930043

変更の背景

この変更の背景には、非ブロッキングソケットに対する connect システムコールの挙動と、特定のUnix系OS(特にDragonFly BSDとSolaris)におけるその特殊な振る舞いがあります。

  1. 非ブロッキング connect の問題: 通常、非ブロッキングソケットで connect を呼び出すと、接続が即座に確立できない場合、EINPROGRESS または EALREADY エラーを返します。この場合、アプリケーションはソケットが書き込み可能になるのを待ってから、getsockoptSO_ERROR を使って最終的な接続ステータスを確認する必要があります。しかし、この connect を複数回呼び出すと、特に一部のOSでは予測不能な挙動を引き起こす可能性がありました。
  2. DragonFly BSDの特殊な挙動 (Issue #7474): DragonFly BSDでは、非ブロッキングソケットに対して connect を複数回呼び出すと、以前のエラーが必ずしも返されず、特に localhost への接続時に問題が発生することが報告されていました。netpoll がソケットの準備ができたことを通知した後でも、SO_ERROR を確認しないと真の接続状態が把握できないという問題がありました。以前のコードでは、DragonFly BSDに特化した処理がありましたが、それでも不十分でした。
  3. Solarisの EINVAL 問題 (Issue #6828): Solarisでは、ソケットがすでにサーバーによって受け入れられ、閉じられている場合でも、connectEINVAL エラーを返すことがありました。この場合、Goランタイムはこれを成功した接続として扱う必要がありました。なぜなら、そのソケットへの書き込みはすぐにEOF(End Of File)となるため、実質的には接続が確立されたと見なせるからです。
  4. 以前の修正の不完全さ: コミットメッセージにあるように、以前の修正(CL 69340044)では、これらの問題が完全に解決されておらず、connect の複数回呼び出しの可能性が残っていました。このコミットは、その残された可能性を排除し、より堅牢な接続確立ロジックを提供します。
  5. Issue #7541: このIssueは直接 connect の挙動とは関係ないように見えますが、コミットメッセージで参照されていることから、ネットワーク接続の安定性やエラーハンドリング全般に関連する広範な問題の一部として解決された可能性があります。

前提知識の解説

  • syscall.Connect: 指定されたソケットをリモートアドレスに接続するためのシステムコールです。ブロッキングソケットの場合、接続が確立されるまで(またはエラーが発生するまで)呼び出し元をブロックします。非ブロッキングソケットの場合、接続が即座に確立できない場合はエラーコード(EINPROGRESSなど)を返し、接続処理はバックグラウンドで続行されます。
  • 非ブロッキングソケット: I/O操作が即座に完了しない場合でも、呼び出し元をブロックせずにエラーを返すソケットです。これにより、単一のスレッドで複数のI/O操作を同時に管理できます。
  • syscall.EINPROGRESS: 非ブロッキングソケットで connect が呼び出され、接続がまだ進行中であることを示します。
  • syscall.EALREADY: 非ブロッキングソケットで connect が呼び出され、すでに接続が進行中であることを示します。
  • syscall.EINTR: システムコールがシグナルによって中断されたことを示します。通常、システムコールを再試行する必要があります。
  • syscall.EISCONN: ソケットがすでに接続されていることを示します。
  • syscall.SOL_SOCKET / syscall.SO_ERROR: getsockopt システムコールで使用されるソケットオプションです。SO_ERROR は、ソケットに保留中のエラーがある場合にそのエラーコードを返します。非ブロッキング connect の完了状態を確認するためによく使用されます。エラーがない場合は 0 を返します。
  • netFD: Goの net パッケージ内部で使用される構造体で、OSのファイルディスクリプタ(sysfd)をラップし、Goランタイムのネットワークポーラー(pdpoll.FD)と連携して非同期I/Oを管理します。
  • fd.pd.PrepareWrite() / fd.pd.WaitWrite(): Goのネットワークポーラー(poll.FD)のメソッドです。PrepareWrite() は書き込み操作の準備をし、WaitWrite() はソケットが書き込み可能になるまで待機します。これにより、非ブロッキングI/Oを効率的に処理できます。

技術的詳細

このコミットが解決しようとしている主要な技術的問題は、非ブロッキングソケットにおける connect システムコールのセマンティクスと、異なるUnix系OS間でのその挙動の差異をGoランタイムがどのように統一的に扱うかという点です。

従来のコードでは、syscall.Connect をループ内で複数回呼び出す可能性がありました。これは、EINPROGRESSEALREADY が返された場合に、ポーラーがソケットの準備ができたと通知するまでループを続けるというアプローチでした。しかし、このアプローチには以下の問題がありました。

  1. 複数回 connect 呼び出しの危険性: connect は一度だけ呼び出すべきシステムコールです。非ブロッキングソケットの場合、最初の呼び出しで EINPROGRESS が返されたら、その後はポーラーの通知を待ち、SO_ERROR を確認するのが正しい手順です。複数回呼び出すと、特にDragonFly BSDのような一部のOSでは、カーネルが予測不能な非同期接続確立を試み、競合状態や誤ったエラー報告につながる可能性がありました。
  2. DragonFly BSDの SO_ERROR の重要性: DragonFly BSDでは、connectEINPROGRESS を返した後、ソケットが書き込み可能になったとしても、SO_ERROR を確認しないと実際の接続結果(成功か失敗か)が正確に把握できませんでした。以前のコードにはDragonFly BSDに特化した SO_ERROR チェックがありましたが、for ループの構造上、まだ問題が残っていました。
  3. Solarisの EINVAL の特殊処理: Solarisでは、特定の状況下で connectEINVAL を返すことがあり、これは実際には接続が成功していると見なすべきケースでした。この特殊なケースを適切に処理しないと、誤って接続失敗と判断されてしまう可能性がありました。

このコミットは、これらの問題を解決するために、connect の呼び出しロジックを根本的に変更し、一度の connect 呼び出しと、その後の SO_ERROR を使った厳密な状態確認に移行しました。

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

src/pkg/net/fd_unix.go(*netFD).connect メソッドが変更されました。

--- a/src/pkg/net/fd_unix.go
+++ b/src/pkg/net/fd_unix.go
@@ -75,51 +75,47 @@ func (fd *netFD) connect(la, ra syscall.Sockaddr) error {
 	if err := fd.pd.PrepareWrite(); err != nil {
 		return err
 	}
-	for {
-		err := syscall.Connect(fd.sysfd, ra)
-		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
+	switch err := syscall.Connect(fd.sysfd, ra); err {
+	case syscall.EINPROGRESS, syscall.EALREADY, syscall.EINTR:
+	case nil, syscall.EISCONN:
+		return nil
+	case syscall.EINVAL:
+		// 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" {
+			return nil
 		}
-
-		if err != syscall.EINPROGRESS && err != syscall.EALREADY && err != syscall.EINTR {
+		fallthrough
+	default:
+		return err
+	}
+	for {
+		// Performing multiple connect system calls on a
+		// non-blocking socket under Unix variants does not
+		// necessarily result in earlier errors being
+		// returned. Instead, once runtime-integrated network
+		// poller tells us that the socket is ready, get the
+		// SO_ERROR socket option to see if the connection
+		// succeeded or failed. See issue 7474 for further
+		// details.
+		if err := fd.pd.WaitWrite(); err != nil {
 			return err
 		}
-		if err = fd.pd.WaitWrite(); err != nil {
+		nerr, err := syscall.GetsockoptInt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_ERROR)
+		if err != nil {
 			return err
 		}
-
-		// Performing multiple connect system calls on a non-blocking
-		// socket under DragonFly BSD does not necessarily result in
-		// earlier errors being returned, particularly if we are
-		// connecting to localhost. Instead, once netpoll tells us that
-		// the socket is ready, get the SO_ERROR socket option to see
-		// if the connection succeeded or failed. See issue 7474 for
-		// further details. At some point we may want to consider
-		// doing the same on other Unixes.
-		if runtime.GOOS == "dragonfly" {
-			nerr, err := syscall.GetsockoptInt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_ERROR)
-			if err != nil {
-				return err
-			}
-			if nerr == 0 {
-				return nil
-			}
-			err = syscall.Errno(nerr)
-			if err != syscall.EINPROGRESS && err != syscall.EALREADY && err != syscall.EINTR {
+		switch err := syscall.Errno(nerr); err {
+		case syscall.EINPROGRESS, syscall.EALREADY, syscall.EINTR:
+		case syscall.Errno(0), syscall.EISCONN:
+			return nil
+		default:
 				return err
-			}
 		}
 	}
-	return nil
 }
 
 func (fd *netFD) destroy() {

主な変更点は以下の通りです。

  • syscall.Connect の呼び出しを囲んでいた外側の for ループが削除されました。
  • 最初の syscall.Connect の戻り値を処理するために switch ステートメントが導入されました。
  • syscall.EINVAL のSolaris特有の処理が、switch ステートメントの case syscall.EINVAL: ブロックに移動し、runtime.GOOS == "solaris" の場合に即座に nil を返すようになりました。
  • 以前のDragonFly BSDに特化した if runtime.GOOS == "dragonfly" ブロックが削除され、より一般的な SO_ERROR チェックのロジックに統合されました。
  • fd.pd.WaitWrite() の呼び出しと、その後の syscall.GetsockoptInt を使った SO_ERROR のチェックが、新しい for ループ内に配置されました。

コアとなるコードの解説

新しい (*netFD).connect メソッドのロジックは、以下のステップで動作します。

  1. 書き込み準備: fd.pd.PrepareWrite() を呼び出し、ソケットが書き込み操作の準備ができていることを確認します。
  2. 最初の connect 呼び出し:
    • syscall.Connect(fd.sysfd, ra) を一度だけ呼び出します。
    • この呼び出しの結果は switch ステートメントで処理されます。
    • nil または syscall.EISCONN: 接続が即座に確立されたか、すでに接続済みの場合、関数は nil(エラーなし)を返して終了します。
    • syscall.EINPROGRESS, syscall.EALREADY, syscall.EINTR: 接続がまだ進行中であるか、シグナルによって中断された場合、switch ステートメントは何もせず、次の for ループに処理を移します。これは、接続が非同期的に確立されるのを待つ必要があることを意味します。
    • syscall.EINVAL: Solarisの場合、ソケットがすでにサーバーによって受け入れられ、閉じられている可能性があるため、このエラーは成功と見なされ、関数は nil を返して終了します(Issue #6828の対応)。Solaris以外で EINVAL が発生した場合は、fallthroughdefault ケースに移行し、エラーとして返されます。
    • その他のエラー: 上記以外のエラーが発生した場合、関数は直ちにそのエラーを返して終了します。
  3. 接続完了の待機と SO_ERROR の確認:
    • 最初の connect 呼び出しが EINPROGRESS などで返された場合、コードは新しい for ループに入ります。
    • ループ内で、まず fd.pd.WaitWrite() を呼び出し、ソケットが書き込み可能になるまで待機します。これにより、接続処理が進んだことがポーラーによって通知されます。
    • ソケットが書き込み可能になったら、syscall.GetsockoptInt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_ERROR) を呼び出して、ソケットの SO_ERROR オプションの値を取得します。この値が、非同期接続試行の最終的な結果を示します。
    • 取得した nerrSO_ERROR の値)は syscall.Errno(nerr)error 型に変換され、再度 switch ステートメントで処理されます。
    • syscall.EINPROGRESS, syscall.EALREADY, syscall.EINTR: SO_ERROR がこれらの値の場合、接続はまだ進行中であるか、一時的な状態にあることを意味するため、ループは続行され、再度 WaitWrite() を待ちます。
    • syscall.Errno(0) または syscall.EISCONN: SO_ERROR0(エラーなし)であるか、ソケットがすでに接続済みであることを示す場合、接続は成功したと判断され、関数は nil を返して終了します。
    • その他のエラー: SO_ERROR が上記以外のエラー値を示した場合、接続は失敗したと判断され、関数はそのエラーを返して終了します。

この新しいロジックにより、syscall.Connect は一度だけ呼び出され、その後の接続状態の確認は SO_ERROR を通じて行われるため、複数回呼び出しによる予測不能な挙動が排除され、より堅牢でOSの挙動に依存しない接続確立処理が実現されました。

関連リンク

参考にした情報源リンク