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

[インデックス 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)」として知られています。

具体的には、以下のシナリオで問題が発生する可能性があります。

  1. 部分的な書き込み: syscall.Writeは、バッファの途中で停止し、要求されたバイト数よりも少ないバイト数を書き込んで成功を返すことがあります。これは、ディスクI/Oの準備ができていない、ネットワークバッファが一時的に満杯である、または他のシステムリソースの制約など、様々な理由で発生します。
  2. 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パッケージのファイル書き込み処理が、どのような状況下でも確実にすべてのデータを書き込むようにするためのものです。

前提知識の解説

このコミットを理解するためには、以下のシステムプログラミングに関する前提知識が必要です。

  1. システムコール (System Call): オペレーティングシステムが提供するサービスをプログラムが利用するためのインターフェースです。ファイルI/O(読み書き)、メモリ管理、プロセス制御など、低レベルな操作を行う際に使用されます。Go言語では、syscallパッケージを通じてこれらのシステムコールにアクセスできます。

  2. syscall.Write: Unix系システムにおけるwrite(2)システムコールに対応するGo言語の関数です。指定されたファイルディスクリプタ(f.fd)に、バイトスライス(b)のデータを書き込みます。この関数は、実際に書き込まれたバイト数と、発生したエラーを返します。

  3. ショートライト (Short Write): write(2)システムコール(およびsyscall.Write)の重要な特性の一つで、要求されたバイト数(len(b))よりも少ないバイト数(m)を書き込んで成功を返すことがあります。これはエラーではありませんが、呼び出し元は残りのデータを書き込むために追加のwrite呼び出しを行う必要があります。ショートライトは、以下のような状況で発生し得ます。

    • パイプやソケットのバッファが満杯: 書き込み先のバッファに十分な空きがない場合、一部のデータしか書き込めないことがあります。
    • 非ブロッキングI/O: ファイルディスクリプタが非ブロッキングモードに設定されている場合、すぐに書き込み可能なデータのみを書き込み、残りは後で再試行する必要があります。
    • ディスクI/Oの制約: ディスクへの書き込みが一時的に遅延する場合など。
  4. EINTRエラー: errnoの一つで、システムコールがシグナルによって中断されたことを示します。例えば、プログラムがシグナルハンドラを実行するためにシステムコールを一時停止した場合に発生します。EINTRが返された場合、システムコールは失敗したわけではなく、単に中断されただけなので、通常は同じ引数でシステムコールを再試行する必要があります。

  5. ファイルディスクリプタ (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") // この行は実際には到達しない
}

このループは、以下の条件が満たされるまで繰り返されます。

  1. 完全な書き込みが成功した場合: m(実際に書き込まれたバイト数)がlen(b)(残りの書き込み対象バイト数)と等しく、かつエラーがない場合。この場合、ループはreturn n, errで終了します。
  2. エラーが発生し、かつEINTRではない場合: syscall.WriteEINTR以外のエラーを返した場合。この場合も、ループは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ループと条件分岐にあります。

  1. for { ... }: これは無限ループを意味します。書き込み操作が完全に完了するか、致命的なエラーが発生するまで、ループ内の処理が繰り返されます。

  2. m, err := syscall.Write(f.fd, b): 実際にシステムコールwrite(2)を呼び出します。

    • f.fd: 書き込み対象のファイルディスクリプタ。
    • b: 書き込むべき残りのバイトスライス。
    • m: syscall.Writeが実際に書き込んだバイト数。
    • err: syscall.Writeが返したエラー。
  3. n += m: nは、このFile.writeメソッドの呼び出し全体でこれまでに書き込まれた合計バイト数を追跡する変数です。各ループイテレーションでsyscall.Writeが書き込んだバイト数mnに加算します。

  4. 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がシグナルによって中断されたことを意味します。この場合、データは書き込まれていないか、部分的にしか書き込まれていない可能性があり、操作を再試行する必要があります。
  5. b = b[m:]: 上記のif条件が真の場合(つまり、再試行が必要な場合)、次に書き込むべきデータは、すでに書き込まれたmバイトを除いた残りの部分になります。スライスbmバイト分進めることで、次のsyscall.Write呼び出しでは未書き込みのデータのみが対象となります。

  6. continue: if条件が真の場合、continueステートメントによってループの次のイテレーションが開始され、残りのデータに対するsyscall.Writeが再度試行されます。

  7. return n, err: if条件が偽の場合(つまり、完全な書き込みが成功したか、EINTR以外のエラーが発生した場合)、ループを終了し、これまでに書き込まれた合計バイト数nと、最後に発生したエラーerrを返します。

  8. panic("not reached"): この行は、Goのコンパイラが「この関数は常に値を返す」ということを認識させるためのものです。論理的には、ループは常にreturnステートメントで終了するため、このpanicには到達しません。

このロジックにより、File.writeは、低レベルのシステムコールが部分的な成功や一時的な中断を返しても、高レベルでは完全な書き込み操作として振る舞うことが保証されます。

関連リンク

参考にした情報源リンク

  • Unix write(2) man page: writeシステムコールの挙動、特にショートライトやEINTRに関する詳細な情報源。
  • Go syscall package documentation: Go言語におけるシステムコールインターフェースの公式ドキュメント。
  • Go issues tracker: Go言語の過去のバグ報告や機能要求を検索するためのリソース。
  • Linux man 7 signal: シグナルとEINTRに関する一般的な情報源。