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

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

このコミットは、Go言語の標準ライブラリosパッケージ内のfile_unix.goファイルに対する変更です。このファイルは、Unix系システム(Linux, macOS, FreeBSDなど)におけるファイル操作の低レベルな実装、特にsyscallパッケージを介したシステムコールとの連携を扱っています。具体的には、ファイルの読み書き(read, pread, write, pwrite)に関する挙動が定義されています。

コミット

commit 8409dea8ee87dcaf8ecbf16ed59214eb08524973
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Sat Apr 26 10:34:33 2014 -0700

    os: cap reads and writes to 2GB on Darwin and FreeBSD
    
    Fixes #7812
    
    LGTM=josharian, iant
    R=rsc, iant, adg, ruiu, minux.ma, josharian
    CC=golang-codereviews
    https://golang.org/cl/89900044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/8409dea8ee87dcaf8ecbf16ed59214eb08524973

元コミット内容

os: cap reads and writes to 2GB on Darwin and FreeBSD

Fixes #7812

変更の背景

このコミットは、Go言語のIssue #7812を解決するために導入されました。Issue #7812は、Darwin(macOS)およびFreeBSDオペレーティングシステムにおいて、たとえ64ビットシステムであっても、一度に2GBを超えるサイズの読み書き操作が正しく機能しないという問題点を指摘していました。

具体的には、これらのOSでは、read(2)write(2)といったシステムコールが、2GBを超えるバッファサイズを受け取った場合に、期待通りに動作しない、あるいはエラーを返すという挙動を示すことがありました。これは、GoプログラムがこれらのOS上で大容量のファイルI/OやネットワークI/Oを行う際に、予期せぬエラーやデータ破損を引き起こす可能性がありました。

この問題を回避するため、Goランタイムは、DarwinおよびFreeBSD上でのreadpreadwritepwriteシステムコール呼び出しにおいて、渡されるバッファサイズを2GB未満に制限する(キャップする)必要がありました。これにより、OSのシステムコールが処理できる範囲内で操作が行われるようになり、安定性と信頼性が向上します。

前提知識の解説

1. システムコール (syscallパッケージ)

Go言語では、オペレーティングシステムの機能にアクセスするためにsyscallパッケージを使用します。syscall.Readsyscall.Writeなどは、それぞれOSのread(2)write(2)システムコールを直接呼び出すためのGoのラッパー関数です。これらの関数は、ファイルディスクリプタ(f.fd)、データバッファ、およびオフセット(pread, pwriteの場合)を引数として取り、読み書きされたバイト数とエラーを返します。

2. read(2) / write(2) システムコールの制限

Unix系OSのread(2)write(2)システムコールは、通常、一度に処理できるデータ量に上限があります。多くのシステムでは、この上限はSSIZE_MAX(通常は2^31 - 1、つまり約2GB)で定義されています。しかし、一部のOS(特に古いバージョンや特定のカーネル設定のDarwin/FreeBSD)では、この上限が厳密に適用されたり、あるいは2GBを超えるバッファを渡した場合に予期せぬ挙動を示すことがあります。これは、システムコールの内部実装や、32ビット時代の名残によるものと考えられます。

3. runtime.GOOS

runtime.GOOSは、Goプログラムが実行されているオペレーティングシステムを示す文字列定数です。例えば、macOSでは"darwin"、FreeBSDでは"freebsd"、Linuxでは"linux"となります。この定数を利用することで、特定のOSに依存するコードパスを条件付きでコンパイルしたり、実行時に異なるロジックを適用したりすることが可能になります。このコミットでは、runtime.GOOSを使ってDarwinとFreeBSDの場合にのみ特別な処理を適用しています。

4. ファイルディスクリプタ (f.fd)

Unix系OSでは、開かれたファイルやソケットなどのI/Oリソースは、整数値のファイルディスクリプタによって識別されます。Goのos.File構造体は、このファイルディスクリプタを内部に保持しており、syscallパッケージの関数に渡して実際のI/O操作を行います。

技術的詳細

