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

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

このコミットは、Go言語のnetパッケージにおけるDragonFly BSD上での非ブロッキングコネクト処理のバグ修正に関するものです。具体的には、net/fd_unix.goファイルが変更され、非ブロッキングソケットでのconnectシステムコールが複数回実行された際の挙動が改善されています。

コミット

commit 734d4637c5925826904ffe7406cd411568928cb4
Author: Joel Sing <jsing@google.com>
Date:   Thu Mar 6 00:07:16 2014 +1100

    net: fix non-blocking connect handling on dragonfly
    
    Performing multiple connect system calls on a non-blocking socket
    under DragonFly BSD does not necessarily result in errors from earlier
    connect calls 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.
    
    Fixes #7474
    
    LGTM=mikioh.mikioh
    R=mikioh.mikioh
    CC=golang-codereviews
    https://golang.org/cl/69340044

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

https://github.com/golang/go/commit/734d4637c5925826904ffe7406cd411568928cb4

元コミット内容

このコミットの元の内容は、DragonFly BSD環境において、非ブロッキングソケットに対する複数回のconnectシステムコールが、以前のconnectコールからのエラーを適切に返さないという問題に対処するものです。特にlocalhostへの接続時にこの問題が顕著でした。修正は、netpollがソケットの準備ができたことを通知した後、SO_ERRORソケットオプションを取得して接続の成否を確認するというアプローチを取っています。

変更の背景

この変更の背景には、Go言語のネットワークスタックが様々なOSで一貫して動作するようにするための努力があります。特に、非ブロッキングI/Oは高性能なネットワークアプリケーションを構築する上で不可欠な要素ですが、OSごとにその挙動には微妙な違いが存在します。

DragonFly BSDは、FreeBSDからフォークしたUNIX系OSであり、独自のカーネル設計やシステムコール実装を持つことがあります。このコミットで修正された問題は、DragonFly BSDのconnectシステムコールが、非ブロッキングモードで複数回呼び出された際に、期待されるエラーコード(例えばEINPROGRESSEALREADY)を常に返さないというOS固有の挙動に起因しています。

Goのnetパッケージは、内部的にnetpollというメカニズムを使用して、非ブロッキングI/Oの準備ができたことを効率的に検出します。しかし、connectが完了したとnetpollが通知した後でも、実際の接続結果(成功または失敗)を正確に把握するためには、追加のチェックが必要となるケースがありました。特に、localhostへの接続は非常に高速に完了するため、OSがエラーを返す前に接続が確立されてしまうことがあり、これが問題を引き起こしていました。

この問題は、GoのIssue #7474として報告されており、このコミットはその解決策として実装されました。

前提知識の解説

1. 非ブロッキングI/Oとconnectシステムコール

  • ブロッキングI/O: 通常のソケット操作はブロッキングモードで動作します。これは、connectreadwriteなどのシステムコールが、操作が完了するまで(またはエラーが発生するまで)プログラムの実行を停止(ブロック)することを意味します。
  • 非ブロッキングI/O: 非ブロッキングモードでは、システムコールはすぐに制御を呼び出し元に返します。操作がすぐに完了しない場合、通常はEINPROGRESS(操作が進行中)やEWOULDBLOCK(操作がブロックされるだろう)といったエラーコードを返します。これにより、アプリケーションはI/O操作の完了を待つ間に他の処理を実行できます。
  • connectシステムコール: クライアントがサーバーへの接続を確立するために使用するシステムコールです。TCP接続の場合、3ウェイハンドシェイクが完了すると接続が確立されます。非ブロッキングモードでは、connectはすぐに戻り、接続がバックグラウンドで進行します。

2. netpoll

Go言語のランタイムは、効率的なネットワークI/Oのためにnetpollという抽象化レイヤーを使用しています。これは、OSが提供するI/O多重化メカニズム(Linuxのepoll、macOS/FreeBSDのkqueue、WindowsのI/O Completion Portsなど)をラップしたものです。netpollは、ソケットが読み書き可能になったり、接続が完了したりした際に、Goのゴルーチンをスケジュールするために使用されます。

3. SO_ERRORソケットオプション

ソケットには様々なオプションがあり、getsockoptシステムコールを使って取得したり、setsockoptシステムコールを使って設定したりできます。SO_ERRORは、ソケットに関連付けられた保留中のエラーを取得するために使用されるソケットオプションです。非ブロッキングconnectが完了した後、このオプションをチェックすることで、接続が成功したのか、それとも非同期的にエラーが発生したのかを判断できます。SO_ERRORが0であればエラーはなく、それ以外の値であれば対応するエラーコード(例: ECONNREFUSEDETIMEDOUTなど)を示します。

