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

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

このコミットは、Go言語の net パッケージにおける connect() システムコールの挙動を、新しいネットワークポーラー(network poller)の導入に備えて修正するものです。特に、新しいポーラーが誤った準備完了通知(spurious readiness notifications)を発する可能性がある問題に対処し、ソケットが実際に接続されていることを保証するための変更が加えられています。

コミット

  • コミットハッシュ: a11d7d4e11207be9186c6dbeda11fedfef3cbe4d
  • Author: Dmitriy Vyukov dvyukov@google.com
  • Date: Thu Mar 14 10:32:42 2013 +0400
  • コミットメッセージ:
    net: prepare connect() for new network poller
    The problem is that new network poller can have spurious
    rediness notifications. This implementation ensures that
    the socket is actually connected.
    
    R=golang-dev, rsc, akumar
    CC=golang-dev
    https://golang.org/cl/7785043
    

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

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

元コミット内容

net: prepare connect() for new network poller
The problem is that new network poller can have spurious
rediness notifications. This implementation ensures that
the socket is actually connected.

R=golang-dev, rsc, akumar
CC=golang-dev
https://golang.org/cl/7785043

変更の背景

この変更の背景には、Go言語のネットワークI/O処理における重要な進化があります。当時のGoのネットワークポーラーは、ソケットの準備完了状態を効率的に監視するためのメカニズムでしたが、新しいポーラーの設計では「spurious readiness notifications」(誤った準備完了通知)が発生する可能性がありました。

connect() システムコールは、非ブロッキングモードで呼び出された場合、即座に接続が確立されずに EINPROGRESS エラーを返すことがあります。この場合、アプリケーションはソケットが書き込み可能になるのを待つ必要があります。ソケットが書き込み可能になったとき、それは接続が確立されたか、またはエラーが発生したことを意味します。

従来のポーラーでは、connect()EINPROGRESS を返した後、WaitWrite() で書き込み可能になるのを待ち、その後 GetsockoptInt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_ERROR) を呼び出して実際の接続結果(エラーコード)を取得していました。

しかし、新しいネットワークポーラーでは、ソケットが実際に接続されていないにもかかわらず、ポーラーが誤って「書き込み可能」という通知を出す可能性がありました。このような誤った通知を受け取ると、Goのランタイムはソケットが接続されたと誤解し、後続の操作で予期せぬエラーやデッドロックを引き起こす可能性がありました。

このコミットは、この「誤った準備完了通知」の問題に対処し、connect() が実際に成功したか、または適切なエラーで終了するまで、ポーラーからの通知を待機し続けるロバストなメカニズムを導入することを目的としています。これにより、Goのネットワークスタックの信頼性と堅牢性が向上します。

前提知識の解説

このコミットを理解するためには、以下の概念を把握しておく必要があります。

  1. connect() システムコール:

    • ソケットをリモートアドレスに接続するために使用されるシステムコールです。
    • ブロッキングモードでは、接続が確立されるまで呼び出し元をブロックします。
    • 非ブロッキングモードでは、接続が即座に確立されない場合、EINPROGRESS エラーを返してすぐに戻ります。この場合、接続の完了は非同期的に行われます。
  2. 非ブロッキングI/Oとポーリング:

    • 非ブロッキングI/Oでは、I/O操作が即座に完了しない場合でも、システムコールはブロックせずにエラー(例: EAGAIN, EINPROGRESS)を返します。
    • アプリケーションは、select(), poll(), epoll(), kqueue() などのポーリングメカニズムを使用して、ソケットが読み書き可能になったり、接続が完了したりするのを待ちます。Goのネットワークポーラーは、これらのOS固有のメカニズムを抽象化したものです。
  3. syscall.EINPROGRESS:

    • connect() が非ブロッキングソケットに対して呼び出され、接続がまだ確立されていないことを示すエラーコードです。接続はバックグラウンドで進行中です。
  4. syscall.EALREADY:

    • 非ブロッキングソケットに対して connect() が既に進行中であることを示すエラーコードです。通常、EINPROGRESS の後に再度 connect() を呼び出した場合に発生することがあります。
  5. syscall.EINTR:

    • システムコールがシグナルによって中断されたことを示すエラーコードです。システムコールは再試行されるべきです。
  6. syscall.EISCONN:

    • ソケットが既に接続されていることを示すエラーコードです。connect() が既に接続済みのソケットに対して呼び出された場合に返されることがあります。
  7. syscall.GetsockoptInt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_ERROR):

    • ソケットのオプションを取得するためのシステムコール getsockopt() のGoラッパーです。
    • SOL_SOCKET はソケットレベルのオプションを指定します。
    • SO_ERROR は、ソケットに保留中のエラーがある場合にそのエラーコードを取得するために使用されます。非ブロッキング connect() が完了した後、このオプションをチェックすることで、接続が成功したか、またはエラーで失敗したかを確認できます。
  8. Goのネットワークポーラー:

    • Goランタイムは、効率的な非同期ネットワークI/Oを実現するために内部的なネットワークポーラーを使用しています。これは、OSのI/O多重化メカニズム(epoll, kqueueなど)を抽象化し、GoルーチンがブロッキングI/O操作を実行しているかのように見せかけながら、実際には非同期でI/Oを処理します。
    • 「spurious readiness notifications」は、ポーラーがソケットが準備完了であると誤って報告する状況を指します。これは、ポーラーの実装上のバグや、特定のOSの挙動によって発生する可能性があります。

