[インデックス 19230] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet
パッケージ内のfd_unix.go
ファイルに対する変更です。具体的には、Unix系システムにおけるネットワークファイルディスクリプタ(netFD
)のwriteMsg
関数において、メッセージ送信時に実際に転送されたバイト数を正確に返すように修正が行われました。
コミット
- コミットハッシュ:
e419ab6452a9973025f95d3a9702abe1c7a6df04
- 作者: Mikio Hara (
mikioh.mikioh@gmail.com
) - コミット日時: 2014年4月26日 土曜日 06:52:37 +0900
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e419ab6452a9973025f95d3a9702abe1c7a6df04
元コミット内容
net: make WriteMsg{IP,UDP,Unix} return the correct number of bytes transferred
Fixes #7645
LGTM=iant, bradfitz
R=iant, bradfitz
CC=golang-codereviews
https://golang.org/cl/90170046
変更の背景
このコミットは、Go言語のnet
パッケージが提供するWriteMsg{IP,UDP,Unix}
系の関数が、ネットワーク経由でメッセージを送信する際に、実際に転送されたバイト数ではなく、送信を試みたバイト数(送信バッファの全長)を誤って返してしまうというバグを修正するために行われました。
ネットワークプログラミングにおいて、sendmsg
のようなシステムコールは、様々な理由(例: ソケットバッファの枯渇、ネットワークの輻輳、非ブロッキングI/Oでの一時的なブロック)により、指定されたデータの一部しか一度に送信できない「部分的な書き込み」を行うことがあります。このような場合、システムコールは実際に送信できたバイト数を正確に返します。しかし、Goのnet
パッケージの内部実装では、このシステムコールからの正確な戻り値を無視し、常に送信しようとしたデータの全長を返してしまう問題がありました。
この不正確な戻り値は、アプリケーションレベルでのデータ送信の確認、エラーハンドリング、または再送ロジックに深刻な影響を与える可能性があります。例えば、アプリケーションが「データがすべて送信された」と誤認し、実際には一部しか送信されていないにもかかわらず次の処理に進んでしまう、といった状況が考えられます。
この問題はGoのIssue #7645として報告されており、本コミットはその問題を解決することを目的としています。
前提知識の解説
Go言語のnet
パッケージ
Go言語のnet
パッケージは、TCP/IP、UDP、Unixドメインソケットなど、様々なネットワークプロトコルを扱うための基本的なインターフェースと実装を提供します。このパッケージは、高レベルなAPI(例: net.Dial
, net.Listen
)から、低レベルなソケット操作(例: net.Conn
インターフェースの実装)までをカバーしており、Goアプリケーションでネットワーク通信を行う際の基盤となります。
WriteMsg
関数群
net
パッケージ内部には、WriteMsgIP
, WriteMsgUDP
, WriteMsgUnix
といった、メッセージ指向のプロトコル(UDPやUnixドメインソケットなど)でデータと補助データ(out-of-band data)を送信するための内部関数群が存在します。これらの関数は、最終的にOSのsendmsg(2)
システムコールを呼び出して実際のデータ送信を行います。
syscall.Sendmsg
とsyscall.SendmsgN
syscall
パッケージ: Go言語からOSのシステムコールを直接呼び出すための低レベルなインターフェースを提供するパッケージです。OS固有の機能や、Goの標準ライブラリでは提供されていない高度な操作を行う際に使用されます。sendmsg(2)
システムコール: Unix系OSで提供されるシステムコールで、ソケットを介してメッセージを送信するために使用されます。このシステムコールは、送信するデータ(ペイロード)、補助データ、宛先アドレスなどを指定でき、実際に送信されたバイト数を戻り値として返します。syscall.Sendmsg
: Goのsyscall
パッケージにおけるsendmsg(2)
システムコールのラッパー関数です。syscall.SendmsgN
: このコミットで導入された、または既存のsyscall
パッケージ内の関数で、sendmsg(2)
システムコールが返す実際のバイト数をn
として明示的に返すように設計された関数です。従来のsyscall.Sendmsg
がエラーのみを返すようなシグネチャであった場合、SendmsgN
はn int, err error
というシグネチャを持つことで、より直接的に送信バイト数を取得できるようにします。
EAGAIN
エラー
EAGAIN
(またはEWOULDBLOCK
)は、非ブロッキングI/O操作において、要求された操作(この場合はデータの書き込み)が即座に完了できない場合にOSが返すエラーコードです。これは一時的な状態であり、通常は後で操作を再試行する必要があることを示します。ネットワークプログラミングでは、ソケットが書き込み可能になるまで待機し、その後操作を再試行するループを実装するのが一般的です。
fd_unix.go
src/pkg/net/fd_unix.go
は、Goのnet
パッケージ内でUnix系OSに特化したファイルディスクリプタ(fd
)の操作を扱うソースファイルです。ソケットの作成、読み書き、クローズなど、低レベルなソケットI/Oの実装が含まれています。
技術的詳細
このコミットの技術的な核心は、src/pkg/net/fd_unix.go
ファイル内のnetFD.writeMsg
関数におけるsyscall.Sendmsg
の呼び出し方法の変更にあります。
変更前のコードでは、syscall.Sendmsg
を呼び出した後、エラーが発生しなかった場合に無条件でn = len(p)
(送信しようとしたペイロードの全長)という代入を行っていました。これは、sendmsg(2)
システムコールが実際に送信したバイト数をn
として返していたとしても、その値を無視して常にlen(p)
をwriteMsg
関数の戻り値n
として設定してしまうことを意味します。結果として、部分的な書き込みが発生した場合でも、呼び出し元には常に「全データが送信された」と誤った情報が伝えられていました。
このコミットでは、以下の2つの主要な変更が行われました。
-
syscall.Sendmsg
からsyscall.SendmsgN
への変更: 以前はerr = syscall.Sendmsg(fd.sysfd, p, oob, sa, 0)
のように、syscall.Sendmsg
の戻り値としてエラーのみを受け取っていました。 変更後はn, err = syscall.SendmsgN(fd.sysfd, p, oob, sa, 0)
となり、syscall.SendmsgN
が返す実際の送信バイト数n
を直接受け取るようになりました。これにより、n
にはsendmsg(2)
システムコールが返した正確なバイト数が格納されます。 -
n = len(p)
の削除:syscall.SendmsgN
が正確な送信バイト数を返すようになったため、エラーがない場合にn
をlen(p)
に上書きしていた行が不要となり、削除されました。これにより、writeMsg
関数は、OSが報告した実際の送信バイト数をそのまま呼び出し元に返すようになります。
EAGAIN
エラーに対する再試行ロジックは変更されておらず、非ブロッキングソケットでの一時的な書き込み不可状態には引き続き適切に対応しています。この修正により、net
パッケージのWriteMsg
関数群は、より堅牢で正確なネットワークI/O操作を提供するようになりました。
コアとなるコードの変更箇所
--- a/src/pkg/net/fd_unix.go
+++ b/src/pkg/net/fd_unix.go
@@ -375,7 +375,7 @@ func (fd *netFD) writeMsg(p []byte, oob []byte, sa syscall.Sockaddr) (n int, oob
return 0, 0, &OpError{"write", fd.net, fd.raddr, err}
}
for {
- err = syscall.Sendmsg(fd.sysfd, p, oob, sa, 0)
+ n, err = syscall.SendmsgN(fd.sysfd, p, oob, sa, 0)
if err == syscall.EAGAIN {
if err = fd.pd.WaitWrite(); err == nil {
continue
@@ -384,7 +384,6 @@ func (fd *netFD) writeMsg(p []byte, oob []byte, sa syscall.Sockaddr) (n int, oob
break
}
if err == nil {
- n = len(p)
oobn = len(oob)
} else {
err = &OpError{"write", fd.net, fd.raddr, err}
コアとなるコードの解説
-
func (fd *netFD) writeMsg(p []byte, oob []byte, sa syscall.Sockaddr) (n int, oobn int, err error)
: この関数は、netFD
型のレシーバ(ネットワークファイルディスクリプタ)に対して定義されており、ペイロードp
、補助データoob
、および宛先ソケットアドレスsa
を使用してメッセージを送信します。戻り値は、実際に送信されたペイロードのバイト数n
、補助データのバイト数oobn
、および操作中に発生したエラーerr
です。 -
- err = syscall.Sendmsg(fd.sysfd, p, oob, sa, 0)
: 変更前のコードです。ここではsyscall.Sendmsg
を呼び出し、その戻り値としてエラーのみをerr
変数に格納していました。sendmsg(2)
システムコールが返す実際の送信バイト数は、この時点ではn
変数に反映されていませんでした。 -
+ n, err = syscall.SendmsgN(fd.sysfd, p, oob, sa, 0)
: 変更後のコードです。syscall.SendmsgN
という関数が呼び出されています。この関数は、sendmsg(2)
システムコールが返す実際の送信バイト数をn
として、そしてエラーをerr
として、両方を戻り値として提供します。これにより、n
変数には、OSが実際に送信したバイト数が正確に格納されるようになります。 -
if err == syscall.EAGAIN { ... }
: この部分は、非ブロッキングソケットでの書き込み操作が一時的にブロックされた場合(EAGAIN
エラー)のハンドリングです。fd.pd.WaitWrite()
を呼び出してソケットが書き込み可能になるまで待機し、その後continue
でループの先頭に戻り、SendmsgN
の呼び出しを再試行します。このロジックは変更されていません。 -
- n = len(p)
: 変更前のコードで、syscall.Sendmsg
の呼び出し後にエラーが発生しなかった場合に実行されていた行です。この行は、n
の値を送信バッファp
の全長で上書きしていました。これにより、sendmsg(2)
が部分的な書き込みを行ったとしても、writeMsg
関数は常にlen(p)
を返してしまい、バグの原因となっていました。syscall.SendmsgN
が正確なn
を返すようになったため、この行は不要となり削除されました。
この変更により、writeMsg
関数は、ネットワークI/O操作の正確な結果を呼び出し元に報告できるようになり、net
パッケージを利用するアプリケーションの信頼性が向上しました。
関連リンク
- Go Issue #7645: このコミットが修正した問題の報告。 https://github.com/golang/go/issues/7645
- Go CL 90170046: このコミットに対応するGoのコードレビューシステム(Gerrit)上のチェンジリスト。 https://golang.org/cl/90170046
参考にした情報源リンク
- Go言語の公式ドキュメント (
net
パッケージ,syscall
パッケージ) - Unix
sendmsg(2)
manページ - GoのIssueトラッカー (Issue #7645)
syscall.SendmsgN
に関するWeb検索結果