4. DragonFly BSDの特性

DragonFly BSDは、FreeBSD 4.xからフォークしたOSであり、独自の設計思想を持っています。特に、カーネルの同期メカニズムやVFS(Virtual File System)の設計に違いがあります。このコミットが示唆するように、ネットワークスタックの特定の挙動、特に非ブロッキングconnectのセマンティクスが他のUNIX系OS(LinuxやFreeBSD)と異なる場合がありました。

技術的詳細

このコミットの技術的詳細を掘り下げると、Goのnetパッケージがどのように非ブロッキングconnectを処理しているか、そしてDragonFly BSDの特定の挙動にどのように対応しているかが明らかになります。

Goのnetパッケージでは、fd_unix.goファイルがUNIX系OSにおけるファイルディスクリプタ(ソケットも含む)の低レベルな操作を扱っています。netFD構造体は、ネットワークファイルディスクリプタを表し、connectメソッドはそのソケットでの接続処理を担当します。

通常の非ブロッキングconnectのフローは以下のようになります。

  1. ソケットを非ブロッキングモードに設定する。
  2. connectシステムコールを呼び出す。
  3. connectEINPROGRESSを返した場合(接続がすぐに完了しない場合)、netpollにソケットが書き込み可能になるのを待つように登録する。
  4. netpollがソケットが書き込み可能になったことを通知したら、接続が完了したと判断する。

しかし、DragonFly BSDでは、この「接続が完了したと判断する」部分に問題がありました。コミットメッセージによると、「非ブロッキングソケットで複数回のconnectシステムコールを実行しても、以前のconnectコールからのエラーが必ずしも返されない」という問題がありました。これは、connectEINPROGRESSを返さずに、内部的に接続を試行し続けるか、あるいはlocalhostのような非常に高速な接続の場合に、netpollが準備完了を通知する前に、OSがエラー状態を適切に設定しない可能性を示唆しています。

この修正では、netpollがソケットの準備ができたと通知した後、追加のステップとしてsyscall.GetsockoptInt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_ERROR)を呼び出してSO_ERRORソケットオプションの値を取得しています。

  • fd.sysfd: ソケットのファイルディスクリプタ。
  • syscall.SOL_SOCKET: ソケットレベルのオプションを指定する。
  • syscall.SO_ERROR: 取得したいオプションが保留中のエラーコードであることを示す。

GetsockoptIntは、指定されたソケットオプションの整数値を取得します。取得したnerr0であれば、エラーは発生しておらず、接続は成功したと判断されます。nerr0以外の場合、その値はエラーコードを表すため、syscall.Errno(nerr)を使ってGoのエラー型に変換し、それを返します。

このチェックは、syscall.EINPROGRESSsyscall.EALREADYsyscall.EINTRといった、非ブロッキングconnectで通常期待される一時的なエラーコードではない場合にのみ、エラーとして返されます。これにより、OS固有の挙動によってconnectが直接エラーを返さなかった場合でも、SO_ERRORを通じて真の接続状態を正確に把握できるようになります。

この修正は、GoのネットワークスタックがOSの差異を吸収し、一貫した動作を提供するための重要な一例です。

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

変更は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
@@ -96,6 +96,28 @@ func (fd *netFD) connect(la, ra syscall.Sockaddr) error {
 		if err = fd.pd.WaitWrite(); 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 {
+				return err
+			}
+		}
 	}
 	return nil
 }

コアとなるコードの解説

