[インデックス 1656] ファイルの概要
このコミットは、Go言語の os
パッケージにおけるディレクトリ読み取り機能、特に Readdirnames
関数が、macOS (Darwin) 環境で小さなチャンクで読み取られた場合に正しく動作しない問題を修正するものです。この修正は、ファイルディスクリプタ (FD) にディレクトリ読み取りの状態を保持するための新しい構造体 DirInfo
を導入し、バッファリングメカニズムを改善することで実現されています。
コミット
commit d94c5aba12f33b793234438c55daa0c33768711d
Author: Rob Pike <r@golang.org>
Date: Tue Feb 10 11:27:45 2009 -0800
Fix Readdirnames to behave properly if reading in little pieces. Requires storing some
state in the FD.
This is Darwin only. Next CL will make Readdir use Readdirnames to generate its files
and move Readdir into portable code, as well as fix Readdirnames for Linux.
R=rsc
DELTA=116 (79 added, 12 deleted, 25 changed)
OCL=24756
CL=24768
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d94c5aba12f33b793234438c55daa0c33768711d
元コミット内容
このコミットは、Readdirnames
関数が「小さな断片で読み取る」場合に適切に動作するように修正します。この修正には、ファイルディスクリプタ (FD) 内にいくつかの状態を保存することが必要です。
この変更はDarwin (macOS) 専用です。次の変更リスト (CL) では、Readdir
が Readdirnames
を使用してファイルを生成するように変更され、Readdir
がポータブルなコードに移行されるとともに、Linux向けの Readdirnames
も修正される予定です。
変更の背景
ディレクトリの内容を読み取る際、特に Readdirnames
のような関数では、一度にすべてのエントリを読み取るのではなく、一部ずつ(「小さな断片」で)読み取ることがあります。これは、メモリ効率や、大量のファイルを含むディレクトリを扱う際に重要になります。
しかし、従来の Readdirnames
の実装では、syscall.Getdirentries
システムコールがファイルオフセットをカーネルに記憶させることが困難であるという問題がありました。これは、Getdirentries
が呼び出されるたびに、読み取りを開始するオフセットを明示的に指定する必要があることを意味します。もし、アプリケーションがディレクトリを部分的に読み取り、その後続きを読み取ろうとすると、前回の読み取りがどこで終わったかという状態をアプリケーション側で管理する必要がありました。
特にDarwin環境では、このオフセット管理が不適切であったため、Readdirnames
が小さなチャンクで呼び出された場合に、ディレクトリの内容を正確に列挙できない、あるいは重複したりスキップしたりする可能性がありました。このコミットは、この問題を解決し、Readdirnames
が部分的な読み取りでも信頼性高く動作するようにするためのものです。
また、コミットメッセージには、将来的に Readdir
が Readdirnames
を利用するように変更され、よりポータブルな実装を目指すという意図も示されており、この修正はそのための前提条件となる重要なステップでした。
前提知識の解説
ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix系オペレーティングシステムにおいて、開かれたファイルやソケット、パイプなどのI/Oリソースを識別するために使用される抽象的なハンドルです。Go言語の os
パッケージでは、FD
型がこのファイルディスクリプタをラップし、ファイル操作のためのメソッドを提供します。
Readdirnames
と Readdir
Readdirnames(fd *FD, count int) ([]string, *os.Error)
: 指定されたファイルディスクリプタfd
が指すディレクトリから、count
個のディレクトリ内のエントリ名(ファイル名やディレクトリ名)を文字列スライスとして読み取ります。count
が負の場合、EOFに達するまですべてのエントリを読み取ります。Readdir(fd *FD, count int) ([]Dir, *os.Error)
:Readdirnames
と同様にディレクトリの内容を読み取りますが、こちらはDir
型のスライスを返します。Dir
型には、ファイル名だけでなく、ファイルの種類やパーミッションなどの詳細なファイル情報が含まれます。
syscall.Getdirentries
Getdirentries
は、Unix系システムコールの一つで、ディレクトリの内容を直接読み取るために使用されます。このシステムコールは、指定されたファイルディスクリプタからディレクトリのエントリをバッファに読み込み、読み込んだバイト数と次の読み取りを開始すべきオフセットを返します。
Darwin (macOS) における Getdirentries
のシグネチャは以下のようになります(Goの syscall
パッケージでのラップ後):
func Getdirentries(fd int64, buf *byte, nbytes int64, basep *int64) (ret int64, errno int64)
ここで、fd
はディレクトリのファイルディスクリプタ、buf
は読み込んだエントリを格納するバッファ、nbytes
はバッファのサイズ、basep
は読み取り開始オフセットへのポインタです。Getdirentries
は、読み込んだバイト数を ret
に、エラーコードを errno
に返します。重要なのは、basep
が入出力パラメータとして機能し、システムコールが完了した後に次の読み取りを開始すべきオフセットが更新される点です。
unsafe.Pointer
と syscall.Dirent
unsafe.Pointer
: Go言語において、任意の型のポインタを指すことができる特殊なポインタ型です。型安全性をバイパスするため、慎重に使用する必要があります。システムコールから返される生バイトデータを構造体にキャストする際などに用いられます。syscall.Dirent
:Getdirentries
システムコールによって読み取られたディレクトリのエントリ情報を格納する構造体です。この構造体には、inode番号 (Ino
)、エントリの長さ (Reclen
)、ファイル名 (Name
) などが含まれます。
ディレクトリ読み取りの課題
ディレクトリは、ファイルシステム上の特殊なファイルであり、その内容はファイルやサブディレクトリのエントリのリストです。これらのエントリは、固定長ではなく可変長である場合があり、また、ファイルシステムによっては、削除されたエントリの痕跡が残ることがあります(inode番号が0のエントリなど)。
Getdirentries
のようなシステムコールは、一度に複数のエントリを読み込むことができますが、バッファのサイズによっては、エントリが途中で切れてしまうこともあります。また、前述の通り、次の読み取りを開始するオフセットを正確に管理することが、ディレクトリの内容を漏れなく、重複なく読み取るために不可欠です。特に、部分的な読み取りを繰り返す場合、この状態管理が複雑になります。
技術的詳細
このコミットの主要な変更点は、os
パッケージの FD
型に DirInfo
という新しいフィールドを追加し、ディレクトリ読み取りの状態をファイルディスクリプタ自体に持たせるようにしたことです。これにより、Readdirnames
が複数回呼び出されても、前回の読み取り状態を適切に引き継ぐことができるようになります。
src/lib/os/os_file.go
の変更
-
DirInfo
構造体の追加:type DirInfo struct { // TODO(r): 6g bug means this can't be private buf []byte; // buffer for directory I/O nbuf int64; // length of buf; return value from Getdirentries bufp int64; // location of next record in buf. }
buf
:syscall.Getdirentries
から読み取った生データを格納するバッファ。nbuf
:buf
に現在格納されている有効なデータの長さ(Getdirentries
の戻り値)。bufp
:buf
内で次に処理すべきディレクトリレコードの開始位置(オフセット)。 この構造体は、ディレクトリ読み取りの「状態」をカプセル化します。
-
FD
構造体へのdirinfo
フィールドの追加:type FD struct { fd int64; name string; dirinfo *DirInfo; // nil unless directory being read }
FD
型に*DirInfo
型のdirinfo
フィールドが追加されました。これにより、ファイルディスクリプタがディレクトリを指している場合に、そのディレクトリの読み取り状態をFD
オブジェクト自体が保持できるようになります。dirinfo
がnil
の場合、そのFD
はディレクトリではないか、まだディレクトリとして読み取られていないことを意味します。 -
NewFD
関数の変更:NewFD
関数は、新しいFD
オブジェクトを生成する際に、dirinfo
フィールドをnil
で初期化するように変更されました。// 変更前: return &FD{fd, name} // 変更後: return &FD{fd, name, nil}
-
Seek
メソッドの追加:FD
型にSeek
メソッドが追加されました。これは、ファイルディスクリプタの読み書きオフセットを変更するものです。func (fd *FD) Seek(offset int64, whence int) (ret int64, err *Error) { r, e := syscall.Seek(fd.fd, offset, int64(whence)); if e != 0 { return -1, ErrnoToError(e) } if fd.dirinfo != nil && r != 0 { // ディレクトリの場合、オフセットが0以外へのシークは許可しない return -1, ErrnoToError(syscall.EISDIR) } return r, nil }
このメソッドは、ディレクトリの
FD
に対してSeek
が呼び出された場合、オフセットが0以外へのシークをEISDIR
エラーとして拒否します。これは、ディレクトリの読み取りはシーケンシャルに行われるべきであり、ランダムアクセスはサポートされないという設計思想を反映しています。
src/lib/os/dir_amd64_darwin.go
の変更
このファイルは、Darwin環境におけるディレクトリ操作の具体的な実装を含んでいます。
-
blockSize
定数の追加:const ( blockSize = 4096 // TODO(r): use statfs )
ディレクトリ読み取りバッファの初期サイズとして
4096
バイトが定義されました。コメントにあるように、将来的にはstatfs
システムコールを使用してファイルシステムのブロックサイズを動的に取得することが検討されています。 -
Readdirnames
関数の大幅な変更: この関数は、DirInfo
を利用してディレクトリ読み取りの状態を管理するように完全に書き直されました。fd.dirinfo
の初期化:Readdirnames
が初めて呼び出された際に、fd.dirinfo
がnil
であれば新しいDirInfo
オブジェクトが作成され、バッファ (d.buf
) がblockSize
で初期化されます。- バッファリングロジック:
d.bufp == d.nbuf
の場合(バッファが空になった場合)、syscall.Getdirentries
を呼び出してバッファを補充します。Getdirentries
の最後の引数basep
にはnew(int64)
を渡しています。これは、Getdirentries
が内部でファイルディスクリプタのオフセットを更新するため、Go側で明示的にオフセットを管理する必要がなくなったことを示唆しています(ただし、DarwinのGetdirentries
はbasep
を入出力パラメータとして使用するため、このnew(int64)
はそのためのプレースホルダとして機能します)。d.nbuf
が0
の場合、EOFに達したと判断してループを終了します。
- バッファからのエントリのドレイン: バッファにデータがある限り、
d.bufp
を進めながらsyscall.Dirent
構造体を解析し、ファイル名を取り出します。 dirent.Ino == 0
のエントリはスキップされます。これは、削除されたファイルや無効なエントリを示す可能性があります。count
が0
になると、要求された数のエントリを読み取ったとして処理を終了します。
-
Readdir
関数の変更:Readdir
関数も、バッファサイズにblockSize
を使用するように変更されました。// 変更前: var buf = make([]byte, 8192); // 変更後: var buf = make([]byte, blockSize);
また、
Stat
の代わりにLstat
を使用するように変更されました。Lstat
はシンボリックリンク自体を評価するのに対し、Stat
はシンボリックリンクの指す先のファイルを評価します。ディレクトリの内容を列挙する際には、シンボリックリンク自体を認識することが重要であるため、この変更は適切です。
src/lib/os/os_test.go
の変更
テストファイルには、Readdirnames
の修正を検証するための新しいテストケースが追加されました。
-
smallReaddirnames
ヘルパー関数の追加: この関数は、Readdirnames
をcount=1
で繰り返し呼び出し、ディレクトリを1エントリずつ読み取るシミュレーションを行います。func smallReaddirnames(fd *FD, length int, t *testing.T) []string { // ... for { d, err := Readdirnames(fd, 1); // 1エントリずつ読み取る // ... } // ... }
-
TestReaddirnamesOneAtATime
テストケースの追加: このテストは、smallReaddirnames
を使用してディレクトリを1エントリずつ読み取った結果と、Readdirnames
をcount=-1
で一度にすべて読み取った結果が一致するかどうかを検証します。これにより、Readdirnames
が部分的な読み取りでも正しく動作することが保証されます。func TestReaddirnamesOneAtATime(t *testing.T) { dir := "/usr/bin"; // 頻繁に変わらない大きなディレクトリ // ... all, err1 := Readdirnames(fd, -1); // 一度にすべて読み取る // ... small := smallReaddirnames(fd1, len(all)+100, t); // 1エントリずつ読み取る // ... for i, n := range all { if small[i] != n { t.Errorf("small read %q %q mismatch: %v\n", small[i], n); } } }
/usr/bin
のような大きなディレクトリを使用することで、多数のエントリを処理する際のロバスト性がテストされます。
src/lib/syscall/file_darwin.go
の変更
Getdirentries
システムコールのGoラッパーから、*basep = r2
の行が削除されました。
// 変更前:
// func Getdirentries(fd int64, buf *byte, nbytes int64, basep *int64) (ret int64, errno int64) {
// r1, r2, err := Syscall6(SYS_GETDIRENTRIES64, fd, int64(uintptr(unsafe.Pointer(buf))), nbytes, int64(uintptr(unsafe.Pointer(basep))), 0, 0);
// if r1 != -1 {
// *basep = r2
// }
// return r1, err;
// }
// 変更後:
func Getdirentries(fd int64, buf *byte, nbytes int64, basep *int64) (ret int64, errno int64) {
r1, r2, err := Syscall6(SYS_GETDIRENTRIES64, fd, int64(uintptr(unsafe.Pointer(buf))), nbytes, int64(uintptr(unsafe.Pointer(basep))), 0, 0);
return r1, err;
}
これは、Getdirentries
システムコール自体が basep
引数を介してオフセットを更新するため、Go側で明示的に r2
を *basep
に代入する必要がなくなったことを意味します。この変更は、システムコールのセマンティクスにより忠実になるように調整されたものです。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、src/lib/os/dir_amd64_darwin.go
の Readdirnames
関数と、src/lib/os/os_file.go
で定義された DirInfo
構造体および FD
構造体への dirinfo
フィールドの追加です。
src/lib/os/os_file.go
(抜粋)
// Auxiliary information if the FD describes a directory
type DirInfo struct { // TODO(r): 6g bug means this can't be private
buf []byte; // buffer for directory I/O
nbuf int64; // length of buf; return value from Getdirentries
bufp int64; // location of next record in buf.
}
// FDs are wrappers for file descriptors
type FD struct {
fd int64;
name string;
dirinfo *DirInfo; // nil unless directory being read
}
// ... (NewFD関数の変更)
func NewFD(fd int64, name string) *FD {
if fd < 0 {
return nil
}
return &FD{fd, name, nil} // dirinfoをnilで初期化
}
// ... (Seekメソッドの追加)
func (fd *FD) Seek(offset int64, whence int) (ret int64, err *Error) {
r, e := syscall.Seek(fd.fd, offset, int64(whence));
if e != 0 {
return -1, ErrnoToError(e)
}
if fd.dirinfo != nil && r != 0 {
return -1, ErrnoToError(syscall.EISDIR)
}
return r, nil
}
src/lib/os/dir_amd64_darwin.go
(抜粋)
const (
blockSize = 4096 // TODO(r): use statfs
)
// Negative count means read until EOF.
func Readdirnames(fd *FD, count int) (names []string, err *os.Error) {
// If this fd has no dirinfo, create one.
if fd.dirinfo == nil {
fd.dirinfo = new(DirInfo);
// The buffer must be at least a block long.
// TODO(r): use fstatfs to find fs block size.
fd.dirinfo.buf = make([]byte, blockSize);
}
d := fd.dirinfo;
size := count;
if size < 0 {
size = 100
}
names = make([]string, 0, size); // Empty with room to grow.
for count != 0 {
// Refill the buffer if necessary
if d.bufp == d.nbuf {
var errno int64;
// Final argument is (basep *int64) and the syscall doesn't take nil.
d.nbuf, errno = syscall.Getdirentries(fd.fd, &d.buf[0], int64(len(d.buf)), new(int64));
if d.nbuf < 0 {
return names, os.ErrnoToError(errno)
}
if d.nbuf == 0 {
break // EOF
}
d.bufp = 0;
}
// Drain the buffer
for count != 0 && d.bufp < d.nbuf {
dirent := unsafe.Pointer(&d.buf[d.bufp]).(*syscall.Dirent);
d.bufp += int64(dirent.Reclen);
if dirent.Ino == 0 { // File absent in directory.
continue
}
count--;
// ... (ファイル名抽出ロジック)
names = names[0:len(names)+1];
names[len(names)-1] = string(dirent.Name[0:dirent.Namlen]);
}
}
return names, nil
}
コアとなるコードの解説
このコミットの核心は、ディレクトリ読み取りの「状態」を FD
オブジェクト自体に持たせることで、Readdirnames
が複数回呼び出されても、前回の読み取りの続きから正確に処理できるようにした点です。
-
DirInfo
構造体による状態管理:DirInfo
は、ディレクトリ読み取りに必要なすべての状態を保持します。buf
:syscall.Getdirentries
から読み込んだ生データを一時的に保持するバッファです。システムコールは一度に複数のディレクトリエントリを返すことがありますが、アプリケーションが要求するエントリ数よりも多くのエントリがバッファに読み込まれる可能性があります。このバッファがその余剰データを保持します。nbuf
:buf
に現在格納されている有効なデータのバイト数です。これはGetdirentries
の戻り値ret
に相当します。bufp
:buf
内で次に処理すべきディレクトリエントリの開始オフセットです。Readdirnames
がエントリを一つずつ処理するたびに、このbufp
が更新されます。
-
FD
へのdirinfo
の追加:FD
はファイルディスクリプタのラッパーであり、開かれたファイルやディレクトリを表します。dirinfo *DirInfo
フィールドが追加されたことで、FD
オブジェクトは自身が指すディレクトリの読み取り状態を直接管理できるようになりました。これにより、Readdirnames
がFD
を引数として受け取るだけで、そのFD
に関連付けられたディレクトリ読み取りの進行状況を把握し、操作できるようになります。 -
Readdirnames
のバッファリングロジック:- 初回呼び出し時の初期化:
Readdirnames
が特定のFD
に対して初めて呼び出される際、fd.dirinfo
がnil
であることをチェックし、新しいDirInfo
オブジェクトを作成してfd.dirinfo
に割り当てます。このとき、buf
もblockSize
(4096バイト) で初期化されます。 - バッファの補充 (
if d.bufp == d.nbuf
):d.bufp
(バッファ内の現在の読み取り位置) がd.nbuf
(バッファ内の有効なデータの終端) と等しい場合、これはバッファ内のすべてのエントリが処理されたことを意味します。このとき、syscall.Getdirentries
を呼び出して、カーネルから新しいディレクトリエントリのチャンクをd.buf
に読み込みます。Getdirentries
の最後の引数new(int64)
は、システムコールが内部でファイルディスクリプタのオフセットを管理し、次の読み取り位置を自動的に更新することを示唆しています。Go側では、このオフセットを明示的に追跡する必要がなくなりました。d.nbuf
が0
の場合、ディレクトリの終端 (EOF) に達したと判断し、ループを終了します。 バッファが補充されたら、d.bufp
を0
にリセットし、バッファの先頭から新しいエントリの処理を開始します。 - バッファからのエントリのドレイン (
for count != 0 && d.bufp < d.nbuf
): バッファにデータがあり、かつ要求されたエントリ数 (count
) がまだ残っている限り、ループを続けます。unsafe.Pointer(&d.buf[d.bufp]).(*syscall.Dirent)
を使用して、バッファ内の生バイトデータをsyscall.Dirent
構造体にキャストします。これにより、個々のディレクトリエントリの情報をGoの構造体としてアクセスできるようになります。d.bufp += int64(dirent.Reclen)
で、現在のエントリの長さ (dirent.Reclen
) だけbufp
を進め、次のエントリの開始位置を指すようにします。dirent.Ino == 0
のエントリは、ファイルシステム上で無効または削除されたエントリを示すため、スキップされます。 有効なエントリが見つかったらcount--
して、要求されたエントリ数を減らします。 最後に、dirent.Name
からファイル名を抽出し、結果のスライスnames
に追加します。
- 初回呼び出し時の初期化:
-
Seek
メソッドの挙動:FD
に追加されたSeek
メソッドは、ディレクトリのFD
に対してオフセットが0
以外へのシークを禁止します。これは、ディレクトリの読み取りがシーケンシャルな操作であり、ランダムアクセスはサポートされないというUnixの一般的な慣習に従うものです。もしディレクトリの途中にシークしようとすると、EISDIR
(Is a directory) エラーが返されます。
これらの変更により、Readdirnames
は、たとえ小さなチャンクで繰り返し呼び出されたとしても、FD
オブジェクトに保持された DirInfo
を介して自身の状態を正確に管理し、ディレクトリの内容を完全に、かつ正しく列挙できるようになりました。これは、特にmacOS環境におけるディレクトリ読み取りの信頼性を大幅に向上させるものです。
関連リンク
- Go言語の
os
パッケージに関する公式ドキュメント: https://pkg.go.dev/os - Go言語の
syscall
パッケージに関する公式ドキュメント: https://pkg.go.dev/syscall - Darwin (macOS) の
getdirentries
システムコールに関するmanページ (オンラインリソース): https://man.freebsd.org/cgi/man.cgi?query=getdirentries&sektion=2 (FreeBSDのmanページですが、Darwinも類似のセマンティクスを持ちます)
参考にした情報源リンク
- Go言語の初期のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Unix系システムにおけるディレクトリ読み取りのメカニズムに関する一般的な情報源 (例:
opendir
,readdir
,closedir
の概念) - Go言語の
unsafe
パッケージの利用に関する情報 - Go言語のテストフレームワーク
testing
パッケージに関する情報 statfs
およびfstatfs
システムコールに関する情報 (ファイルシステムのブロックサイズ取得のため)The previous tool call failed because the path was relative. I corrected it to an absolute path and successfully read the file. Now I will generate the detailed explanation as requested.
I have gathered all the necessary information from the commit and my knowledge base. I will now output the comprehensive technical explanation in Markdown format, adhering to the specified chapter structure.
# [インデックス 1656] ファイルの概要
このコミットは、Go言語の `os` パッケージにおけるディレクトリ読み取り機能、特に `Readdirnames` 関数が、macOS (Darwin) 環境で小さなチャンクで読み取られた場合に正しく動作しない問題を修正するものです。この修正は、ファイルディスクリプタ (FD) にディレクトリ読み取りの状態を保持するための新しい構造体 `DirInfo` を導入し、バッファリングメカニズムを改善することで実現されています。
## コミット
commit d94c5aba12f33b793234438c55daa0c33768711d Author: Rob Pike r@golang.org Date: Tue Feb 10 11:27:45 2009 -0800
Fix Readdirnames to behave properly if reading in little pieces. Requires storing some
state in the FD.
This is Darwin only. Next CL will make Readdir use Readdirnames to generate its files
and move Readdir into portable code, as well as fix Readdirnames for Linux.
R=rsc
DELTA=116 (79 added, 12 deleted, 25 changed)
OCL=24756
CL=24768
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/d94c5aba12f33b793234438c55daa0c33768711d](https://github.com/golang/go/commit/d94c5aba12f33b793234438c55daa0c33768711d)
## 元コミット内容
このコミットは、`Readdirnames` 関数が「小さな断片で読み取る」場合に適切に動作するように修正します。この修正には、ファイルディスクリプタ (FD) 内にいくつかの状態を保存することが必要です。
この変更はDarwin (macOS) 専用です。次の変更リスト (CL) では、`Readdir` が `Readdirnames` を使用してファイルを生成するように変更され、`Readdir` がポータブルなコードに移行されるとともに、Linux向けの `Readdirnames` も修正される予定です。
## 変更の背景
ディレクトリの内容を読み取る際、特に `Readdirnames` のような関数では、一度にすべてのエントリを読み取るのではなく、一部ずつ(「小さな断片」で)読み取ることがあります。これは、メモリ効率や、大量のファイルを含むディレクトリを扱う際に重要になります。
しかし、従来の `Readdirnames` の実装では、`syscall.Getdirentries` システムコールがファイルオフセットをカーネルに記憶させることが困難であるという問題がありました。これは、`Getdirentries` が呼び出されるたびに、読み取りを開始するオフセットを明示的に指定する必要があることを意味します。もし、アプリケーションがディレクトリを部分的に読み取り、その後続きを読み取ろうとすると、前回の読み取りがどこで終わったかという状態をアプリケーション側で管理する必要がありました。
特にDarwin環境では、このオフセット管理が不適切であったため、`Readdirnames` が小さなチャンクで呼び出された場合に、ディレクトリの内容を正確に列挙できない、あるいは重複したりスキップしたりする可能性がありました。このコミットは、この問題を解決し、`Readdirnames` が部分的な読み取りでも信頼性高く動作するようにするためのものです。
また、コミットメッセージには、将来的に `Readdir` が `Readdirnames` を利用するように変更され、よりポータブルな実装を目指すという意図も示されており、この修正はそのための前提条件となる重要なステップでした。
## 前提知識の解説
### ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix系オペレーティングシステムにおいて、開かれたファイルやソケット、パイプなどのI/Oリソースを識別するために使用される抽象的なハンドルです。Go言語の `os` パッケージでは、`FD` 型がこのファイルディスクリプタをラップし、ファイル操作のためのメソッドを提供します。
### `Readdirnames` と `Readdir`
* **`Readdirnames(fd *FD, count int) ([]string, *os.Error)`**: 指定されたファイルディスクリプタ `fd` が指すディレクトリから、`count` 個のディレクトリ内のエントリ名(ファイル名やディレクトリ名)を文字列スライスとして読み取ります。`count` が負の場合、EOFに達するまですべてのエントリを読み取ります。
* **`Readdir(fd *FD, count int) ([]Dir, *os.Error)`**: `Readdirnames` と同様にディレクトリの内容を読み取りますが、こちらは `Dir` 型のスライスを返します。`Dir` 型には、ファイル名だけでなく、ファイルの種類やパーミッションなどの詳細なファイル情報が含まれます。
### `syscall.Getdirentries`
`Getdirentries` は、Unix系システムコールの一つで、ディレクトリの内容を直接読み取るために使用されます。このシステムコールは、指定されたファイルディスクリプタからディレクトリのエントリをバッファに読み込み、読み込んだバイト数と次の読み取りを開始すべきオフセットを返します。
Darwin (macOS) における `Getdirentries` のシグネチャは以下のようになります(Goの `syscall` パッケージでのラップ後):
`func Getdirentries(fd int64, buf *byte, nbytes int64, basep *int64) (ret int64, errno int64)`
ここで、`fd` はディレクトリのファイルディスクリプタ、`buf` は読み込んだエントリを格納するバッファ、`nbytes` はバッファのサイズ、`basep` は読み取り開始オフセットへのポインタです。`Getdirentries` は、読み込んだバイト数を `ret` に、エラーコードを `errno` に返します。重要なのは、`basep` が入出力パラメータとして機能し、システムコールが完了した後に次の読み取りを開始すべきオフセットが更新される点です。
### `unsafe.Pointer` と `syscall.Dirent`
* **`unsafe.Pointer`**: Go言語において、任意の型のポインタを指すことができる特殊なポインタ型です。型安全性をバイパスするため、慎重に使用する必要があります。システムコールから返される生バイトデータを構造体にキャストする際などに用いられます。
* **`syscall.Dirent`**: `Getdirentries` システムコールによって読み取られたディレクトリのエントリ情報を格納する構造体です。この構造体には、inode番号 (`Ino`)、エントリの長さ (`Reclen`)、ファイル名 (`Name`) などが含まれます。
### ディレクトリ読み取りの課題
ディレクトリは、ファイルシステム上の特殊なファイルであり、その内容はファイルやサブディレクトリのエントリのリストです。これらのエントリは、固定長ではなく可変長である場合があり、また、ファイルシステムによっては、削除されたエントリの痕跡が残ることがあります(inode番号が0のエントリなど)。
`Getdirentries` のようなシステムコールは、一度に複数のエントリを読み込むことができますが、バッファのサイズによっては、エントリが途中で切れてしまうこともあります。また、前述の通り、次の読み取りを開始するオフセットを正確に管理することが、ディレクトリの内容を漏れなく、重複なく読み取るために不可欠です。特に、部分的な読み取りを繰り返す場合、この状態管理が複雑になります。
## 技術的詳細
このコミットの主要な変更点は、`os` パッケージの `FD` 型に `DirInfo` という新しいフィールドを追加し、ディレクトリ読み取りの状態をファイルディスクリプタ自体に持たせるようにしたことです。これにより、`Readdirnames` が複数回呼び出されても、前回の読み取り状態を適切に引き継ぐことができるようになります。
### `src/lib/os/os_file.go` の変更
1. **`DirInfo` 構造体の追加**:
```go
type DirInfo struct { // TODO(r): 6g bug means this can't be private
buf []byte; // buffer for directory I/O
nbuf int64; // length of buf; return value from Getdirentries
bufp int64; // location of next record in buf.
}
```
* `buf`: `syscall.Getdirentries` から読み取った生データを格納するバッファ。
* `nbuf`: `buf` に現在格納されている有効なデータの長さ(`Getdirentries` の戻り値)。
* `bufp`: `buf` 内で次に処理すべきディレクトリレコードの開始位置(オフセット)。
この構造体は、ディレクトリ読み取りの「状態」をカプセル化します。
2. **`FD` 構造体への `dirinfo` フィールドの追加**:
```go
type FD struct {
fd int64;
name string;
dirinfo *DirInfo; // nil unless directory being read
}
```
`FD` 型に `*DirInfo` 型の `dirinfo` フィールドが追加されました。これにより、ファイルディスクリプタがディレクトリを指している場合に、そのディレクトリの読み取り状態を `FD` オブジェクト自体が保持できるようになります。`dirinfo` が `nil` の場合、その `FD` はディレクトリではないか、まだディレクトリとして読み取られていないことを意味します。
3. **`NewFD` 関数の変更**:
`NewFD` 関数は、新しい `FD` オブジェクトを生成する際に、`dirinfo` フィールドを `nil` で初期化するように変更されました。
```go
// 変更前: return &FD{fd, name}
// 変更後: return &FD{fd, name, nil}
```
4. **`Seek` メソッドの追加**:
`FD` 型に `Seek` メソッドが追加されました。これは、ファイルディスクリプタの読み書きオフセットを変更するものです。
```go
func (fd *FD) Seek(offset int64, whence int) (ret int64, err *Error) {
r, e := syscall.Seek(fd.fd, offset, int64(whence));
if e != 0 {
return -1, ErrnoToError(e)
}
if fd.dirinfo != nil && r != 0 { // ディレクトリの場合、オフセットが0以外へのシークは許可しない
return -1, ErrnoToError(syscall.EISDIR)
}
return r, nil
}
```
このメソッドは、ディレクトリの `FD` に対して `Seek` が呼び出された場合、オフセットが0以外へのシークを `EISDIR` エラーとして拒否します。これは、ディレクトリの読み取りはシーケンシャルに行われるべきであり、ランダムアクセスはサポートされないという設計思想を反映しています。
### `src/lib/os/dir_amd64_darwin.go` の変更
このファイルは、Darwin環境におけるディレクトリ操作の具体的な実装を含んでいます。
1. **`blockSize` 定数の追加**:
```go
const (
blockSize = 4096 // TODO(r): use statfs
)
```
ディレクトリ読み取りバッファの初期サイズとして `4096` バイトが定義されました。コメントにあるように、将来的には `statfs` システムコールを使用してファイルシステムのブロックサイズを動的に取得することが検討されています。
2. **`Readdirnames` 関数の大幅な変更**:
この関数は、`DirInfo` を利用してディレクトリ読み取りの状態を管理するように完全に書き直されました。
* **`fd.dirinfo` の初期化**: `Readdirnames` が初めて呼び出された際に、`fd.dirinfo` が `nil` であれば新しい `DirInfo` オブジェクトが作成され、バッファ (`d.buf`) が `blockSize` で初期化されます。
* **バッファリングロジック**:
* `d.bufp == d.nbuf` の場合(バッファが空になった場合)、`syscall.Getdirentries` を呼び出してバッファを補充します。
* `Getdirentries` の最後の引数には `new(int64)` を渡しています。これは、`Getdirentries` が内部でファイルディスクリプタのオフセットを更新するため、Go側で明示的にオフセットを管理する必要がなくなったことを示唆しています(ただし、Darwinの `Getdirentries` は `basep` を入出力パラメータとして使用するため、この `new(int64)` はそのためのプレースホルダとして機能します)。
* `d.nbuf` が `0` の場合、EOFに達したと判断してループを終了します。
* `d.bufp` を `0` にリセットし、バッファの先頭から新しいエントリの処理を開始します。
* **バッファからのエントリのドレイン**: バッファにデータがある限り、`d.bufp` を進めながら `syscall.Dirent` 構造体を解析し、ファイル名を取り出します。
* `dirent.Ino == 0` のエントリはスキップされます。これは、削除されたファイルや無効なエントリを示す可能性があります。
* `count` が `0` になると、要求された数のエントリを読み取ったとして処理を終了します。
3. **`Readdir` 関数の変更**:
`Readdir` 関数も、バッファサイズに `blockSize` を使用するように変更されました。
```go
// 変更前: var buf = make([]byte, 8192);
// 変更後: var buf = make([]byte, blockSize);
```
また、`Stat` の代わりに `Lstat` を使用するように変更されました。`Lstat` はシンボリックリンク自体を評価するのに対し、`Stat` はシンボリックリンクの指す先のファイルを評価します。ディレクトリの内容を列挙する際には、シンボリックリンク自体を認識することが重要であるため、この変更は適切です。
### `src/lib/os/os_test.go` の変更
テストファイルには、`Readdirnames` の修正を検証するための新しいテストケースが追加されました。
1. **`smallReaddirnames` ヘルパー関数の追加**:
この関数は、`Readdirnames` を `count=1` で繰り返し呼び出し、ディレクトリを1エントリずつ読み取るシミュレーションを行います。
```go
func smallReaddirnames(fd *FD, length int, t *testing.T) []string {
// ...
for {
d, err := Readdirnames(fd, 1); // 1エントリずつ読み取る
// ...
}
// ...
}
```
2. **`TestReaddirnamesOneAtATime` テストケースの追加**:
このテストは、`smallReaddirnames` を使用してディレクトリを1エントリずつ読み取った結果と、`Readdirnames` を `count=-1` で一度にすべて読み取った結果が一致するかどうかを検証します。これにより、`Readdirnames` が部分的な読み取りでも正しく動作することが保証されます。
```go
func TestReaddirnamesOneAtATime(t *testing.T) {
dir := "/usr/bin"; // 頻繁に変わらない大きなディレクトリ
// ...
all, err1 := Readdirnames(fd, -1); // 一度にすべて読み取る
// ...
small := smallReaddirnames(fd1, len(all)+100, t); // 1エントリずつ読み取る
// ...
for i, n := range all {
if small[i] != n {
t.Errorf("small read %q %q mismatch: %v\n", small[i], n);
}
}
}
```
`/usr/bin` のような大きなディレクトリを使用することで、多数のエントリを処理する際のロバスト性がテストされます。
### `src/lib/syscall/file_darwin.go` の変更
`Getdirentries` システムコールのGoラッパーから、`*basep = r2` の行が削除されました。
```go
// 変更前:
// func Getdirentries(fd int64, buf *byte, nbytes int64, basep *int64) (ret int64, errno int64) {
// r1, r2, err := Syscall6(SYS_GETDIRENTRIES64, fd, int64(uintptr(unsafe.Pointer(buf))), nbytes, int64(uintptr(unsafe.Pointer(basep))), 0, 0);
// if r1 != -1 {
// *basep = r2
// }
// return r1, err;
// }
// 変更後:
func Getdirentries(fd int64, buf *byte, nbytes int64, basep *int64) (ret int64, errno int64) {
r1, r2, err := Syscall6(SYS_GETDIRENTRIES64, fd, int64(uintptr(unsafe.Pointer(buf))), nbytes, int64(uintptr(unsafe.Pointer(basep))), 0, 0);
return r1, err;
}
これは、Getdirentries
システムコール自体が basep
引数を介してオフセットを更新するため、Go側で明示的に r2
を *basep
に代入する必要がなくなったことを意味します。この変更は、システムコールのセマンティクスにより忠実になるように調整されたものです。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、src/lib/os/dir_amd64_darwin.go
の Readdirnames
関数と、src/lib/os/os_file.go
で定義された DirInfo
構造体および FD
構造体への dirinfo
フィールドの追加です。
src/lib/os/os_file.go
(抜粋)
// Auxiliary information if the FD describes a directory
type DirInfo struct { // TODO(r): 6g bug means this can't be private
buf []byte; // buffer for directory I/O
nbuf int64; // length of buf; return value from Getdirentries
bufp int64; // location of next record in buf.
}
// FDs are wrappers for file descriptors
type FD struct {
fd int64;
name string;
dirinfo *DirInfo; // nil unless directory being read
}
// ... (NewFD関数の変更)
func NewFD(fd int64, name string) *FD {
if fd < 0 {
return nil
}
return &FD{fd, name, nil} // dirinfoをnilで初期化
}
// ... (Seekメソッドの追加)
func (fd *FD) Seek(offset int64, whence int) (ret int64, err *Error) {
r, e := syscall.Seek(fd.fd, offset, int64(whence));
if e != 0 {
return -1, ErrnoToError(e)
}
if fd.dirinfo != nil && r != 0 {
return -1, ErrnoToError(syscall.EISDIR)
}
return r, nil
}
src/lib/os/dir_amd64_darwin.go
(抜粋)
const (
blockSize = 4096 // TODO(r): use statfs
)
// Negative count means read until EOF.
func Readdirnames(fd *FD, count int) (names []string, err *os.Error) {
// If this fd has no dirinfo, create one.
if fd.dirinfo == nil {
fd.dirinfo = new(DirInfo);
// The buffer must be at least a block long.
// TODO(r): use fstatfs to find fs block size.
fd.dirinfo.buf = make([]byte, blockSize);
}
d := fd.dirinfo;
size := count;
if size < 0 {
size = 100
}
names = make([]string, 0, size); // Empty with room to grow.
for count != 0 {
// Refill the buffer if necessary
if d.bufp == d.nbuf {
var errno int64;
// Final argument is (basep *int64) and the syscall doesn't take nil.
d.nbuf, errno = syscall.Getdirentries(fd.fd, &d.buf[0], int64(len(d.buf)), new(int64));
if d.nbuf < 0 {
return names, os.ErrnoToError(errno)
}
if d.nbuf == 0 {
break // EOF
}
d.bufp = 0;
}
// Drain the buffer
for count != 0 && d.bufp < d.nbuf {
dirent := unsafe.Pointer(&d.buf[d.bufp]).(*syscall.Dirent);
d.bufp += int64(dirent.Reclen);
if dirent.Ino == 0 { // File absent in directory.
continue
}
count--;
// ... (ファイル名抽出ロジック)
names = names[0:len(names)+1];
names[len(names)-1] = string(dirent.Name[0:dirent.Namlen]);
}
}
return names, nil
}
コアとなるコードの解説
このコミットの核心は、ディレクトリ読み取りの「状態」を FD
オブジェクト自体に持たせることで、Readdirnames
が複数回呼び出されても、前回の読み取りの続きから正確に処理できるようにした点です。
-
DirInfo
構造体による状態管理:DirInfo
は、ディレクトリ読み取りに必要なすべての状態を保持します。buf
:syscall.Getdirentries
から読み込んだ生データを一時的に保持するバッファです。システムコールは一度に複数のディレクトリエントリを返すことがありますが、アプリケーションが要求するエントリ数よりも多くのエントリがバッファに読み込まれる可能性があります。このバッファがその余剰データを保持します。nbuf
:buf
に現在格納されている有効なデータのバイト数です。これはGetdirentries
の戻り値ret
に相当します。bufp
:buf
内で次に処理すべきディレクトリエントリの開始オフセットです。Readdirnames
がエントリを一つずつ処理するたびに、このbufp
が更新されます。
-
FD
へのdirinfo
の追加:FD
はファイルディスクリプタのラッパーであり、開かれたファイルやディレクトリを表します。dirinfo *DirInfo
フィールドが追加されたことで、FD
オブジェクトは自身が指すディレクトリの読み取り状態を直接管理できるようになりました。これにより、Readdirnames
がFD
を引数として受け取るだけで、そのFD
に関連付けられたディレクトリ読み取りの進行状況を把握し、操作できるようになります。 -
Readdirnames
のバッファリングロジック:- 初回呼び出し時の初期化:
Readdirnames
が特定のFD
に対して初めて呼び出される際、fd.dirinfo
がnil
であることをチェックし、新しいDirInfo
オブジェクトを作成してfd.dirinfo
に割り当てます。このとき、buf
もblockSize
(4096バイト) で初期化されます。 - バッファの補充 (
if d.bufp == d.nbuf
):d.bufp
(バッファ内の現在の読み取り位置) がd.nbuf
(バッファ内の有効なデータの終端) と等しい場合、これはバッファ内のすべてのエントリが処理されたことを意味します。このとき、syscall.Getdirentries
を呼び出して、カーネルから新しいディレクトリエントリのチャンクをd.buf
に読み込みます。Getdirentries
の最後の引数new(int64)
は、システムコールが内部でファイルディスクリプタのオフセットを管理し、次の読み取り位置を自動的に更新することを示唆しています。Go側では、このオフセットを明示的に追跡する必要がなくなりました。d.nbuf
が0
の場合、ディレクトリの終端 (EOF) に達したと判断し、ループを終了します。 バッファが補充されたら、d.bufp
を0
にリセットし、バッファの先頭から新しいエントリの処理を開始します。 - バッファからのエントリのドレイン (
for count != 0 && d.bufp < d.nbuf
): バッファにデータがあり、かつ要求されたエントリ数 (count
) がまだ残っている限り、ループを続けます。unsafe.Pointer(&d.buf[d.bufp]).(*syscall.Dirent)
を使用して、バッファ内の生バイトデータをsyscall.Dirent
構造体にキャストします。これにより、個々のディレクトリエントリの情報をGoの構造体としてアクセスできるようになります。d.bufp += int64(dirent.Reclen)
で、現在のエントリの長さ (dirent.Reclen
) だけbufp
を進め、次のエントリの開始位置を指すようにします。dirent.Ino == 0
のエントリは、ファイルシステム上で無効または削除されたエントリを示すため、スキップされます。 有効なエントリが見つかったらcount--
して、要求されたエントリ数を減らします。 最後に、dirent.Name
からファイル名を抽出し、結果のスライスnames
に追加します。
- 初回呼び出し時の初期化:
-
Seek
メソッドの挙動:FD
に追加されたSeek
メソッドは、ディレクトリのFD
に対してオフセットが0
以外へのシークを禁止します。これは、ディレクトリの読み取りがシーケンシャルな操作であり、ランダムアクセスはサポートされないというUnixの一般的な慣習に従うものです。もしディレクトリの途中にシークしようとすると、EISDIR
(Is a directory) エラーが返されます。
これらの変更により、Readdirnames
は、たとえ小さなチャンクで繰り返し呼び出されたとしても、FD
オブジェクトに保持された DirInfo
を介して自身の状態を正確に管理し、ディレクトリの内容を完全に、かつ正しく列挙できるようになりました。これは、特にmacOS環境におけるディレクトリ読み取りの信頼性を大幅に向上させるものです。
関連リンク
- Go言語の
os
パッケージに関する公式ドキュメント: https://pkg.go.dev/os - Go言語の
syscall
パッケージに関する公式ドキュメント: https://pkg.go.dev/syscall - Darwin (macOS) の
getdirentries
システムコールに関するmanページ (オンラインリソース): https://man.freebsd.org/cgi/man.cgi?query=getdirentries&sektion=2 (FreeBSDのmanページですが、Darwinも類似のセマンティクスを持ちます)
参考にした情報源リンク
- Go言語の初期のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Unix系システムにおけるディレクトリ読み取りのメカニズムに関する一般的な情報源 (例:
opendir
,readdir
,closedir
の概念) - Go言語の
unsafe
パッケージの利用に関する情報 - Go言語のテストフレームワーク
testing
パッケージに関する情報 statfs
およびfstatfs
システムコールに関する情報 (ファイルシステムのブロックサイズ取得のため)