技術的詳細

このコミットの主要な変更は、src/pkg/net/fd_unix.go ファイル内の netFD.connect() メソッドにあります。

変更前: 変更前のコードは、非ブロッキング connect()EINPROGRESS を返した場合、fd.pd.WaitWrite() を呼び出してソケットが書き込み可能になるのを待ちます。その後、syscall.GetsockoptInt を使用して SO_ERROR を取得し、実際の接続結果を確認していました。

	err := syscall.Connect(fd.sysfd, ra)
	if err == syscall.EINPROGRESS {
		if err = fd.pd.WaitWrite(); err != nil {
			return err
		}
		var e int
		e, err = syscall.GetsockoptInt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_ERROR)
		if err != nil {
			return os.NewSyscallError("getsockopt", err)
		}
		if e != 0 {
			err = syscall.Errno(e)
		}
	}
	return err

変更後: 変更後のコードは、connect() の呼び出しを for ループで囲み、より堅牢な接続完了チェックメカニズムを導入しています。

	for {
		err := syscall.Connect(fd.sysfd, ra)
		if err == nil || err == syscall.EISCONN {
			break // 接続成功または既に接続済み
		}
		// EINPROGRESS, EALREADY, EINTR 以外のエラーは即座に返す
		if err != syscall.EINPROGRESS && err != syscall.EALREADY && err != syscall.EINTR {
			return err
		}
		// EINPROGRESS, EALREADY, EINTR の場合は書き込み可能になるのを待つ
		if err = fd.pd.WaitWrite(); err != nil {
			return err
		}
	}
	return nil // 最終的に接続成功

この変更のポイントは以下の通りです。

  1. for ループの導入: connect() の呼び出しが for ループ内に移動されました。これにより、connect()EINPROGRESSEALREADYEINTR を返した場合でも、ポーラーが書き込み可能通知を出すたびに connect() を再試行できるようになります。
  2. 即時成功/既に接続済みチェック: if err == nil || err == syscall.EISCONN の条件が追加されました。connect() がエラーなしで成功した場合(err == nil)またはソケットが既に接続済みである場合(err == syscall.EISCONN)は、ループを抜けて接続が完了したと判断します。
  3. 特定のエラーコードの処理:
    • syscall.EINPROGRESS: 接続が進行中であることを示します。この場合、fd.pd.WaitWrite() でソケットが書き込み可能になるのを待ち、ループの次のイテレーションで connect() を再試行します。
    • syscall.EALREADY: 接続が既に進行中であることを示します。これも EINPROGRESS と同様に処理され、WaitWrite() を待って再試行します。
    • syscall.EINTR: システムコールがシグナルによって中断されたことを示します。この場合も WaitWrite() を待って再試行します。
  4. その他のエラーの即時返却: 上記の特定のエラーコード(nil, EISCONN, EINPROGRESS, EALREADY, EINTR)以外の場合、それは真の接続エラーであると判断し、即座にそのエラーを返します。
  5. GetsockoptInt(SO_ERROR) の削除: 変更後のコードでは、GetsockoptInt(SO_ERROR) を使用して接続結果を確認するロジックが削除されました。代わりに、connect() システムコール自体が成功する(err == nil または err == syscall.EISCONN)までループを続けることで、ソケットが実際に接続されたことを保証します。これは、新しいネットワークポーラーの「spurious readiness notifications」問題への直接的な対処です。ポーラーが誤って「書き込み可能」と通知しても、connect() が実際に成功するまでループが続くため、誤った状態での処理を防ぐことができます。

この変更により、Goのネットワークスタックは、非ブロッキング connect() の完了をより堅牢に処理できるようになり、特に新しいネットワークポーラーの挙動に対応できるようになりました。

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

