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

[インデックス 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) では、ReaddirReaddirnames を使用してファイルを生成するように変更され、Readdir がポータブルなコードに移行されるとともに、Linux向けの Readdirnames も修正される予定です。

変更の背景

ディレクトリの内容を読み取る際、特に Readdirnames のような関数では、一度にすべてのエントリを読み取るのではなく、一部ずつ(「小さな断片」で)読み取ることがあります。これは、メモリ効率や、大量のファイルを含むディレクトリを扱う際に重要になります。

しかし、従来の Readdirnames の実装では、syscall.Getdirentries システムコールがファイルオフセットをカーネルに記憶させることが困難であるという問題がありました。これは、Getdirentries が呼び出されるたびに、読み取りを開始するオフセットを明示的に指定する必要があることを意味します。もし、アプリケーションがディレクトリを部分的に読み取り、その後続きを読み取ろうとすると、前回の読み取りがどこで終わったかという状態をアプリケーション側で管理する必要がありました。

特にDarwin環境では、このオフセット管理が不適切であったため、Readdirnames が小さなチャンクで呼び出された場合に、ディレクトリの内容を正確に列挙できない、あるいは重複したりスキップしたりする可能性がありました。このコミットは、この問題を解決し、Readdirnames が部分的な読み取りでも信頼性高く動作するようにするためのものです。

また、コミットメッセージには、将来的に ReaddirReaddirnames を利用するように変更され、よりポータブルな実装を目指すという意図も示されており、この修正はそのための前提条件となる重要なステップでした。

前提知識の解説

ファイルディスクリプタ (File Descriptor, FD)

ファイルディスクリプタは、Unix系オペレーティングシステムにおいて、開かれたファイルやソケット、パイプなどのI/Oリソースを識別するために使用される抽象的なハンドルです。Go言語の os パッケージでは、FD 型がこのファイルディスクリプタをラップし、ファイル操作のためのメソッドを提供します。

ReaddirnamesReaddir

  • 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.Pointersyscall.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 構造体の追加:

    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 フィールドの追加:

    type FD struct {
        fd int64;
        name string;
        dirinfo *DirInfo; // nil unless directory being read
    }
    

    FD 型に *DirInfo 型の dirinfo フィールドが追加されました。これにより、ファイルディスクリプタがディレクトリを指している場合に、そのディレクトリの読み取り状態を FD オブジェクト自体が保持できるようになります。dirinfonil の場合、その FD はディレクトリではないか、まだディレクトリとして読み取られていないことを意味します。

  3. NewFD 関数の変更: NewFD 関数は、新しい FD オブジェクトを生成する際に、dirinfo フィールドを nil で初期化するように変更されました。

    // 変更前: return &FD{fd, name}
    // 変更後: return &FD{fd, name, nil}
    
  4. 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環境におけるディレクトリ操作の具体的な実装を含んでいます。

  1. blockSize 定数の追加:

    const (
        blockSize = 4096 // TODO(r): use statfs
    )
    

    ディレクトリ読み取りバッファの初期サイズとして 4096 バイトが定義されました。コメントにあるように、将来的には statfs システムコールを使用してファイルシステムのブロックサイズを動的に取得することが検討されています。

  2. Readdirnames 関数の大幅な変更: この関数は、DirInfo を利用してディレクトリ読み取りの状態を管理するように完全に書き直されました。

    • fd.dirinfo の初期化: Readdirnames が初めて呼び出された際に、fd.dirinfonil であれば新しい DirInfo オブジェクトが作成され、バッファ (d.buf) が blockSize で初期化されます。
    • バッファリングロジック:
      • d.bufp == d.nbuf の場合(バッファが空になった場合)、syscall.Getdirentries を呼び出してバッファを補充します。
      • Getdirentries の最後の引数 basep には new(int64) を渡しています。これは、Getdirentries が内部でファイルディスクリプタのオフセットを更新するため、Go側で明示的にオフセットを管理する必要がなくなったことを示唆しています(ただし、Darwinの Getdirentriesbasep を入出力パラメータとして使用するため、この new(int64) はそのためのプレースホルダとして機能します)。
      • d.nbuf0 の場合、EOFに達したと判断してループを終了します。
    • バッファからのエントリのドレイン: バッファにデータがある限り、d.bufp を進めながら syscall.Dirent 構造体を解析し、ファイル名を取り出します。
    • dirent.Ino == 0 のエントリはスキップされます。これは、削除されたファイルや無効なエントリを示す可能性があります。
    • count0 になると、要求された数のエントリを読み取ったとして処理を終了します。
  3. Readdir 関数の変更: Readdir 関数も、バッファサイズに blockSize を使用するように変更されました。

    // 変更前: var buf = make([]byte, 8192);
    // 変更後: var buf = make([]byte, blockSize);
    

    また、Stat の代わりに Lstat を使用するように変更されました。Lstat はシンボリックリンク自体を評価するのに対し、Stat はシンボリックリンクの指す先のファイルを評価します。ディレクトリの内容を列挙する際には、シンボリックリンク自体を認識することが重要であるため、この変更は適切です。