追加されたコードブロックは、runtime.GOOS == "dragonfly"という条件分岐の中にあります。これは、この修正がDragonFly BSDに特化したものであることを示しています。

  1. if runtime.GOOS == "dragonfly" { ... }: Goのビルド時に設定される環境変数GOOSdragonflyである場合にのみ、このコードブロックが実行されます。これにより、他のOSでは既存の挙動が維持され、DragonFly BSD特有の問題にのみ対処します。

  2. nerr, err := syscall.GetsockoptInt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_ERROR): syscall.GetsockoptInt関数を呼び出し、ソケットのファイルディスクリプタfd.sysfdに対して、SOL_SOCKETレベルのSO_ERRORオプションの整数値を取得します。

    • nerr: 取得されたエラーコードの整数値。エラーがない場合は0
    • err: GetsockoptInt自体の呼び出しで発生したエラー。
  3. if err != nil { return err }: GetsockoptIntの呼び出し自体が失敗した場合、そのエラーを即座に返します。

  4. if nerr == 0 { return nil }: SO_ERROR0である場合、ソケットには保留中のエラーがないことを意味します。これは接続が成功したことを示唆するため、nil(エラーなし)を返してconnectメソッドを終了します。

  5. err = syscall.Errno(nerr): nerr0でない場合、それは実際のエラーコードを表します。syscall.Errno(nerr)を使って、その整数値をGoのsyscall.Errno型(エラーインターフェースを実装)に変換します。

  6. if err != syscall.EINPROGRESS && err != syscall.EALREADY && err != syscall.EINTR { return err }: 最後に、変換されたエラーが、非ブロッキングconnectで通常発生しうる一時的なエラー(EINPROGRESSEALREADYEINTR)ではないことを確認します。

    • EINPROGRESS: 接続がまだ進行中である。
    • EALREADY: 接続が既に進行中である(以前のconnect呼び出しがまだ完了していない)。
    • EINTR: システムコールがシグナルによって中断された。 これらのエラーは、接続がまだ完了していないか、一時的な状態であることを示すため、この場合はエラーとして返さずに、connectメソッドのループが継続することを期待します(元のコードのfd.pd.WaitWrite()が再度呼び出される)。 上記以外のエラー(例: ECONNREFUSEDETIMEDOUTなど)は、接続が実際に失敗したことを意味するため、そのエラーを返してconnectメソッドを終了します。

このコードは、DragonFly BSDの非ブロッキングconnectの挙動の特殊性を考慮し、SO_ERRORを明示的にチェックすることで、接続の最終的な状態を正確に判断し、Goのネットワークスタックが期待通りに動作するようにしています。

関連リンク

参考にした情報源リンク

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

このコミットは、Go言語のnetパッケージにおけるDragonFly BSD上での非ブロッキングコネクト処理のバグ修正に関するものです。具体的には、net/fd_unix.goファイルが変更され、非ブロッキングソケットでのconnectシステムコールが複数回実行された際の挙動が改善されています。

コミット

commit 734d4637c5925826904ffe7406cd411568928cb4
Author: Joel Sing <jsing@google.com>
Date:   Thu Mar 6 00:07:16 2014 +1100

    net: fix non-blocking connect handling on dragonfly
    
    Performing multiple connect system calls on a non-blocking socket
    under DragonFly BSD does not necessarily result in errors from earlier
    connect calls 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.
    
    Fixes #7474
    
    LGTM=mikioh.mikioh
    R=mikioh.mikioh
    CC=golang-codereviews
    https://golang.org/cl/69340044

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

https://github.com/golang/go/commit/734d4637c5925826904ffe7406cd411568928cb4

元コミット内容

このコミットの元の内容は、DragonFly BSD環境において、非ブロッキングソケットに対する複数回のconnectシステムコールが、以前のconnectコールからのエラーを適切に返さないという問題に対処するものです。特にlocalhostへの接続時にこの問題が顕著でした。修正は、netpollがソケットの準備ができたことを通知した後、SO_ERRORソケットオプションを取得して接続の成否を確認するというアプローチを取っています。

変更の背景

この変更の背景には、Go言語のネットワークスタックが様々なOSで一貫して動作するようにするための努力があります。特に、非ブロッキングI/Oは高性能なネットワークアプリケーションを構築する上で不可欠な要素ですが、OSごとにその挙動には微妙な違いが存在します。

DragonFly BSDは、FreeBSDからフォークしたUNIX系OSであり、独自のカーネル設計やシステムコール実装を持つことがあります。このコミットで修正された問題は、DragonFly BSDのconnectシステムコールが、非ブロッキングモードで複数回呼び出された際に、期待されるエラーコード(例えばEINPROGRESSEALREADY)を常に返さないというOS固有の挙動に起因しています。

Goのnetパッケージは、内部的にnetpollというメカニズムを使用して、非ブロッキングI/Oの準備ができたことを効率的に検出します。しかし、connectが完了したとnetpollが通知した後でも、実際の接続結果(成功または失敗)を正確に把握するためには、追加のチェックが必要となるケースがありました。特に、localhostへの接続は非常に高速に完了するため、OSがエラーを返す前に接続が確立されてしまうことがあり、これが問題を引き起こしていました。

この問題は、GoのIssue #7474として報告されており、このコミットはその解決策として実装されました。

前提知識の解説