--- a/src/pkg/net/fd_unix.go
+++ b/src/pkg/net/fd_unix.go
@@ -86,21 +86,19 @@ func (fd *netFD) connect(ra syscall.Sockaddr) error {
 	if err := fd.pd.PrepareWrite(); err != nil {
 		return err
 	}
-	err := syscall.Connect(fd.sysfd, ra)
-	if err == syscall.EINPROGRESS {
-		if err = fd.pd.WaitWrite(); err != nil {
-			return err
+	for {
+		err := syscall.Connect(fd.sysfd, ra)
+		if err == nil || err == syscall.EISCONN {
+			break
 		}
-		var e int
-		e, err = syscall.GetsockoptInt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_ERROR)
-		if err != nil {
-			return os.NewSyscallError("getsockopt", err)
+		if err != syscall.EINPROGRESS && err != syscall.EALREADY && err != syscall.EINTR {
+			return err
 		}
-		if e != 0 {
-			err = syscall.Errno(e)
+		if err = fd.pd.WaitWrite(); err != nil {
+			return err
 		}
 	}
-	return err
+	return nil
 }
 
 // Add a reference to this fd.

コアとなるコードの解説

fd_unix.go は、Unix系システムにおけるファイルディスクリプタ(fd)の操作、特にネットワーク関連のI/Oプリミティブを扱うGoの内部パッケージです。netFD 構造体は、ネットワーク接続を表すファイルディスクリプタとその関連状態をカプセル化しています。

変更された connect メソッドは、netFD 型のレシーバーを持つメソッドで、ソケットをリモートアドレス ra に接続する役割を担います。

  1. if err := fd.pd.PrepareWrite(); err != nil { return err }

    • これは、Goの内部的なポーラー(fd.pdpollDesc 型のインスタンス)に対して、ソケットが書き込み操作の準備をしていることを通知します。これにより、ポーラーはソケットの書き込み準備完了イベントを監視し始めます。エラーが発生した場合は即座に返します。
  2. for { ... }

    • ここが変更の核心です。connect() システムコールの呼び出しが無限ループの中に配置されました。これは、connect() が非同期的に完了する可能性があるため、実際に接続が確立されるまで、または致命的なエラーが発生するまで繰り返し試行するためです。
  3. err := syscall.Connect(fd.sysfd, ra)

    • 実際の connect() システムコールを呼び出します。fd.sysfd はソケットのファイルディスクリプタ、ra は接続先のアドレスです。
  4. if err == nil || err == syscall.EISCONN { break }

    • connect() がエラーなしで成功した場合(err == nil)、またはソケットが既に接続済みである場合(err == syscall.EISCONN)、接続は完了したと判断し、ループを抜けます。これは、接続が即座に確立された場合や、以前の試行で既に接続が完了していた場合に発生します。
  5. if err != syscall.EINPROGRESS && err != syscall.EALREADY && err != syscall.EINTR { return err }

    • この条件は、connect() が返したエラーが、接続が進行中であることを示す EINPROGRESS、既に進行中であることを示す EALREADY、またはシグナルによって中断されたことを示す EINTR のいずれでもない場合に真となります。
    • これらのエラーは、接続が失敗したことを示す致命的なエラーであるため、即座にそのエラーを呼び出し元に返します。
  6. if err = fd.pd.WaitWrite(); err != nil { return err }

    • connect()EINPROGRESS, EALREADY, EINTR のいずれかを返した場合、この行が実行されます。
    • fd.pd.WaitWrite() は、Goのポーラーに対して、ソケットが書き込み可能になるまで現在のGoルーチンをブロックするよう指示します。ポーラーはOSのI/O多重化メカニズムを使用して、ソケットの準備完了イベントを待ちます。
    • WaitWrite() がエラーを返した場合(例: ポーラーが閉じられた、タイムアウトなど)、そのエラーを返します。
    • WaitWrite() が成功して戻った場合、ソケットは書き込み可能になったと判断され、ループの次のイテレーションで connect() が再試行されます。これにより、ポーラーの「spurious readiness notifications」があったとしても、実際に connect() が成功するまでループが繰り返されるため、堅牢性が確保されます。
  7. return nil

    • ループを抜けた場合(つまり、connect()nil または syscall.EISCONN を返した場合)、最終的に接続は成功したと判断し、nil(エラーなし)を返します。

この変更により、Goのネットワーク接続確立ロジックは、非同期 connect() の挙動と新しいネットワークポーラーの特性により適切に対応できるようになり、より信頼性の高い接続処理が実現されています。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (src/pkg/net/fd_unix.go)
  • Unix/Linux man pages for connect(2), getsockopt(2)
  • Goのネットワークポーラーに関する一般的な知識
  • 非ブロッキングソケットI/Oに関する一般的な知識
  • syscall パッケージのドキュメント
  • GoのIssueトラッカーやメーリングリストでの関連議論 (具体的なリンクはコミットメッセージに記載されているCLのみ)