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

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

このコミットは、Go言語のネットワークパッケージ(net)における、netFD.ReadnetFD.ReadFromnetFD.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.ReadnetFD.ReadFromnetFD.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系ではepollkqueue、WindowsではI/O完了ポートなど)を持っています。ゴルーチンがI/O操作でブロックする場合、そのゴルーチンはスリープ状態になり、ファイルディスクリプタはポーラーに登録されます。データが到着するなどしてファイルディスクリプタが準備完了状態になると、ポーラーがゴルーチンを再開させます。
  • ブロッキングI/Oのような振る舞い: アプリケーション開発者から見ると、net.Conn.Read()などの操作はブロッキングI/Oのように見えます。これは、Goランタイムが非ブロッキングI/Oとポーラーを透過的に利用して、ゴルーチンを適切にスケジューリングしているためです。

2. syscall.Readsyscall.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です。

重要なのは、nerrは独立して扱われるべきであるという点です。例えば、部分的な読み取りが成功し、かつエラーも発生した場合(例: EOF)、nは0より大きい値になり、errnilではない値になります。しかし、今回のケースのように、読み取りが全く行われなかった場合(EAGAINなど)は、nは0であるべきです。

技術的詳細

このコミットの技術的詳細は、GoのネットワークI/Oにおけるエラーハンドリングの正確性に焦点を当てています。

netFD.ReadnetFD.ReadFromnetFD.ReadMsgの各関数は、内部でsyscall.Readsyscall.Recvfromsyscall.Recvmsgといった低レベルのシステムコールを呼び出しています。これらのシステムコールが非ブロッキングモードで実行され、かつデータが即座に利用できない場合、syscall.EAGAINエラーを返します。

コミット前の問題は、syscall.EAGAINが返された際に、n(読み取られたバイト数)が適切にリセットされず、以前の呼び出しで設定された可能性のある不正確な値(特に-1)を保持してしまうことでした。これは、GoのI/Oインターフェースの期待される振る舞い(エラーが発生し、かつデータが読み取られなかった場合はn=0)に反します。

このコミットは、以下の修正を導入することでこの問題を解決します。

  1. syscall.EAGAIN検出時のnのリセット: 各読み取り関数内で、if err == syscall.EAGAINという条件が真になった場合、直後にn = 0という行が追加されます。
  2. エラーの変換: syscall.EAGAINは、Goの内部的なタイムアウトエラー(errTimeout)に変換されます。これは、Goのネットワークポーラーがタイムアウトを処理する際の内部的なメカニズムと連携するためです。
  3. ポーラーへの待機指示: 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つのメソッドに適用されています。

  1. 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.ReadEAGAIN(非ブロッキングI/Oでデータが即座に利用できない)を返した場合、読み取られたバイト数nを明示的に0に設定します。これにより、以前の不正確な値(-1など)が返されるのを防ぎます。
    • err = errTimeout: その後、EAGAINエラーはGo内部のerrTimeoutに変換されます。これは、Goのネットワークポーラーがタイムアウトを処理する際の内部的なエラー表現です。
  2. 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が返された場合にn0に設定します。
  3. 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が返された場合にn0に設定します。

これらの変更は、GoのネットワークI/Oの内部実装において、非ブロッキングI/Oの振る舞いをより正確に、かつAPIの期待に沿うように調整するものです。これにより、nerrの組み合わせが常に論理的に一貫したものとなり、上位レイヤーでのエラーハンドリングが簡素化され、バグの発生リスクが低減されます。

関連リンク

参考にした情報源リンク

  • コミットメッセージの内容
  • syscall.EAGAINに関するWeb検索結果 (例: https://pkg.go.dev/syscall#EAGAIN や、Goの非ブロッキングI/Oに関する一般的な解説記事)
  • Go言語のネットワークプログラミングと内部実装に関する一般的な知識