[インデックス 12651] ファイルの概要
このコミットは、Go言語のos
パッケージにおけるファイル書き込み処理の堅牢性を向上させるためのものです。具体的には、syscall.Write
システムコールが常に要求されたバイト数すべてを書き込むとは限らないという問題(いわゆる「ショートライト」)に対処し、部分的な書き込みやEINTR
エラーが発生した場合に、残りのデータを書き込むまで処理をリトライするロジックを導入しています。これにより、ファイル書き込みの信頼性が向上し、特定の環境(例: Plan 9)でのテスト失敗などの問題が解決されます。
コミット
- コミットハッシュ:
b7b36524143e64738997ce3dbcfe38437e070f3c
- 作者: Russ Cox rsc@golang.org
- コミット日時: 2012年3月15日 木曜日 15:10:19 -0400
- コミットメッセージ:
os: do not assume syscall.Write will write everything Fixes #3323. R=golang-dev, remyoudompheng, gri CC=golang-dev https://golang.org/cl/5837047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b7b36524143e64738997ce3dbcfe38437e070f3c
元コミット内容
commit b7b36524143e64738997ce3dbcfe38437e070f3c
Author: Russ Cox <rsc@golang.org>
Date: Thu Mar 15 15:10:19 2012 -0400
os: do not assume syscall.Write will write everything
Fixes #3323.
R=golang-dev, remyoudompheng, gri
CC=golang-dev
https://golang.org/cl/5837047
---
src/pkg/os/file_unix.go | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/src/pkg/os/file_unix.go b/src/pkg/os/file_unix.go
index 6aa0280f4a..6271c3189e 100644
--- a/src/pkg/os/file_unix.go
+++ b/src/pkg/os/file_unix.go
@@ -173,7 +173,21 @@ func (f *File) pread(b []byte, off int64) (n int, err error) {
// write writes len(b) bytes to the File.
// It returns the number of bytes written and an error, if any.
func (f *File) write(b []byte) (n int int, err error) {
- return syscall.Write(f.fd, b)
+ for {
+ m, err := syscall.Write(f.fd, b)
+ n += m
+
+ // If the syscall wrote some data but not all (short write)
+ // or it returned EINTR, then assume it stopped early for
+ // reasons that are uninteresting to the caller, and try again.
+ if 0 < m && m < len(b) || err == syscall.EINTR {
+ b = b[m:]
+ continue
+ }
+
+ return n, err
+ }
+ panic("not reached")
}
// pwrite writes len(b) bytes to the File starting at byte offset off.
変更の背景
このコミットの背景には、syscall.Write
システムコールが、呼び出し元が要求したすべてのバイトを一度に書き込むとは限らないという、Unix系システムプログラミングにおける一般的な問題があります。これは「ショートライト(short write)」として知られています。
具体的には、以下のシナリオで問題が発生する可能性があります。
- 部分的な書き込み:
syscall.Write
は、バッファの途中で停止し、要求されたバイト数よりも少ないバイト数を書き込んで成功を返すことがあります。これは、ディスクI/Oの準備ができていない、ネットワークバッファが一時的に満杯である、または他のシステムリソースの制約など、様々な理由で発生します。 EINTR
エラー: システムコールがシグナルによって中断された場合、EINTR
エラーを返すことがあります。この場合、システムコールはデータを書き込む前に中断されたため、呼び出し元は操作を再試行する必要があります。
Go言語のos
パッケージのFile.write
メソッドは、以前はsyscall.Write
の戻り値をそのまま返していました。この実装では、上記のようなショートライトやEINTR
が発生した場合に、アプリケーション層で完全な書き込みが行われたと誤解する可能性がありました。
コミットメッセージにあるFixes #3323
は、この問題がGoの特定のテスト(TestRootRemoveDot failing on Plan 9
など)で顕在化したことを示唆しています。Plan 9のような特定のOS環境では、syscall.Write
の挙動が他のUnix系OSと異なり、ショートライトやEINTR
がより頻繁に発生した可能性があります。このため、os
パッケージのファイル書き込み処理が堅牢でないと、テストが不安定になったり、予期せぬデータ破損が発生したりするリスクがありました。
このコミットは、このような潜在的な問題を解決し、os
パッケージのファイル書き込み処理が、どのような状況下でも確実にすべてのデータを書き込むようにするためのものです。
前提知識の解説
このコミットを理解するためには、以下のシステムプログラミングに関する前提知識が必要です。
-
システムコール (System Call): オペレーティングシステムが提供するサービスをプログラムが利用するためのインターフェースです。ファイルI/O(読み書き)、メモリ管理、プロセス制御など、低レベルな操作を行う際に使用されます。Go言語では、
syscall
パッケージを通じてこれらのシステムコールにアクセスできます。 -
syscall.Write
: Unix系システムにおけるwrite(2)
システムコールに対応するGo言語の関数です。指定されたファイルディスクリプタ(f.fd
)に、バイトスライス(b
)のデータを書き込みます。この関数は、実際に書き込まれたバイト数と、発生したエラーを返します。 -
ショートライト (Short Write):
write(2)
システムコール(およびsyscall.Write
)の重要な特性の一つで、要求されたバイト数(len(b)
)よりも少ないバイト数(m
)を書き込んで成功を返すことがあります。これはエラーではありませんが、呼び出し元は残りのデータを書き込むために追加のwrite
呼び出しを行う必要があります。ショートライトは、以下のような状況で発生し得ます。- パイプやソケットのバッファが満杯: 書き込み先のバッファに十分な空きがない場合、一部のデータしか書き込めないことがあります。
- 非ブロッキングI/O: ファイルディスクリプタが非ブロッキングモードに設定されている場合、すぐに書き込み可能なデータのみを書き込み、残りは後で再試行する必要があります。
- ディスクI/Oの制約: ディスクへの書き込みが一時的に遅延する場合など。
-
EINTR
エラー:errno
の一つで、システムコールがシグナルによって中断されたことを示します。例えば、プログラムがシグナルハンドラを実行するためにシステムコールを一時停止した場合に発生します。EINTR
が返された場合、システムコールは失敗したわけではなく、単に中断されただけなので、通常は同じ引数でシステムコールを再試行する必要があります。 -
ファイルディスクリプタ (File Descriptor, FD): Unix系システムにおいて、開かれたファイルやソケット、パイプなどのI/Oリソースを識別するために使用される非負の整数です。
os.File
構造体は内部的にこのファイルディスクリプタを保持しています。
これらの概念を理解することで、syscall.Write
が常に完全な書き込みを保証しないこと、そしてなぜ再試行ロジックが必要なのかが明確になります。
技術的詳細
このコミットの技術的詳細は、os
パッケージのFile
構造体に対するwrite
メソッドの変更に集約されます。変更前は、write
メソッドは単にsyscall.Write
を呼び出し、その結果をそのまま返していました。
func (f *File) write(b []byte) (n int, err error) {
return syscall.Write(f.fd, b)
}
このシンプルな実装は、syscall.Write
が常にlen(b)
バイトを書き込むと仮定していました。しかし、前述の通り、この仮定はUnix系システムプログラミングの現実とは異なります。
変更後の実装では、for
ループを導入し、syscall.Write
がショートライトを返したり、EINTR
エラーを返したりした場合に、書き込み操作を継続的に再試行するロジックが追加されました。
func (f *File) write(b []byte) (n int, err error) {
for {
m, err := syscall.Write(f.fd, b)
n += m
// If the syscall wrote some data but not all (short write)
// or it returned EINTR, then assume it stopped early for
// reasons that are uninteresting to the caller, and try again.
if 0 < m && m < len(b) || err == syscall.EINTR {
b = b[m:]
continue
}
return n, err
}
panic("not reached") // この行は実際には到達しない
}
このループは、以下の条件が満たされるまで繰り返されます。
- 完全な書き込みが成功した場合:
m
(実際に書き込まれたバイト数)がlen(b)
(残りの書き込み対象バイト数)と等しく、かつエラーがない場合。この場合、ループはreturn n, err
で終了します。 - エラーが発生し、かつ
EINTR
ではない場合:syscall.Write
がEINTR
以外のエラーを返した場合。この場合も、ループはreturn n, err
で終了し、エラーが呼び出し元に伝播されます。
この変更により、os.File.Write
(Goの公開API)は、内部でsyscall.Write
が部分的な書き込みや中断を経験したとしても、最終的には要求されたすべてのバイトを書き込むか、または致命的なエラーが発生した場合にのみエラーを返すという、より堅牢な振る舞いをするようになります。これは、GoプログラムがファイルI/Oを扱う際の信頼性を大幅に向上させます。
panic("not reached")
という行は、Goのコンパイラが無限ループの可能性を警告するのを避けるための慣用的な記述です。このループはreturn
ステートメントで必ず終了するため、このpanic
には到達しません。
コアとなるコードの変更箇所
src/pkg/os/file_unix.go
ファイルの File.write
メソッドが変更されました。
--- a/src/pkg/os/file_unix.go
+++ b/src/pkg/os/file_unix.go
@@ -173,7 +173,21 @@ func (f *File) pread(b []byte, off int64) (n int, err error) {
// write writes len(b) bytes to the File.
// It returns the number of bytes written and an error, if any.
func (f *File) write(b []byte) (n int, err error) {
- return syscall.Write(f.fd, b)
+ for {
+ m, err := syscall.Write(f.fd, b)
+ n += m
+
+ // If the syscall wrote some data but not all (short write)
+ // or it returned EINTR, then assume it stopped early for
+ // reasons that are uninteresting to the caller, and try again.
+ if 0 < m && m < len(b) || err == syscall.EINTR {
+ b = b[m:]
+ continue
+ }
+
+ return n, err
+ }
+ panic("not reached")
}
// pwrite writes len(b) bytes to the File starting at byte offset off.
コアとなるコードの解説
変更されたFile.write
メソッドのコアとなるロジックは、for
ループと条件分岐にあります。
-
for { ... }
: これは無限ループを意味します。書き込み操作が完全に完了するか、致命的なエラーが発生するまで、ループ内の処理が繰り返されます。 -
m, err := syscall.Write(f.fd, b)
: 実際にシステムコールwrite(2)
を呼び出します。f.fd
: 書き込み対象のファイルディスクリプタ。b
: 書き込むべき残りのバイトスライス。m
:syscall.Write
が実際に書き込んだバイト数。err
:syscall.Write
が返したエラー。
-
n += m
:n
は、このFile.write
メソッドの呼び出し全体でこれまでに書き込まれた合計バイト数を追跡する変数です。各ループイテレーションでsyscall.Write
が書き込んだバイト数m
をn
に加算します。 -
if 0 < m && m < len(b) || err == syscall.EINTR { ... }
: これが再試行の条件を決定する重要な部分です。0 < m && m < len(b)
: これは「ショートライト」が発生したことを意味します。syscall.Write
は一部のデータを書き込んだ(0 < m
)が、すべてのデータを書き込んだわけではない(m < len(b)
)場合です。この場合、まだ書き込むべきデータが残っているため、再試行が必要です。err == syscall.EINTR
:syscall.Write
がシグナルによって中断されたことを意味します。この場合、データは書き込まれていないか、部分的にしか書き込まれていない可能性があり、操作を再試行する必要があります。
-
b = b[m:]
: 上記のif
条件が真の場合(つまり、再試行が必要な場合)、次に書き込むべきデータは、すでに書き込まれたm
バイトを除いた残りの部分になります。スライスb
をm
バイト分進めることで、次のsyscall.Write
呼び出しでは未書き込みのデータのみが対象となります。 -
continue
:if
条件が真の場合、continue
ステートメントによってループの次のイテレーションが開始され、残りのデータに対するsyscall.Write
が再度試行されます。 -
return n, err
:if
条件が偽の場合(つまり、完全な書き込みが成功したか、EINTR
以外のエラーが発生した場合)、ループを終了し、これまでに書き込まれた合計バイト数n
と、最後に発生したエラーerr
を返します。 -
panic("not reached")
: この行は、Goのコンパイラが「この関数は常に値を返す」ということを認識させるためのものです。論理的には、ループは常にreturn
ステートメントで終了するため、このpanic
には到達しません。
このロジックにより、File.write
は、低レベルのシステムコールが部分的な成功や一時的な中断を返しても、高レベルでは完全な書き込み操作として振る舞うことが保証されます。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/b7b36524143e64738997ce3dbcfe38437e070f3c
- Go Code Review (CL): https://golang.org/cl/5837047
- Go Issue #3323 (TestRootRemoveDot failing on Plan 9): https://goissues.org/issue/3323 (このコミットが修正した可能性のある、当時のGoのバグトラッカー上のIssue)
参考にした情報源リンク
- Unix
write(2)
man page:write
システムコールの挙動、特にショートライトやEINTR
に関する詳細な情報源。 - Go
syscall
package documentation: Go言語におけるシステムコールインターフェースの公式ドキュメント。 - Go issues tracker: Go言語の過去のバグ報告や機能要求を検索するためのリソース。
- Linux
man 7 signal
: シグナルとEINTR
に関する一般的な情報源。