[インデックス 14472] ファイルの概要
このコミットは、Go言語のネットワークパッケージ(net
)における、netFD.Read
、netFD.ReadFrom
、netFD.ReadMsg
といった読み取り操作が、非ブロッキングI/Oにおいてsyscall.EAGAIN
エラーを返した場合に、読み取られたバイト数を示す変数n
が不正確な値(-1)を保持してしまう問題を修正するものです。これにより、ネットワーク読み取りがタイムアウトした場合でも、n
が常に0バイトを返すように挙動が統一され、より堅牢なエラーハンドリングが実現されます。
コミット
commit 73b3e2301ebbaa4f940006b0869a158156613b4b
Author: Dave Cheney <dave@cheney.net>
Date: Mon Nov 26 10:59:43 2012 +1100
net: never return -1 bytes read from netFD.Read
If the a network read would block, and a packet arrived just before the timeout expired, then the number of bytes from the previous (blocking) read, -1, would be returned.
This change restores the previous logic, where n would be unconditionally set to 0 if err != nil, but was skipped due to a change in CL 6851096.
The test for this change is CL 6851061.
R=bradfitz, mikioh.mikioh, dvyukov, rsc
CC=golang-dev
https://golang.org/cl/6852085
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/73b3e2301ebbaa4f940006b0869a158156613b4b
元コミット内容
このコミットは、netFD.Read
、netFD.ReadFrom
、netFD.ReadMsg
といったネットワーク読み取り関数において、syscall.EAGAIN
エラーが発生した場合に、読み取られたバイト数n
が不正確な値(具体的には-1)を返す可能性があった問題を修正します。コミットメッセージによると、以前のブロッキング読み取りで返された-1という値が、タイムアウト直前にパケットが到着した場合にそのまま返されてしまうことがあったようです。この変更は、エラーが発生した際にn
を無条件に0に設定するという以前のロジックを復元するものです。これは、以前の変更(CL 6851096)によってスキップされていた処理を元に戻すことを意図しています。
変更の背景
Go言語のネットワークI/Oは、内部的に非ブロッキングI/Oとイベント駆動型ポーリング(Goランタイムのネットワークポーラー)を利用して実装されています。これにより、複数のゴルーチンが同時にI/O操作を行っても、OSレベルでは非ブロッキングで効率的に処理され、アプリケーションレベルではブロッキングI/Oのように振る舞うことができます。
しかし、このコミットが修正しようとしている問題は、低レベルのシステムコール(syscall.Read
など)がsyscall.EAGAIN
を返した場合のn
の値の扱いにありました。syscall.EAGAIN
は、非ブロッキングI/O操作が即座に完了できないことを示すエラーです。例えば、読み取り操作でデータがまだ到着していない場合などに返されます。
問題は、以前の変更(コミットメッセージで言及されているCL 6851096)によって、syscall.EAGAIN
が発生した際にn
を0にリセットするロジックがスキップされてしまったことに起因します。その結果、ネットワーク読み取りがブロッキング状態になり、タイムアウトが発生する直前にパケットが到着した場合、n
が以前のブロッキング読み取りで設定された不正確な値(-1)を保持したまま返される可能性がありました。これは、APIの利用者にとって混乱を招き、誤ったバイト数に基づいて処理を進めてしまうバグにつながる可能性があります。
このコミットは、この不整合を解消し、syscall.EAGAIN
エラーが発生した際には、読み取られたバイト数n
を明示的に0に設定することで、APIの振る舞いを一貫させ、より予測可能なものにすることを目的としています。
前提知識の解説
1. netFD
とGoのネットワークI/Oモデル
Go言語のnet
パッケージは、ネットワーク通信のための高レベルなインターフェースを提供しますが、その内部ではOSのシステムコールを直接利用しています。netFD
は、ファイルディスクリプタ(Unix系OSにおけるソケットなどのI/Oリソースを識別する整数値)をラップし、Goランタイムのネットワークポーラーと連携して非ブロッキングI/Oを効率的に管理するための内部構造体です。
GoのネットワークI/Oは、以下の特徴を持ちます。
- 非ブロッキングI/O: OSレベルでは、ソケットは非ブロッキングモードで設定されます。これにより、読み取りや書き込み操作が即座に完了しない場合でも、システムコールはブロックせずにエラー(例:
EAGAIN
)を返します。 - ネットワークポーラー: Goランタイムは、内部にネットワークポーラー(Unix系では
epoll
、kqueue
、WindowsではI/O完了ポートなど)を持っています。ゴルーチンがI/O操作でブロックする場合、そのゴルーチンはスリープ状態になり、ファイルディスクリプタはポーラーに登録されます。データが到着するなどしてファイルディスクリプタが準備完了状態になると、ポーラーがゴルーチンを再開させます。 - ブロッキングI/Oのような振る舞い: アプリケーション開発者から見ると、
net.Conn.Read()
などの操作はブロッキングI/Oのように見えます。これは、Goランタイムが非ブロッキングI/Oとポーラーを透過的に利用して、ゴルーチンを適切にスケジューリングしているためです。
2. syscall.Read
とsyscall.EAGAIN
syscall.Read
: これは、OSのread
システムコールをGoから呼び出すための関数です。指定されたファイルディスクリプタからデータを読み取り、読み取られたバイト数とエラーを返します。syscall.EAGAIN
: これは、Unix系OSで定義されているエラーコードの一つで、"Resource temporarily unavailable" を意味します。非ブロッキングI/O操作において、データがまだ準備できていないため、操作が即座に完了できない場合に返されます。このエラーを受け取った場合、通常は後で再試行する必要があります。EWOULDBLOCK
と同じ値を持つことが多く、同じ意味で使われます。
3. 読み取り関数の戻り値 (n int, err error)
GoのI/O操作関数は、慣例的に(n int, err error)
という形式で戻り値を返します。
n
: 読み取りまたは書き込みが成功したバイト数を示します。err
: 操作中に発生したエラーを示します。エラーがない場合はnil
です。
重要なのは、n
とerr
は独立して扱われるべきであるという点です。例えば、部分的な読み取りが成功し、かつエラーも発生した場合(例: EOF)、n
は0より大きい値になり、err
もnil
ではない値になります。しかし、今回のケースのように、読み取りが全く行われなかった場合(EAGAIN
など)は、n
は0であるべきです。
技術的詳細
このコミットの技術的詳細は、GoのネットワークI/Oにおけるエラーハンドリングの正確性に焦点を当てています。
netFD.Read
、netFD.ReadFrom
、netFD.ReadMsg
の各関数は、内部でsyscall.Read
、syscall.Recvfrom
、syscall.Recvmsg
といった低レベルのシステムコールを呼び出しています。これらのシステムコールが非ブロッキングモードで実行され、かつデータが即座に利用できない場合、syscall.EAGAIN
エラーを返します。
コミット前の問題は、syscall.EAGAIN
が返された際に、n
(読み取られたバイト数)が適切にリセットされず、以前の呼び出しで設定された可能性のある不正確な値(特に-1)を保持してしまうことでした。これは、GoのI/Oインターフェースの期待される振る舞い(エラーが発生し、かつデータが読み取られなかった場合はn=0
)に反します。
このコミットは、以下の修正を導入することでこの問題を解決します。
syscall.EAGAIN
検出時のn
のリセット: 各読み取り関数内で、if err == syscall.EAGAIN
という条件が真になった場合、直後にn = 0
という行が追加されます。- エラーの変換:
syscall.EAGAIN
は、Goの内部的なタイムアウトエラー(errTimeout
)に変換されます。これは、Goのネットワークポーラーがタイムアウトを処理する際の内部的なメカニズムと連携するためです。 - ポーラーへの待機指示:
errTimeout
に変換された後、fd.pollServer.WaitRead(fd)
が呼び出され、Goランタイムのネットワークポーラーに対して、このファイルディスクリプタが読み取り可能になるまで待機するように指示します。
この修正により、syscall.EAGAIN
が発生した際には、n
が確実に0に設定されるため、APIの利用者は、エラーが発生した際に読み取られたバイト数が0であることを安全に仮定できるようになります。これにより、ネットワークI/Oの堅牢性が向上し、予期せぬn
の値によるバグが防止されます。
コミットメッセージで言及されている「CL 6851096」と「CL 6851061」については、Goの内部的な変更リスト番号であり、公開されているGitHubのコミットとは直接関連付けが難しい場合があります。今回のWeb検索では、これらのCL番号がこのコミットの文脈とは異なる内容を示していました。これは、コミットメッセージが書かれた時点での内部的な参照か、あるいは参照が古くなっている可能性を示唆しています。しかし、このコミットの核心は、syscall.EAGAIN
発生時のn
の正確な処理にあり、その修正自体は明確です。
コアとなるコードの変更箇所
変更は src/pkg/net/fd_unix.go
ファイルに集中しており、以下の3つの関数にそれぞれ1行ずつ追加されています。
--- a/src/pkg/net/fd_unix.go
+++ b/src/pkg/net/fd_unix.go
@@ -431,6 +431,7 @@ func (fd *netFD) Read(p []byte) (n int, err error) {
}
n, err = syscall.Read(int(fd.sysfd), p)
if err == syscall.EAGAIN {
+ n = 0
err = errTimeout
if fd.rdeadline >= 0 {
if err = fd.pollServer.WaitRead(fd); err == nil {
@@ -467,6 +468,7 @@ 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 {
+ n = 0
err = errTimeout
if fd.rdeadline >= 0 {
if err = fd.pollServer.WaitRead(fd); err == nil {
@@ -501,6 +503,7 @@ 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 = fd.pollServer.WaitRead(fd); err == nil {
コアとなるコードの解説
上記の変更は、netFD
構造体の以下の3つのメソッドに適用されています。
-
func (fd *netFD) Read(p []byte) (n int, err error)
:- このメソッドは、一般的なネットワーク接続からのバイト列の読み取りを担当します。
n, err = syscall.Read(int(fd.sysfd), p)
: ここで実際のシステムコールによる読み取りが行われます。if err == syscall.EAGAIN { n = 0 }
: この行が追加されました。syscall.Read
がEAGAIN
(非ブロッキングI/Oでデータが即座に利用できない)を返した場合、読み取られたバイト数n
を明示的に0
に設定します。これにより、以前の不正確な値(-1など)が返されるのを防ぎます。err = errTimeout
: その後、EAGAIN
エラーはGo内部のerrTimeout
に変換されます。これは、Goのネットワークポーラーがタイムアウトを処理する際の内部的なエラー表現です。
-
func (fd *netFD) ReadFrom(p []byte) (n int, sa syscall.Sockaddr, err error)
:- このメソッドは、UDPなどのコネクションレスなソケットからデータを読み取り、送信元のアドレスも取得します。
n, sa, err = syscall.Recvfrom(fd.sysfd, p, 0)
: ここでrecvfrom
システムコールが呼び出されます。if err == syscall.EAGAIN { n = 0 }
:Read
メソッドと同様に、EAGAIN
が返された場合にn
を0
に設定します。
-
func (fd *netFD) ReadMsg(p []byte, oob []byte) (n, oobn, flags int, sa syscall.Sockaddr, err error)
:- このメソッドは、より低レベルなメッセージベースの読み取り(例: Unixドメインソケットからの補助データを含むメッセージ読み取り)を行います。
n, oobn, flags, sa, err = syscall.Recvmsg(fd.sysfd, p, oob, 0)
: ここでrecvmsg
システムコールが呼び出されます。if err == syscall.EAGAIN { n = 0 }
: 同様に、EAGAIN
が返された場合にn
を0
に設定します。
これらの変更は、GoのネットワークI/Oの内部実装において、非ブロッキングI/Oの振る舞いをより正確に、かつAPIの期待に沿うように調整するものです。これにより、n
とerr
の組み合わせが常に論理的に一貫したものとなり、上位レイヤーでのエラーハンドリングが簡素化され、バグの発生リスクが低減されます。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/73b3e2301ebbaa4f940006b0869a158156613b4b
- Go Change List (CL) 6852085: https://golang.org/cl/6852085 (これはコミットメッセージに記載されているGoの内部的な変更リストへのリンクです。GitHubのコミットと直接対応しているわけではありませんが、Goのレビューシステムにおける変更の履歴を示します。)
参考にした情報源リンク
- コミットメッセージの内容
syscall.EAGAIN
に関するWeb検索結果 (例: https://pkg.go.dev/syscall#EAGAIN や、Goの非ブロッキングI/Oに関する一般的な解説記事)- Go言語のネットワークプログラミングと内部実装に関する一般的な知識