1. 非ブロッキングI/Oとconnectシステムコール

  • ブロッキングI/O: 通常のソケット操作はブロッキングモードで動作します。これは、connectreadwriteなどのシステムコールが、操作が完了するまで(またはエラーが発生するまで)プログラムの実行を停止(ブロック)することを意味します。
  • 非ブロッキングI/O: 非ブロッキングモードでは、システムコールはすぐに制御を呼び出し元に返します。操作がすぐに完了しない場合、通常はEINPROGRESS(操作が進行中)やEWOULDBLOCK(操作がブロックされるだろう)といったエラーコードを返します。これにより、アプリケーションはI/O操作の完了を待つ間に他の処理を実行できます。
  • connectシステムコール: クライアントがサーバーへの接続を確立するために使用するシステムコールです。TCP接続の場合、3ウェイハンドシェイクが完了すると接続が確立されます。非ブロッキングモードでは、connectはすぐに戻り、接続がバックグラウンドで進行します。

2. netpoll

Go言語のランタイムは、効率的なネットワークI/Oのためにnetpollという抽象化レイヤーを使用しています。これは、OSが提供するI/O多重化メカニズム(Linuxのepoll、macOS/FreeBSDのkqueue、WindowsのI/O Completion Portsなど)をラップしたものです。netpollは、ソケットが読み書き可能になったり、接続が完了したりした際に、Goのゴルーチンをスケジュールするために使用されます。

3. SO_ERRORソケットオプション

ソケットには様々なオプションがあり、getsockoptシステムコールを使って取得したり、setsockoptシステムコールを使って設定したりできます。SO_ERRORは、ソケットに関連付けられた保留中のエラーを取得するために使用されるソケットオプションです。非ブロッキングconnectが完了した後、このオプションをチェックすることで、接続が成功したのか、それとも非同期的にエラーが発生したのかを判断できます。SO_ERRORが0であればエラーはなく、それ以外の値であれば対応するエラーコード(例: ECONNREFUSEDETIMEDOUTなど)を示します。

4. DragonFly BSDの特性

DragonFly BSDは、FreeBSD 4.xからフォークしたOSであり、独自の設計思想を持っています。特に、カーネルの同期メカニズムやVFS(Virtual File System)の設計に違いがあります。このコミットが示唆するように、ネットワークスタックの特定の挙動、特に非ブロッキングconnectのセマンティクスが他のUNIX系OS(LinuxやFreeBSD)と異なる場合がありました。Web検索の結果からも、DragonFly BSDにおける非ブロッキングconnectSO_ERRORの挙動は、他のBSD系OSと同様に標準的なものであることが確認できます。つまり、connectEINPROGRESSを返した後、select()poll()でソケットが書き込み可能になったことを待ち、その後getsockopt()SO_ERRORをチェックするという流れが一般的です。このコミットは、Goのnetパッケージがこの標準的な挙動を正しく解釈できていなかった、あるいは特定の条件下で問題が発生していたことを示唆しています。

技術的詳細

このコミットの技術的詳細を掘り下げると、Goのnetパッケージがどのように非ブロッキングconnectを処理しているか、そしてDragonFly BSDの特定の挙動にどのように対応しているかが明らかになります。

Goのnetパッケージでは、fd_unix.goファイルがUNIX系OSにおけるファイルディスクリプタ(ソケットも含む)の低レベルな操作を扱っています。netFD構造体は、ネットワークファイルディスクリプタを表し、connectメソッドはそのソケットでの接続処理を担当します。

通常の非ブロッキングconnectのフローは以下のようになります。

  1. ソケットを非ブロッキングモードに設定する。
  2. connectシステムコールを呼び出す。
  3. connectEINPROGRESSを返した場合(接続がすぐに完了しない場合)、netpollにソケットが書き込み可能になるのを待つように登録する。
  4. netpollがソケットが書き込み可能になったことを通知したら、接続が完了したと判断する。

しかし、DragonFly BSDでは、この「接続が完了したと判断する」部分に問題がありました。コミットメッセージによると、「非ブロッキングソケットで複数回のconnectシステムコールを実行しても、以前のconnectコールからのエラーが必ずしも返されない」という問題がありました。これは、connectEINPROGRESSを返さずに、内部的に接続を試行し続けるか、あるいはlocalhostのような非常に高速な接続の場合に、OSがエラー状態を適切に設定しない可能性を示唆しています。

