[インデックス 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
netpackage source code (relevant files likefd_unix.go,poll.go): https://github.com/golang/go/tree/master/src/net - Go
syscallpackage source code: https://github.com/golang/go/tree/master/src/syscall