このコミットの主要な技術的詳細は、DarwinおよびFreeBSD上でのI/O操作のバッファサイズを2GB未満に制限するメカニズムの導入です。

  1. 定数の導入: needsMaxRWmaxRWという2つの新しい定数が導入されました。

    • needsMaxRW: runtime.GOOSが"darwin"または"freebsd"の場合にtrueとなるブール定数です。これにより、特定のOSでのみ制限を適用するかどうかを効率的に判断できます。
    • maxRW: 2<<30 - 1、つまり2^31 - 1(約2GB)に設定された整数定数です。これは、DarwinおよびFreeBSDが一度に処理できる最大I/Oサイズとして定義されています。
  2. readおよびpread関数の変更: File.readおよびFile.preadメソッドにおいて、読み取りバッファbの長さがmaxRWを超える場合に、バッファをmaxRWの長さに切り詰める処理が追加されました。

    if needsMaxRW && len(b) > maxRW {
        b = b[:maxRW]
    }
    

    これにより、syscall.Readsyscall.Preadに渡されるバッファが常にOSの制限内に収まるようになります。

  3. write関数の変更: File.writeメソッドは、ループ内でsyscall.Writeを呼び出すことで、大きなデータを分割して書き込むロジックを持っています。この変更では、syscall.Writeに渡すバッファbcapを導入し、その長さがmaxRWを超える場合に切り詰めるようにしました。

    bcap := b
    if needsMaxRW && len(bcap) > maxRW {
        bcap = bcap[:maxRW]
    }
    m, err := syscall.Write(f.fd, bcap)
    

    さらに、書き込みが途中で中断された場合の再試行ロジックも調整されました。特に、needsMaxRWtrueで、かつbcapが元のbよりも短く切り詰められていたにもかかわらず、エラーがnilであった場合(つまり、切り詰められた部分が正常に書き込まれた場合)も、残りのデータを書き込むためにループを継続する条件が追加されました。

    if needsMaxRW && len(bcap) != len(b) && err == nil {
        b = b[m:]
        continue
    }
    

    これにより、大きなデータが複数のチャンクに分割されても、全体として正しく書き込まれることが保証されます。

  4. pwrite関数の変更: File.pwriteメソッドもreadと同様に、書き込みバッファbの長さがmaxRWを超える場合に、バッファをmaxRWの長さに切り詰める処理が追加されました。

    if needsMaxRW && len(b) > maxRW {
        b = b[:maxRW]
    }
    

これらの変更により、GoプログラムはDarwinおよびFreeBSD上で、2GBを超えるサイズのI/O操作を試みた場合でも、内部的に安全なチャンクに分割してシステムコールに渡すようになり、OSの制限に起因する問題を回避できるようになりました。

コアとなるコードの変更箇所

src/pkg/os/file_unix.go

--- a/src/pkg/os/file_unix.go
+++ b/src/pkg/os/file_unix.go
@@ -172,16 +172,29 @@ func (f *File) readdir(n int) (fi []FileInfo, err error) {
 	return fi, err
 }
 
+// Darwin and FreeBSD can't read or write 2GB+ at a time,
+// even on 64-bit systems. See golang.org/issue/7812.
+const (
+	needsMaxRW = runtime.GOOS == "darwin" || runtime.GOOS == "freebsd"
+	maxRW      = 2<<30 - 1
+)
+
 // read reads up to len(b) bytes from the File.
 // It returns the number of bytes read and an error, if any.
 func (f *File) read(b []byte) (n int, err error) {
+	if needsMaxRW && len(b) > maxRW {
+		b = b[:maxRW]
+	}
 	return syscall.Read(f.fd, b)
 }
 
 // pread reads len(b) bytes from the File starting at byte offset off.
 // It returns the number of bytes read and the error, if any.
-// EOF is signaled by a zero count with err set to 0.
+// EOF is signaled by a zero count with err set to nil.
 func (f *File) pread(b []byte, off int64) (n int, err error) {
+	if needsMaxRW && len(b) > maxRW {
+		b = b[:maxRW]
+	}
 	return syscall.Pread(f.fd, b, off)
 }
 
