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

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

このドキュメントは、Go言語のネットワークパッケージにおけるコミット 28b599425dd535539f9001d42ec4dd4d472f3195 について、その技術的な詳細と背景を包括的に解説します。

コミット

commit 28b599425dd535539f9001d42ec4dd4d472f3195
Author: Dave Cheney <dave@cheney.net>
Date:   Wed Nov 28 11:29:25 2012 +1100

    net: move deadline logic into pollServer
    
    Update #4434.
    
    The proposal attempts to reduce the number of places where fd,{r,w}deadline is checked and updated in preparation for issue 4434. In doing so the deadline logic is simplified by letting the pollster return errTimeout from netFD.Wait{Read,Write} as part of the wakeup logic.
    
    The behaviour of setting n = 0 has been restored to match rev 2a55e349097f, which was the previous change to fd_unix.go before CL 6851096.
    
    R=jsing, bradfitz, mikioh.mikioh, rsc
    CC=fullung, golang-dev
    https://golang.org/cl/6850110

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

https://github.com/golang/go/commit/28b599425dd535539f9001d42ec4dd4d472f3195

元コミット内容

このコミットは、Go言語の net パッケージにおけるデッドライン(タイムアウト)処理のロジックを pollServer に移動し、簡素化することを目的としています。具体的には、fd.{r,w}deadline のチェックと更新が行われる箇所を減らし、ポーリングメカニズムが netFD.Wait{Read,Write} から errTimeout を返すことで、デッドラインの期限切れを通知するように変更されています。

また、n = 0 の挙動がリビジョン 2a55e349097f(CL 6851096の前の fd_unix.go への変更)と一致するように復元されています。これは、以前の変更によって意図しない挙動が発生していた可能性を示唆しています。

この変更は、Issue #4434 に関連する準備作業の一環として行われました。

変更の背景

Go言語のネットワーク操作では、読み書きのタイムアウト(デッドライン)を設定することが可能です。これは、ネットワークの応答がない場合にアプリケーションが無限にブロックされるのを防ぐために重要です。しかし、デッドラインの管理ロジックが複数の場所に散らばっていると、コードの複雑性が増し、バグの温床となる可能性があります。

このコミットの背景には、デッドライン処理の一元化と簡素化という明確な目的があります。特に、fd.{r,w}deadline のチェックと更新が多くの場所で行われている現状を改善し、デッドラインの期限切れをポーリングメカニズム(pollServer)が直接 errTimeout として通知するようにすることで、コードの可読性と保守性を向上させようとしています。

また、n = 0 の挙動の復元は、以前の変更が特定のシナリオで予期せぬ結果を引き起こしていた可能性を示唆しています。これは、ネットワークI/Oにおけるバイト数の読み込みとエラーハンドリングの正確性が、Goのネットワークパッケージの堅牢性にとって非常に重要であることを示しています。

Issue #4434 の具体的な内容は不明ですが、このコミットがその解決に向けた重要なステップであることから、ネットワークデッドライン処理に関する既存の問題や改善点が議論されていたと推測されます。

前提知識の解説

このコミットを理解するためには、以下のGo言語のネットワークプログラミングにおける概念とUnixシステムコールに関する知識が必要です。

  • netFD: Go言語の net パッケージ内部で使用されるファイルディスクリプタ(File Descriptor)の抽象化です。ネットワーク接続(ソケット)を表し、読み書き操作やデッドラインの設定など、低レベルのネットワークI/Oを管理します。
  • pollServer: GoのランタイムがネットワークI/Oの多重化(multiplexing)と非同期処理を効率的に行うためのメカニズムです。Unix系システムでは epoll (Linux), kqueue (FreeBSD/macOS), poll (Solaris) などのシステムコールを利用して、複数のファイルディスクリプタからのイベント(読み込み可能、書き込み可能など)を監視します。pollServer は、デッドラインが設定されたI/O操作のタイムアウトを監視し、期限切れになった場合に netFD をウェイクアップする役割を担います。
  • デッドライン(Deadlines): ネットワーク接続における読み書き操作の最大待機時間を設定する機能です。SetReadDeadlineSetWriteDeadline メソッドで設定され、指定された時間内に操作が完了しない場合、タイムアウトエラーが発生します。
  • syscall.EAGAIN: Unix系システムコールが非ブロッキングモードで実行された際に、操作が即座に完了せず、後で再試行する必要があることを示すエラーコードです。ネットワークI/Oでは、データがまだ利用可能でない場合や、バッファが満杯で書き込みができない場合などに返されます。
  • io.EOF: Go言語の io パッケージで定義されているエラーで、入力の終わりに達したことを示します。ネットワーク接続においては、リモートエンドが接続を正常にクローズした場合に、読み込み操作が0バイトを返し、かつエラーが nil の場合に io.EOF として扱われることがあります。
  • syscall.SOCK_DGRAMsyscall.SOCK_RAW: ソケットの種類を表す定数です。
    • SOCK_DGRAM: データグラムソケット(UDPなど)を表します。メッセージの境界が維持され、信頼性や順序性は保証されません。
    • SOCK_RAW: 生ソケットを表します。IPヘッダやその他の低レベルなプロトコルヘッダを直接操作できます。
  • netFD.WaitRead / netFD.WaitWrite: pollServer を介して、netFD が読み込み可能または書き込み可能になるまで待機する内部関数です。これらの関数は、I/O操作がブロックされる可能性がある場合に呼び出され、pollServer がイベントを通知するまでゴルーチンをサスペンドします。

