[インデックス 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)におけるその特殊な振る舞いがあります。
- 非ブロッキング
connect
の問題: 通常、非ブロッキングソケットでconnect
を呼び出すと、接続が即座に確立できない場合、EINPROGRESS
またはEALREADY
エラーを返します。この場合、アプリケーションはソケットが書き込み可能になるのを待ってから、getsockopt
とSO_ERROR
を使って最終的な接続ステータスを確認する必要があります。しかし、このconnect
を複数回呼び出すと、特に一部のOSでは予測不能な挙動を引き起こす可能性がありました。 - DragonFly BSDの特殊な挙動 (Issue #7474): DragonFly BSDでは、非ブロッキングソケットに対して
connect
を複数回呼び出すと、以前のエラーが必ずしも返されず、特にlocalhost
への接続時に問題が発生することが報告されていました。netpoll
がソケットの準備ができたことを通知した後でも、SO_ERROR
を確認しないと真の接続状態が把握できないという問題がありました。以前のコードでは、DragonFly BSDに特化した処理がありましたが、それでも不十分でした。 - Solarisの
EINVAL
問題 (Issue #6828): Solarisでは、ソケットがすでにサーバーによって受け入れられ、閉じられている場合でも、connect
がEINVAL
エラーを返すことがありました。この場合、Goランタイムはこれを成功した接続として扱う必要がありました。なぜなら、そのソケットへの書き込みはすぐにEOF(End Of File)となるため、実質的には接続が確立されたと見なせるからです。 - 以前の修正の不完全さ: コミットメッセージにあるように、以前の修正(CL 69340044)では、これらの問題が完全に解決されておらず、
connect
の複数回呼び出しの可能性が残っていました。このコミットは、その残された可能性を排除し、より堅牢な接続確立ロジックを提供します。 - 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ランタイムのネットワークポーラー(pd
、poll.FD
)と連携して非同期I/Oを管理します。fd.pd.PrepareWrite()
/fd.pd.WaitWrite()
: Goのネットワークポーラー(poll.FD
)のメソッドです。PrepareWrite()
は書き込み操作の準備をし、WaitWrite()
はソケットが書き込み可能になるまで待機します。これにより、非ブロッキングI/Oを効率的に処理できます。
技術的詳細
このコミットが解決しようとしている主要な技術的問題は、非ブロッキングソケットにおける connect
システムコールのセマンティクスと、異なるUnix系OS間でのその挙動の差異をGoランタイムがどのように統一的に扱うかという点です。
従来のコードでは、syscall.Connect
をループ内で複数回呼び出す可能性がありました。これは、EINPROGRESS
や EALREADY
が返された場合に、ポーラーがソケットの準備ができたと通知するまでループを続けるというアプローチでした。しかし、このアプローチには以下の問題がありました。
- 複数回
connect
呼び出しの危険性:connect
は一度だけ呼び出すべきシステムコールです。非ブロッキングソケットの場合、最初の呼び出しでEINPROGRESS
が返されたら、その後はポーラーの通知を待ち、SO_ERROR
を確認するのが正しい手順です。複数回呼び出すと、特にDragonFly BSDのような一部のOSでは、カーネルが予測不能な非同期接続確立を試み、競合状態や誤ったエラー報告につながる可能性がありました。 - DragonFly BSDの
SO_ERROR
の重要性: DragonFly BSDでは、connect
がEINPROGRESS
を返した後、ソケットが書き込み可能になったとしても、SO_ERROR
を確認しないと実際の接続結果(成功か失敗か)が正確に把握できませんでした。以前のコードにはDragonFly BSDに特化したSO_ERROR
チェックがありましたが、for
ループの構造上、まだ問題が残っていました。 - Solarisの
EINVAL
の特殊処理: Solarisでは、特定の状況下でconnect
がEINVAL
を返すことがあり、これは実際には接続が成功していると見なすべきケースでした。この特殊なケースを適切に処理しないと、誤って接続失敗と判断されてしまう可能性がありました。
このコミットは、これらの問題を解決するために、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
メソッドのロジックは、以下のステップで動作します。
- 書き込み準備:
fd.pd.PrepareWrite()
を呼び出し、ソケットが書き込み操作の準備ができていることを確認します。 - 最初の
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
が発生した場合は、fallthrough
でdefault
ケースに移行し、エラーとして返されます。- その他のエラー: 上記以外のエラーが発生した場合、関数は直ちにそのエラーを返して終了します。
- 接続完了の待機と
SO_ERROR
の確認:- 最初の
connect
呼び出しがEINPROGRESS
などで返された場合、コードは新しいfor
ループに入ります。 - ループ内で、まず
fd.pd.WaitWrite()
を呼び出し、ソケットが書き込み可能になるまで待機します。これにより、接続処理が進んだことがポーラーによって通知されます。 - ソケットが書き込み可能になったら、
syscall.GetsockoptInt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_ERROR)
を呼び出して、ソケットのSO_ERROR
オプションの値を取得します。この値が、非同期接続試行の最終的な結果を示します。 - 取得した
nerr
(SO_ERROR
の値)はsyscall.Errno(nerr)
でerror
型に変換され、再度switch
ステートメントで処理されます。 syscall.EINPROGRESS
,syscall.EALREADY
,syscall.EINTR
:SO_ERROR
がこれらの値の場合、接続はまだ進行中であるか、一時的な状態にあることを意味するため、ループは続行され、再度WaitWrite()
を待ちます。syscall.Errno(0)
またはsyscall.EISCONN
:SO_ERROR
が0
(エラーなし)であるか、ソケットがすでに接続済みであることを示す場合、接続は成功したと判断され、関数はnil
を返して終了します。- その他のエラー:
SO_ERROR
が上記以外のエラー値を示した場合、接続は失敗したと判断され、関数はそのエラーを返して終了します。
- 最初の
この新しいロジックにより、syscall.Connect
は一度だけ呼び出され、その後の接続状態の確認は SO_ERROR
を通じて行われるため、複数回呼び出しによる予測不能な挙動が排除され、より堅牢でOSの挙動に依存しない接続確立処理が実現されました。
関連リンク
- Go Issue 7541: https://golang.org/issue/7541
- Go Issue 7474: https://golang.org/issue/7474
- Go Issue 6828: https://golang.org/issue/6828
- Gerrit Change-ID 75930043: https://golang.org/cl/75930043
参考にした情報源リンク
connect(2)
man page: https://man7.org/linux/man-pages/man2/connect.2.htmlgetsockopt(2)
man page: https://man7.org/linux/man-pages/man2/getsockopt.2.htmlsocket(7)
man page (forSO_ERROR
): https://man7.org/linux/man-pages/man7/socket.7.html- Go
net
package source code (relevant files likefd_unix.go
,poll.go
): https://github.com/golang/go/tree/master/src/net - Go
syscall
package source code: https://github.com/golang/go/tree/master/src/syscall