この修正では、netpollがソケットの準備ができたと通知した後、追加のステップとしてsyscall.GetsockoptInt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_ERROR)を呼び出してSO_ERRORソケットオプションの値を取得しています。

  • fd.sysfd: ソケットのファイルディスクリプタ。
  • syscall.SOL_SOCKET: ソケットレベルのオプションを指定する。
  • syscall.SO_ERROR: 取得したいオプションが保留中のエラーコードであることを示す。

GetsockoptIntは、指定されたソケットオプションの整数値を取得します。取得したnerr0であれば、エラーは発生しておらず、接続は成功したと判断されます。nerr0以外の場合、その値はエラーコードを表すため、syscall.Errno(nerr)を使ってGoのエラー型に変換し、それを返します。

このチェックは、syscall.EINPROGRESSsyscall.EALREADYsyscall.EINTRといった、非ブロッキングconnectで通常期待される一時的なエラーコードではない場合にのみ、エラーとして返されます。これにより、OS固有の挙動によってconnectが直接エラーを返さなかった場合でも、SO_ERRORを通じて真の接続状態を正確に把握できるようになります。

この修正は、GoのネットワークスタックがOSの差異を吸収し、一貫した動作を提供するための重要な一例です。

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

変更は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
@@ -96,6 +96,28 @@ func (fd *netFD) connect(la, ra syscall.Sockaddr) error {
 		if err = fd.pd.WaitWrite(); 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 {
+				return err
+			}
+		}
 	}
 	return nil
 }

コアとなるコードの解説

追加されたコードブロックは、runtime.GOOS == "dragonfly"という条件分岐の中にあります。これは、この修正がDragonFly BSDに特化したものであることを示しています。

  1. if runtime.GOOS == "dragonfly" { ... }: Goのビルド時に設定される環境変数GOOSdragonflyである場合にのみ、このコードブロックが実行されます。これにより、他のOSでは既存の挙動が維持され、DragonFly BSD特有の問題にのみ対処します。

  2. nerr, err := syscall.GetsockoptInt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_ERROR): syscall.GetsockoptInt関数を呼び出し、ソケットのファイルディスクリプタfd.sysfdに対して、SOL_SOCKETレベルのSO_ERRORオプションの整数値を取得します。

    • nerr: 取得されたエラーコードの整数値。エラーがない場合は0
    • err: GetsockoptInt自体の呼び出しで発生したエラー。
  3. if err != nil { return err }: GetsockoptIntの呼び出し自体が失敗した場合、そのエラーを即座に返します。

  4. if nerr == 0 { return nil }: SO_ERROR0である場合、ソケットには保留中のエラーがないことを意味します。これは接続が成功したことを示唆するため、nil(エラーなし)を返してconnectメソッドを終了します。

  5. err = syscall.Errno(nerr): nerr0でない場合、それは実際のエラーコードを表します。syscall.Errno(nerr)を使って、その整数値をGoのsyscall.Errno型(エラーインターフェースを実装)に変換します。

  6. if err != syscall.EINPROGRESS && err != syscall.EALREADY && err != syscall.EINTR { return err }: 最後に、変換されたエラーが、非ブロッキングconnectで通常発生しうる一時的なエラー(EINPROGRESSEALREADYEINTR)ではないことを確認します。

    • EINPROGRESS: 接続がまだ進行中である。
    • EALREADY: 接続が既に進行中である(以前のconnect呼び出しがまだ完了していない)。
    • EINTR: システムコールがシグナルによって中断された。 これらのエラーは、接続がまだ完了していないか、一時的な状態であることを示すため、この場合はエラーとして返さずに、connectメソッドのループが継続することを期待します(元のコードのfd.pd.WaitWrite()が再度呼び出される)。 上記以外のエラー(例: ECONNREFUSEDETIMEDOUTなど)は、接続が実際に失敗したことを意味するため、そのエラーを返してconnectメソッドを終了します。

このコードは、DragonFly BSDの非ブロッキングconnectの挙動の特殊性を考慮し、SO_ERRORを明示的にチェックすることで、接続の最終的な状態を正確に判断し、Goのネットワークスタックが期待通りに動作するようにしています。

関連リンク

  • Go Issue #7474: コミットメッセージに記載されていますが、現在のGitHubリポジトリでは直接この番号のIssueは見つかりませんでした。これは、GoのIssueトラッカーが過去に移行したため、古いIssueが異なるシステムに存在するか、アーカイブされている可能性があります。
  • Go CL 69340044: https://golang.org/cl/69340044 (Gerrit Code Review)

参考にした情報源リンク