技術的詳細

このコミットの主要な技術的変更点は、ネットワークI/Oにおけるデッドライン処理の責任を netFD の各I/Oメソッドから pollServer へと集約したことです。

以前のコードでは、Read, ReadFrom, ReadMsg, Write, WriteTo, WriteMsg, accept, connect といった netFD のI/Oメソッド内で、fd.rdeadlinefd.wdeadline といったデッドラインの値を直接チェックし、タイムアウトエラー (errTimeout) を生成していました。また、pollServer.DelFD が呼び出された後に fd.rdeadline = -1fd.wdeadline = -1 といったデッドラインのリセットも行われていました。

このコミットでは、以下の変更が行われています。

  1. pollServer.WakeFD での errTimeout の直接通知: pollServerCheckDeadlines メソッド内で、デッドラインが期限切れになった際に s.WakeFD(fd, mode, errTimeout) を呼び出すように変更されました。これにより、pollServer がデッドラインの期限切れを検知し、直接 errTimeoutnetFD.Wait{Read,Write} に伝えることができるようになりました。 以前は s.WakeFD(fd, mode, nil) を呼び出し、その後 netFD のI/Oメソッド内で fd.rdeadline < 0 のチェックなどを行って errTimeout を生成していました。この変更により、デッドライン処理のロジックが pollServer 側に集約され、各I/Oメソッドからデッドライン関連の冗長なチェックが削除されました。

  2. netFD のI/Oメソッドからのデッドラインチェックの削除: netFD.connect, netFD.Read, netFD.ReadFrom, netFD.ReadMsg, netFD.Write, netFD.WriteTo, netFD.WriteMsg, netFD.accept の各メソッドから、fd.rdeadlinefd.wdeadline の値に基づくタイムアウトチェック(例: if hadTimeout && fd.wdeadline < 0 { return errTimeout })が削除されました。これにより、これらのメソッドは pollServer.Wait{Read,Write} が返すエラーをそのまま利用するようになり、デッドライン処理の責任が pollServer に移譲されました。

  3. chkReadErr ヘルパー関数の導入: 読み込み操作 (Read, ReadFrom, ReadMsg) のエラーハンドリングを共通化するために、chkReadErr という新しいヘルパー関数が導入されました。この関数は、読み込まれたバイト数 n、エラー err、および netFD を引数に取り、io.EOF の適切な処理を行います。特に、ストリームソケット (SOCK_STREAM) 以外(データグラムソケット SOCK_DGRAM や生ソケット SOCK_RAW)では、n=0 かつ err=nil の場合に io.EOF を返さないように修正されています。これは、データグラムソケットなどでは0バイトの読み込みが必ずしもEOFを意味しないためです。

  4. n = 0 挙動の復元: syscall.Readsyscall.Recvfrom などが syscall.EAGAIN を返した場合に、読み込まれたバイト数 n0 に設定する挙動が復元されました。これは、リビジョン 2a55e349097f の挙動に合わせるもので、以前の変更(CL 6851096)でこの挙動が失われていた可能性があります。EAGAIN は「再試行が必要」を意味するため、この時点で有効なバイトは読み込まれていないことを明確にするための変更です。

これらの変更により、デッドライン処理のロジックが pollServer に集約され、netFD のI/Oメソッドはよりシンプルでクリーンなエラーハンドリングを行うようになりました。これにより、コードの重複が減り、デッドライン関連のバグが入り込む可能性が低減されます。

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

このコミットで変更された主要なファイルは src/pkg/net/fd_unix.gosrc/pkg/net/fd_unix_test.go です。