src/lib/os/os_test.go の変更

テストファイルには、Readdirnames の修正を検証するための新しいテストケースが追加されました。

  1. smallReaddirnames ヘルパー関数の追加: この関数は、Readdirnamescount=1 で繰り返し呼び出し、ディレクトリを1エントリずつ読み取るシミュレーションを行います。

    func smallReaddirnames(fd *FD, length int, t *testing.T) []string {
        // ...
        for {
            d, err := Readdirnames(fd, 1); // 1エントリずつ読み取る
            // ...
        }
        // ...
    }
    
  2. TestReaddirnamesOneAtATime テストケースの追加: このテストは、smallReaddirnames を使用してディレクトリを1エントリずつ読み取った結果と、Readdirnamescount=-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.goReaddirnames 関数と、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 が複数回呼び出されても、前回の読み取りの続きから正確に処理できるようにした点です。

  1. DirInfo 構造体による状態管理: DirInfo は、ディレクトリ読み取りに必要なすべての状態を保持します。

    • buf: syscall.Getdirentries から読み込んだ生データを一時的に保持するバッファです。システムコールは一度に複数のディレクトリエントリを返すことがありますが、アプリケーションが要求するエントリ数よりも多くのエントリがバッファに読み込まれる可能性があります。このバッファがその余剰データを保持します。
    • nbuf: buf に現在格納されている有効なデータのバイト数です。これは Getdirentries の戻り値 ret に相当します。
    • bufp: buf 内で次に処理すべきディレクトリエントリの開始オフセットです。Readdirnames がエントリを一つずつ処理するたびに、この bufp が更新されます。
  2. FD への dirinfo の追加: FD はファイルディスクリプタのラッパーであり、開かれたファイルやディレクトリを表します。dirinfo *DirInfo フィールドが追加されたことで、FD オブジェクトは自身が指すディレクトリの読み取り状態を直接管理できるようになりました。これにより、ReaddirnamesFD を引数として受け取るだけで、その FD に関連付けられたディレクトリ読み取りの進行状況を把握し、操作できるようになります。

  3. Readdirnames のバッファリングロジック:

    • 初回呼び出し時の初期化: Readdirnames が特定の FD に対して初めて呼び出される際、fd.dirinfonil であることをチェックし、新しい DirInfo オブジェクトを作成して fd.dirinfo に割り当てます。このとき、bufblockSize (4096バイト) で初期化されます。
    • バッファの補充 (if d.bufp == d.nbuf): d.bufp (バッファ内の現在の読み取り位置) が d.nbuf (バッファ内の有効なデータの終端) と等しい場合、これはバッファ内のすべてのエントリが処理されたことを意味します。このとき、syscall.Getdirentries を呼び出して、カーネルから新しいディレクトリエントリのチャンクを d.buf に読み込みます。 Getdirentries の最後の引数 new(int64) は、システムコールが内部でファイルディスクリプタのオフセットを管理し、次の読み取り位置を自動的に更新することを示唆しています。Go側では、このオフセットを明示的に追跡する必要がなくなりました。 d.nbuf0 の場合、ディレクトリの終端 (EOF) に達したと判断し、ループを終了します。 バッファが補充されたら、d.bufp0 にリセットし、バッファの先頭から新しいエントリの処理を開始します。
    • バッファからのエントリのドレイン (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 に追加します。
  4. Seek メソッドの挙動: FD に追加された Seek メソッドは、ディレクトリの FD に対してオフセットが 0 以外へのシークを禁止します。これは、ディレクトリの読み取りがシーケンシャルな操作であり、ランダムアクセスはサポートされないというUnixの一般的な慣習に従うものです。もしディレクトリの途中にシークしようとすると、EISDIR (Is a directory) エラーが返されます。

これらの変更により、Readdirnames は、たとえ小さなチャンクで繰り返し呼び出されたとしても、FD オブジェクトに保持された DirInfo を介して自身の状態を正確に管理し、ディレクトリの内容を完全に、かつ正しく列挙できるようになりました。これは、特にmacOS環境におけるディレクトリ読み取りの信頼性を大幅に向上させるものです。

関連リンク

参考にした情報源リンク

  • 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.goReaddirnames 関数と、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 が複数回呼び出されても、前回の読み取りの続きから正確に処理できるようにした点です。

  1. DirInfo 構造体による状態管理: DirInfo は、ディレクトリ読み取りに必要なすべての状態を保持します。

    • buf: syscall.Getdirentries から読み込んだ生データを一時的に保持するバッファです。システムコールは一度に複数のディレクトリエントリを返すことがありますが、アプリケーションが要求するエントリ数よりも多くのエントリがバッファに読み込まれる可能性があります。このバッファがその余剰データを保持します。
    • nbuf: buf に現在格納されている有効なデータのバイト数です。これは Getdirentries の戻り値 ret に相当します。
    • bufp: buf 内で次に処理すべきディレクトリエントリの開始オフセットです。Readdirnames がエントリを一つずつ処理するたびに、この bufp が更新されます。
  2. FD への dirinfo の追加: FD はファイルディスクリプタのラッパーであり、開かれたファイルやディレクトリを表します。dirinfo *DirInfo フィールドが追加されたことで、FD オブジェクトは自身が指すディレクトリの読み取り状態を直接管理できるようになりました。これにより、ReaddirnamesFD を引数として受け取るだけで、その FD に関連付けられたディレクトリ読み取りの進行状況を把握し、操作できるようになります。

  3. Readdirnames のバッファリングロジック:

    • 初回呼び出し時の初期化: Readdirnames が特定の FD に対して初めて呼び出される際、fd.dirinfonil であることをチェックし、新しい DirInfo オブジェクトを作成して fd.dirinfo に割り当てます。このとき、bufblockSize (4096バイト) で初期化されます。
    • バッファの補充 (if d.bufp == d.nbuf): d.bufp (バッファ内の現在の読み取り位置) が d.nbuf (バッファ内の有効なデータの終端) と等しい場合、これはバッファ内のすべてのエントリが処理されたことを意味します。このとき、syscall.Getdirentries を呼び出して、カーネルから新しいディレクトリエントリのチャンクを d.buf に読み込みます。 Getdirentries の最後の引数 new(int64) は、システムコールが内部でファイルディスクリプタのオフセットを管理し、次の読み取り位置を自動的に更新することを示唆しています。Go側では、このオフセットを明示的に追跡する必要がなくなりました。 d.nbuf0 の場合、ディレクトリの終端 (EOF) に達したと判断し、ループを終了します。 バッファが補充されたら、d.bufp0 にリセットし、バッファの先頭から新しいエントリの処理を開始します。
    • バッファからのエントリのドレイン (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 に追加します。
  4. Seek メソッドの挙動: FD に追加された Seek メソッドは、ディレクトリの FD に対してオフセットが 0 以外へのシークを禁止します。これは、ディレクトリの読み取りがシーケンシャルな操作であり、ランダムアクセスはサポートされないというUnixの一般的な慣習に従うものです。もしディレクトリの途中にシークしようとすると、EISDIR (Is a directory) エラーが返されます。

これらの変更により、Readdirnames は、たとえ小さなチャンクで繰り返し呼び出されたとしても、FD オブジェクトに保持された DirInfo を介して自身の状態を正確に管理し、ディレクトリの内容を完全に、かつ正しく列挙できるようになりました。これは、特にmacOS環境におけるディレクトリ読み取りの信頼性を大幅に向上させるものです。

関連リンク

参考にした情報源リンク

  • Go言語の初期のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
  • Unix系システムにおけるディレクトリ読み取りのメカニズムに関する一般的な情報源 (例: opendir, readdir, closedir の概念)
  • Go言語の unsafe パッケージの利用に関する情報
  • Go言語のテストフレームワーク testing パッケージに関する情報
  • statfs および fstatfs システムコールに関する情報 (ファイルシステムのブロックサイズ取得のため)