@@ -189,13 +202,22 @@ func (f *File) pread(b []byte, off int64) (n int, err error) {
 // It returns the number of bytes written and an error, if any.
 func (f *File) write(b []byte) (n int, err error) {
 	for {
-		m, err := syscall.Write(f.fd, b)
+		bcap := b
+		if needsMaxRW && len(bcap) > maxRW {
+			bcap = bcap[:maxRW]
+		}
+		m, err := syscall.Write(f.fd, bcap)
 		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 {
+		if 0 < m && m < len(bcap) || err == syscall.EINTR {
+			b = b[m:]
+			continue
+		}
+
+		if needsMaxRW && len(bcap) != len(b) && err == nil {
 			b = b[m:]
 			continue
 		}
@@ -207,6 +229,9 @@ func (f *File) write(b []byte) (n int, err error) {
 // pwrite writes len(b) bytes to the File starting at byte offset off.
 // It returns the number of bytes written and an error, if any.
 func (f *File) pwrite(b []byte, off int64) (n int, err error) {
+	if needsMaxRW && len(b) > maxRW {
+		b = b[:maxRW]
+	}
 	return syscall.Pwrite(f.fd, b, off)
 }
 

コアとなるコードの解説

1. needsMaxRWmaxRW 定数の定義

+const (
+	needsMaxRW = runtime.GOOS == "darwin" || runtime.GOOS == "freebsd"
+	maxRW      = 2<<30 - 1
+)

この部分で、I/Oサイズ制限が必要なOS(DarwinまたはFreeBSD)を識別するためのneedsMaxRWと、その最大サイズ(約2GB)を定義するmaxRWが導入されています。2<<30 - 1は、2^31 - 1をビットシフト演算で表現したもので、符号付き32ビット整数の最大値に相当します。

2. read および pread メソッドにおけるバッファの切り詰め

// read reads up to len(b) bytes from the File.
// It returns the number of bytes read and an error, if any.
func (f *File) read(b []byte) (n int, err error) {
+	if needsMaxRW && len(b) > maxRW {
+		b = b[:maxRW]
+	}
 	return syscall.Read(f.fd, b)
}

// pread reads len(b) bytes from the File starting at byte offset off.
// It returns the number of bytes read and the error, if any.
// EOF is signaled by a zero count with err set to nil.
func (f *File) pread(b []byte, off int64) (n int, err error) {
+	if needsMaxRW && len(b) > maxRW {
+		b = b[:maxRW]
+	}
 	return syscall.Pread(f.fd, b, off)
}

readpread関数では、needsMaxRWtrue(つまりDarwinまたはFreeBSD)で、かつ読み取りバッファbの長さがmaxRWを超える場合に、bmaxRWの長さにスライスして切り詰めています。これにより、syscall.Readsyscall.Preadには常にOSが処理できるサイズのバッファが渡されるようになります。

3. write メソッドにおけるバッファの切り詰めとループ継続条件の調整

// 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) {
	for {
+		bcap := b
+		if needsMaxRW && len(bcap) > maxRW {
+			bcap = bcap[:maxRW]
+		}
+		m, err := syscall.Write(f.fd, bcap)
		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 {
+		if 0 < m && m < len(bcap) || err == syscall.EINTR {
+			b = b[m:]
+			continue
+		}
+
+		if needsMaxRW && len(bcap) != len(b) && err == nil {
 			b = b[m:]
 			continue
 		}
		// ... (rest of the function)
	}
}

write関数は、大きなデータを複数回に分けて書き込むためのループを持っています。

  • まず、bcapという新しい変数に現在の書き込みバッファbを代入し、readpreadと同様にmaxRWで切り詰めます。syscall.Writeにはこのbcapが渡されます。
  • ループの継続条件が変更されました。以前は0 < m && m < len(b)(一部しか書き込まれなかった場合)でしたが、bcapを導入したことで0 < m && m < len(bcap)に変更されています。これは、syscall.Writeに渡されたbcapの範囲内でショートライトが発生した場合にループを継続するという意味です。
  • さらに重要な変更として、needsMaxRW && len(bcap) != len(b) && err == nilという新しい条件が追加されました。これは、needsMaxRWtrueで、かつ元のバッファbmaxRWで切り詰められた(len(bcap) != len(b))にもかかわらず、syscall.Writeがエラーなく完了した場合(err == nil)に、残りのデータ(b[m:])を書き込むためにループを継続するというものです。これにより、2GBを超えるデータが正しく分割されて書き込まれることが保証されます。

4. pwrite メソッドにおけるバッファの切り詰め

// pwrite writes len(b) bytes to the File starting at byte offset off.
// It returns the number of bytes written and an error, if any.
func (f *File) pwrite(b []byte, off int64) (n int, err error) {
+	if needsMaxRW && len(b) > maxRW {
+		b = b[:maxRW]
+	}
 	return syscall.Pwrite(f.fd, b, off)
}

pwrite関数もreadと同様に、書き込みバッファbの長さがmaxRWを超える場合に、バッファをmaxRWの長さにスライスして切り詰めています。

これらの変更により、Goのosパッケージは、DarwinおよびFreeBSD上での大容量I/O操作におけるOSレベルの制限を透過的に処理できるようになり、アプリケーション開発者はこれらのOS固有の制約を意識することなく、大きなデータを扱うことができるようになりました。

関連リンク

参考にした情報源リンク