[インデックス 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上でのread
、pread
、write
、pwrite
システムコール呼び出しにおいて、渡されるバッファサイズを2GB未満に制限する(キャップする)必要がありました。これにより、OSのシステムコールが処理できる範囲内で操作が行われるようになり、安定性と信頼性が向上します。
前提知識の解説
1. システムコール (syscall
パッケージ)
Go言語では、オペレーティングシステムの機能にアクセスするためにsyscall
パッケージを使用します。syscall.Read
やsyscall.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未満に制限するメカニズムの導入です。
-
定数の導入:
needsMaxRW
とmaxRW
という2つの新しい定数が導入されました。needsMaxRW
:runtime.GOOS
が"darwin"または"freebsd"の場合にtrue
となるブール定数です。これにより、特定のOSでのみ制限を適用するかどうかを効率的に判断できます。maxRW
:2<<30 - 1
、つまり2^31 - 1
(約2GB)に設定された整数定数です。これは、DarwinおよびFreeBSDが一度に処理できる最大I/Oサイズとして定義されています。
-
read
およびpread
関数の変更:File.read
およびFile.pread
メソッドにおいて、読み取りバッファb
の長さがmaxRW
を超える場合に、バッファをmaxRW
の長さに切り詰める処理が追加されました。if needsMaxRW && len(b) > maxRW { b = b[:maxRW] }
これにより、
syscall.Read
やsyscall.Pread
に渡されるバッファが常にOSの制限内に収まるようになります。 -
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)
さらに、書き込みが途中で中断された場合の再試行ロジックも調整されました。特に、
needsMaxRW
がtrue
で、かつbcap
が元のb
よりも短く切り詰められていたにもかかわらず、エラーがnil
であった場合(つまり、切り詰められた部分が正常に書き込まれた場合)も、残りのデータを書き込むためにループを継続する条件が追加されました。if needsMaxRW && len(bcap) != len(b) && err == nil { b = b[m:] continue }
これにより、大きなデータが複数のチャンクに分割されても、全体として正しく書き込まれることが保証されます。
-
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. needsMaxRW
と maxRW
定数の定義
+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)
}
read
とpread
関数では、needsMaxRW
がtrue
(つまりDarwinまたはFreeBSD)で、かつ読み取りバッファb
の長さがmaxRW
を超える場合に、b
をmaxRW
の長さにスライスして切り詰めています。これにより、syscall.Read
やsyscall.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
を代入し、read
やpread
と同様に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
という新しい条件が追加されました。これは、needsMaxRW
がtrue
で、かつ元のバッファb
がmaxRW
で切り詰められた(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固有の制約を意識することなく、大きなデータを扱うことができるようになりました。
関連リンク
- Go Issue #7812: https://golang.org/issue/7812
- Go Code Review 89900044: https://golang.org/cl/89900044
参考にした情報源リンク
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHHJExWO0tgTAS6wO5igd4-FghFRCWd00MQyb940A1Qd0Y6lmbSZ8tr6J3h2oW4yTf4PeLTH4-3WDahTJvqOT8qtQKXX1FfVnSfpYaJfRT9eZEfqmgledWykOgjysj79PQwUA==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHnp78ZxyzNyKer7tqZ7Dg6GxFUayQxjCqyp9_h7HeYoHXVuQXJGddBpvnWr6q-06BDrNqNG68gHoQEH18lWVWeSfR7G3hGCkzJIM4gd3IC8o7FJnZtLQb08_EbzqJ3HwYr71G6HtAeDEJI2KbvkuvA2l8MBZMu5Pw_XW8Q-_yB
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHB921e82orpTRQnrl9Hgp-hITnkBSxR5u46HyQA3VLWJLvXtU7nk2uOXzy7Nj79q5oWyxGoumbdw3hfmohDKgiIkOaVNB8jbi2LDxO522Us7i2rxJn5VNbXOIpcjW8QRXMyb_atG6VujHLcKW--w==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGO8cm_bjUzZiu2pnictUqxGaFE6SPindE3PLn8r64TfHCliHnRTvAy8_wzJpg81P-R5X8E1Jnd5ZW1ZS09bp1B90eo2NScX0r67nTzCsbca6t9Vk1Y8KmxlSOzZ-IbeDjgGXOu