[インデックス 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): ネットワーク接続における読み書き操作の最大待機時間を設定する機能です。
SetReadDeadline
やSetWriteDeadline
メソッドで設定され、指定された時間内に操作が完了しない場合、タイムアウトエラーが発生します。 syscall.EAGAIN
: Unix系システムコールが非ブロッキングモードで実行された際に、操作が即座に完了せず、後で再試行する必要があることを示すエラーコードです。ネットワークI/Oでは、データがまだ利用可能でない場合や、バッファが満杯で書き込みができない場合などに返されます。io.EOF
: Go言語のio
パッケージで定義されているエラーで、入力の終わりに達したことを示します。ネットワーク接続においては、リモートエンドが接続を正常にクローズした場合に、読み込み操作が0バイトを返し、かつエラーがnil
の場合にio.EOF
として扱われることがあります。syscall.SOCK_DGRAM
とsyscall.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.rdeadline
や fd.wdeadline
といったデッドラインの値を直接チェックし、タイムアウトエラー (errTimeout
) を生成していました。また、pollServer.DelFD
が呼び出された後に fd.rdeadline = -1
や fd.wdeadline = -1
といったデッドラインのリセットも行われていました。
このコミットでは、以下の変更が行われています。
-
pollServer.WakeFD
でのerrTimeout
の直接通知:pollServer
のCheckDeadlines
メソッド内で、デッドラインが期限切れになった際にs.WakeFD(fd, mode, errTimeout)
を呼び出すように変更されました。これにより、pollServer
がデッドラインの期限切れを検知し、直接errTimeout
をnetFD.Wait{Read,Write}
に伝えることができるようになりました。 以前はs.WakeFD(fd, mode, nil)
を呼び出し、その後netFD
のI/Oメソッド内でfd.rdeadline < 0
のチェックなどを行ってerrTimeout
を生成していました。この変更により、デッドライン処理のロジックがpollServer
側に集約され、各I/Oメソッドからデッドライン関連の冗長なチェックが削除されました。 -
netFD
のI/Oメソッドからのデッドラインチェックの削除:netFD.connect
,netFD.Read
,netFD.ReadFrom
,netFD.ReadMsg
,netFD.Write
,netFD.WriteTo
,netFD.WriteMsg
,netFD.accept
の各メソッドから、fd.rdeadline
やfd.wdeadline
の値に基づくタイムアウトチェック(例:if hadTimeout && fd.wdeadline < 0 { return errTimeout }
)が削除されました。これにより、これらのメソッドはpollServer.Wait{Read,Write}
が返すエラーをそのまま利用するようになり、デッドライン処理の責任がpollServer
に移譲されました。 -
chkReadErr
ヘルパー関数の導入: 読み込み操作 (Read
,ReadFrom
,ReadMsg
) のエラーハンドリングを共通化するために、chkReadErr
という新しいヘルパー関数が導入されました。この関数は、読み込まれたバイト数n
、エラーerr
、およびnetFD
を引数に取り、io.EOF
の適切な処理を行います。特に、ストリームソケット (SOCK_STREAM
) 以外(データグラムソケットSOCK_DGRAM
や生ソケットSOCK_RAW
)では、n=0
かつerr=nil
の場合にio.EOF
を返さないように修正されています。これは、データグラムソケットなどでは0バイトの読み込みが必ずしもEOFを意味しないためです。 -
n = 0
挙動の復元:syscall.Read
やsyscall.Recvfrom
などがsyscall.EAGAIN
を返した場合に、読み込まれたバイト数n
を0
に設定する挙動が復元されました。これは、リビジョン2a55e349097f
の挙動に合わせるもので、以前の変更(CL 6851096)でこの挙動が失われていた可能性があります。EAGAIN
は「再試行が必要」を意味するため、この時点で有効なバイトは読み込まれていないことを明確にするための変更です。
これらの変更により、デッドライン処理のロジックが pollServer
に集約され、netFD
のI/Oメソッドはよりシンプルでクリーンなエラーハンドリングを行うようになりました。これにより、コードの重複が減り、デッドライン関連のバグが入り込む可能性が低減されます。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルは src/pkg/net/fd_unix.go
と src/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 }
デッドラインが期限切れになった際に
WakeFD
にerrTimeout
を直接渡すように変更。fd.rdeadline
やfd.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
chkReadErrTests
とTestChkReadErr
の追加:
新しく追加された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のネットワークスタックにおけるデッドライン処理の「責任の分離」と「一元化」です。
-
デッドライン処理の
pollServer
への集約: 以前は、各netFD
のI/Oメソッド(Read
,Write
など)が、自身のデッドライン (rdeadline
,wdeadline
) をチェックし、期限切れであればerrTimeout
を生成していました。このアプローチは、デッドライン処理のロジックが複数の場所に分散し、コードの重複や複雑性を招いていました。 このコミットでは、pollServer
がデッドラインの監視とタイムアウトの通知の唯一の責任を持つように変更されました。pollServer.CheckDeadlines()
がデッドラインの期限切れを検知すると、直接s.WakeFD(fd, mode, errTimeout)
を呼び出します。これにより、netFD.Wait{Read,Write}
は、デッドラインが期限切れになった場合にerrTimeout
を受け取るようになります。結果として、各I/Oメソッドからデッドライン関連の複雑な条件分岐が削除され、コードが大幅に簡素化されました。 -
chkReadErr
ヘルパー関数による読み込みエラー処理の標準化: 読み込み操作におけるio.EOF
の扱いは、ソケットの種類(ストリームソケットかデータグラムソケットかなど)によって異なります。ストリームソケットでは、0バイトの読み込みとnil
エラーは通常、接続の終了(EOF)を意味します。しかし、データグラムソケットや生ソケットでは、0バイトの読み込みが必ずしもEOFを意味するわけではありません(例えば、空のデータグラムを受信した場合など)。chkReadErr
関数の導入により、この複雑なロジックが一箇所にカプセル化されました。これにより、Read
,ReadFrom
,ReadMsg
といった複数の読み込みメソッドで、一貫性のある正確なio.EOF
処理が保証されます。これは、Goのネットワークパッケージの堅牢性と正確性を高める上で非常に重要です。 -
n = 0
挙動の復元:syscall.EAGAIN
が返された際にn
を0
に設定する挙動の復元は、低レベルのシステムコールからの戻り値を正確に解釈するためのものです。EAGAIN
は「操作がブロックされるため、後で再試行してください」という意味であり、この時点で有効なデータは読み込まれていません。したがって、n=0
とすることで、読み込み操作が何もバイトを返さなかったことを明確に示し、後続の処理が正しい状態を前提とできるようにします。これは、ネットワークI/Oの正確な状態管理に寄与します。
これらの変更は、Goのネットワークパッケージの内部実装をよりクリーンで、保守しやすく、そして堅牢なものにすることを目的としています。デッドライン処理のロジックを pollServer
に集約することで、将来的な機能拡張やバグ修正が容易になります。
関連リンク
- Go CL (Change List): https://golang.org/cl/6850110
- GitHub Commit: https://github.com/golang/go/commit/28b599425dd535539f9001d42ec4dd4d472f3195
参考にした情報源リンク
- 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の構造を理解するために参照しました)