src/pkg/net/fd_unix.go

  • pollServer.CheckDeadlines():

    --- a/src/pkg/net/fd_unix.go
    +++ b/src/pkg/net/fd_unix.go
    @@ -181,12 +181,10 @@ func (s *pollServer) CheckDeadlines() {
     			delete(s.pending, key)
     			if mode == 'r' {
     				s.poll.DelFD(fd.sysfd, mode)
    -				fd.rdeadline = -1
     			} else {
     				s.poll.DelFD(fd.sysfd, mode)
    -				fd.wdeadline = -1
     			}
    -			s.WakeFD(fd, mode, nil)
    +			s.WakeFD(fd, mode, errTimeout)
     		} else if nextDeadline == 0 || t < nextDeadline {
     			nextDeadline = t
     		}
    

    デッドラインが期限切れになった際に WakeFDerrTimeout を直接渡すように変更。fd.rdeadlinefd.wdeadline のリセットが不要になった。

  • netFD.connect():

    --- a/src/pkg/net/fd_unix.go
    +++ b/src/pkg/net/fd_unix.go
    @@ -329,14 +327,10 @@ func (fd *netFD) name() string {
     
     func (fd *netFD) connect(ra syscall.Sockaddr) error {
     	err := syscall.Connect(fd.sysfd, ra)
    -	hadTimeout := fd.wdeadline > 0
     	if err == syscall.EINPROGRESS {
     		if err = fd.pollServer.WaitWrite(fd); err != nil {
     			return err
     		}
    -		if hadTimeout && fd.wdeadline < 0 {
    -			return errTimeout
    -		}
     		var e int
     		e, err = syscall.GetsockoptInt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_ERROR)
     		if err != nil {
    

    接続デッドラインに関する冗長なチェックを削除。

  • netFD.Read():

    --- a/src/pkg/net/fd_unix.go
    +++ b/src/pkg/net/fd_unix.go
    @@ -430,20 +424,15 @@ func (fd *netFD) Read(p []byte) (n int, err error) {
     			}
     		}
     		n, err = syscall.Read(int(fd.sysfd), p)
    -		if err == syscall.EAGAIN {
    +		if err != nil {
     			n = 0
    -			err = errTimeout
    -			if fd.rdeadline >= 0 {
    +			if err == syscall.EAGAIN {
     				if err = fd.pollServer.WaitRead(fd); err == nil {
     					continue
     				}
     			}
     		}
    -		if err != nil {
    -			n = 0
    -		} else if n == 0 && err == nil && fd.sotype != syscall.SOCK_DGRAM {
    -			err = io.EOF
    -		}
    +		err = chkReadErr(n, err, fd)
     		break
     	}
     	if err != nil && err != io.EOF {
    

    EAGAIN 処理と io.EOF 処理を chkReadErr 関数に委譲。n=0 の復元。

  • netFD.ReadFrom():

    --- a/src/pkg/net/fd_unix.go
    +++ b/src/pkg/net/fd_unix.go
    @@ -467,18 +456,15 @@ func (fd *netFD) ReadFrom(p []byte) (n int, sa syscall.Sockaddr, err error) {
     			}
     		}
     		n, sa, err = syscall.Recvfrom(fd.sysfd, p, 0)
    -		if err == syscall.EAGAIN {
    +		if err != nil {
     			n = 0
    -			err = errTimeout
    -			if fd.rdeadline >= 0 {
    +			if err == syscall.EAGAIN {
     				if err = fd.pollServer.WaitRead(fd); err == nil {
     					continue
     				}
     			}
     		}
    -		if err != nil {
    -			n = 0
    -		}
    +		err = chkReadErr(n, err, fd)
     		break
     	}
     	if err != nil && err != io.EOF {
    

    EAGAIN 処理と io.EOF 処理を chkReadErr 関数に委譲。n=0 の復元。

  • netFD.ReadMsg():

    --- a/src/pkg/net/fd_unix.go
    +++ b/src/pkg/net/fd_unix.go
    @@ -502,27 +488,30 @@ func (fd *netFD) ReadMsg(p []byte, oob []byte) (n, oobn, flags int, sa syscall.S
     			}
     		}
     		n, oobn, flags, sa, err = syscall.Recvmsg(fd.sysfd, p, oob, 0)
    -		if err == syscall.EAGAIN {
    -			n = 0
    -			err = errTimeout
    -			if fd.rdeadline >= 0 {
    +		if err != nil {
    +			// TODO(dfc) should n and oobn be set to nil
    +			if err == syscall.EAGAIN {
     				if err = fd.pollServer.WaitRead(fd); err == nil {
     					continue
     				}
     			}
     		}
    -		if err == nil && n == 0 {
    -			err = io.EOF
    -		}
    +		err = chkReadErr(n, err, fd)
     		break
     	}
     	if err != nil && err != io.EOF {
     		err = &OpError{"read", fd.net, fd.laddr, err}
    -		return
     	}
     	return
     }
     
    +func chkReadErr(n int, err error, fd *netFD) error {
    +	if n == 0 && err == nil && fd.sotype != syscall.SOCK_DGRAM && fd.sotype != syscall.SOCK_RAW {
    +		return io.EOF
    +	}
    +	return err
    +}
    +
     func (fd *netFD) Write(p []byte) (int, error) {
     	fd.wio.Lock()
     	defer fd.wio.Unlock()
    

    EAGAIN 処理と io.EOF 処理を chkReadErr 関数に委譲。chkReadErr 関数の新規追加。

  • netFD.Write(), netFD.WriteTo(), netFD.WriteMsg(), netFD.accept(): これらのメソッドからも、errTimeout の直接生成や wdeadline / rdeadline のチェックが削除され、pollServer.Wait{Write,Read} が返すエラーをそのまま利用するように変更されています。

src/pkg/net/fd_unix_test.go

  • chkReadErrTestsTestChkReadErr の追加:
    var chkReadErrTests = []struct {
    	n        int
    	err      error
    	fd       *netFD
    	expected error
    }{
    	// ... テストケース ...
    }
    
    func TestChkReadErr(t *testing.T) {
    	for _, tt := range chkReadErrTests {
    		actual := chkReadErr(tt.n, tt.err, tt.fd)
    		if actual != tt.expected {
    			t.Errorf("chkReadError(%v, %v, %v): expected %v, actual %v", tt.n, tt.err, tt.fd.sotype, tt.expected, actual)
    		}
    	}
    }
    
    新しく追加された chkReadErr ヘルパー関数の動作を検証するためのテストケースとテスト関数が追加されました。これにより、特に io.EOF の挙動がソケットタイプによって正しく処理されることが保証されます。

コアとなるコードの解説

このコミットの核心は、Goのネットワークスタックにおけるデッドライン処理の「責任の分離」と「一元化」です。

  1. デッドライン処理の pollServer への集約: 以前は、各 netFD のI/Oメソッド(Read, Write など)が、自身のデッドライン (rdeadline, wdeadline) をチェックし、期限切れであれば errTimeout を生成していました。このアプローチは、デッドライン処理のロジックが複数の場所に分散し、コードの重複や複雑性を招いていました。 このコミットでは、pollServer がデッドラインの監視とタイムアウトの通知の唯一の責任を持つように変更されました。pollServer.CheckDeadlines() がデッドラインの期限切れを検知すると、直接 s.WakeFD(fd, mode, errTimeout) を呼び出します。これにより、netFD.Wait{Read,Write} は、デッドラインが期限切れになった場合に errTimeout を受け取るようになります。結果として、各I/Oメソッドからデッドライン関連の複雑な条件分岐が削除され、コードが大幅に簡素化されました。

  2. chkReadErr ヘルパー関数による読み込みエラー処理の標準化: 読み込み操作における io.EOF の扱いは、ソケットの種類(ストリームソケットかデータグラムソケットかなど)によって異なります。ストリームソケットでは、0バイトの読み込みと nil エラーは通常、接続の終了(EOF)を意味します。しかし、データグラムソケットや生ソケットでは、0バイトの読み込みが必ずしもEOFを意味するわけではありません(例えば、空のデータグラムを受信した場合など)。 chkReadErr 関数の導入により、この複雑なロジックが一箇所にカプセル化されました。これにより、Read, ReadFrom, ReadMsg といった複数の読み込みメソッドで、一貫性のある正確な io.EOF 処理が保証されます。これは、Goのネットワークパッケージの堅牢性と正確性を高める上で非常に重要です。

  3. n = 0 挙動の復元: syscall.EAGAIN が返された際に n0 に設定する挙動の復元は、低レベルのシステムコールからの戻り値を正確に解釈するためのものです。EAGAIN は「操作がブロックされるため、後で再試行してください」という意味であり、この時点で有効なデータは読み込まれていません。したがって、n=0 とすることで、読み込み操作が何もバイトを返さなかったことを明確に示し、後続の処理が正しい状態を前提とできるようにします。これは、ネットワークI/Oの正確な状態管理に寄与します。

これらの変更は、Goのネットワークパッケージの内部実装をよりクリーンで、保守しやすく、そして堅牢なものにすることを目的としています。デッドライン処理のロジックを pollServer に集約することで、将来的な機能拡張やバグ修正が容易になります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (netパッケージ): https://pkg.go.dev/net
  • Go言語のシステムコールパッケージ (syscall): https://pkg.go.dev/syscall
  • Go言語のioパッケージ: https://pkg.go.dev/io
  • Unix系システムコールに関する一般的な情報 (e.g., epoll, kqueue, poll, EAGAIN, SO_ERROR, SOCK_DGRAM, SOCK_RAW)
  • Go言語のネットワークプログラミングに関する一般的な記事やチュートリアル
  • Go言語のIssue Tracker (Issue #4434 の具体的な内容は特定できませんでしたが、一般的なGoのIssueの構造を理解するために参照しました)