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

[インデックス 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.Sendmsgsyscall.SendmsgN

  • syscallパッケージ: Go言語からOSのシステムコールを直接呼び出すための低レベルなインターフェースを提供するパッケージです。OS固有の機能や、Goの標準ライブラリでは提供されていない高度な操作を行う際に使用されます。
  • sendmsg(2)システムコール: Unix系OSで提供されるシステムコールで、ソケットを介してメッセージを送信するために使用されます。このシステムコールは、送信するデータ(ペイロード)、補助データ、宛先アドレスなどを指定でき、実際に送信されたバイト数を戻り値として返します。
  • syscall.Sendmsg: Goのsyscallパッケージにおけるsendmsg(2)システムコールのラッパー関数です。
  • syscall.SendmsgN: このコミットで導入された、または既存のsyscallパッケージ内の関数で、sendmsg(2)システムコールが返す実際のバイト数をnとして明示的に返すように設計された関数です。従来のsyscall.Sendmsgがエラーのみを返すようなシグネチャであった場合、SendmsgNn 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つの主要な変更が行われました。

  1. 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)システムコールが返した正確なバイト数が格納されます。

  2. n = len(p)の削除: syscall.SendmsgNが正確な送信バイト数を返すようになったため、エラーがない場合にnlen(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言語の公式ドキュメント (netパッケージ, syscallパッケージ)
  • Unix sendmsg(2) manページ
  • GoのIssueトラッカー (Issue #7645)
  • syscall.SendmsgNに関するWeb検索結果