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

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

このコミットは、Go言語の初期段階において、ディレクトリの内容を読み取る機能と、そのためのシステムコール(syscall)サポートを追加するものです。具体的には、osパッケージにディレクトリ内のファイル名を読み取るReaddirnames関数を導入し、その基盤となるsyscallパッケージにOS固有のディレクトリ読み取りシステムコール(DarwinのGetdirentries、LinuxのGetdents)とファイルオフセット操作のためのSeekシステムコールを追加しています。また、これらのシステムコールが使用するDirent構造体も定義されています。

変更されたファイルは以下の通りです。

  • src/lib/os/Makefile: dir_amd64_linux.goのビルド設定が追加されました。
  • src/lib/os/dir_amd64_darwin.go: Darwin (macOS) 環境向けのReaddirnames関数の実装が新規追加されました。
  • src/lib/os/dir_amd64_linux.go: Linux 環境向けのReaddirnames関数の実装が新規追加されました。
  • src/lib/os/os_test.go: Readdirnames関数のテストケースが追加されました。
  • src/lib/syscall/file_darwin.go: Darwin向けのSeekGetdirentriesシステムコールラッパーが追加されました。
  • src/lib/syscall/file_linux.go: Linux向けのSeekGetdentsシステムコールラッパーが追加されました。
  • src/lib/syscall/types_amd64_darwin.go: Darwin向けのDirent構造体とNAME_MAX定数が追加されました。
  • src/lib/syscall/types_amd64_linux.go: Linux向けのDirent構造体とNAME_MAX定数が追加されました。

コミット

このコミットは、Go言語の標準ライブラリにおいて、ディレクトリの内容を読み取るための最初の実装を提供します。これには、osパッケージにおけるReaddirnames関数の追加と、その機能を実現するための低レベルなシステムコールサポートがsyscallパッケージに組み込まれることが含まれます。特に、ディレクトリ内のエントリ名(ファイル名やディレクトリ名)を文字列のスライスとして返す機能が導入されました。

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

https://github.com/golang/go/commit/b5aba026f8070354f01c61a7f5c6127f0e929a44

元コミット内容

First pass at reading directories.
Syscall support.
Readdirnames returns array of strings of contents of directory.

R=rsc
DELTA=216  (201 added, 0 deleted, 15 changed)
OCL=24642
CL=24655

変更の背景

このコミットが行われた2009年2月は、Go言語がまだ一般に公開される前の非常に初期の段階でした。Go言語の設計目標の一つに、システムプログラミングの容易さがありました。ファイルシステムへのアクセスは、あらゆるシステムプログラミングにおいて基本的な機能であり、ディレクトリの内容を読み取る機能はその中でも不可欠です。

当時のGo言語には、ディレクトリの内容を直接読み取るための高レベルなAPIが存在しませんでした。そのため、osパッケージにこの機能を追加し、ユーザーがGoプログラムからディレクトリの内容を簡単に列挙できるようにすることが喫緊の課題でした。この機能を実現するためには、オペレーティングシステムが提供する低レベルなシステムコールをGoから呼び出すためのメカニズム、すなわちsyscallパッケージの拡充が必要でした。

このコミットは、Go言語がファイルシステム操作の基本的な能力を獲得し、より実用的なシステムプログラミング言語としての基盤を固めるための重要な一歩でした。特に、異なるOS(DarwinとLinux)で動作するように、それぞれのOS固有のシステムコールを抽象化して提供する必要がありました。

前提知識の解説

システムコール (Syscall)

システムコールは、ユーザー空間のプログラムがオペレーティングシステム(OS)のカーネル空間のサービスにアクセスするためのインターフェースです。ファイルI/O、プロセス管理、メモリ管理など、OSが提供するほとんどの機能はシステムコールを通じて利用されます。Go言語のsyscallパッケージは、これらの低レベルなシステムコールをGoプログラムから直接呼び出すためのラッパーを提供します。これにより、OS固有の機能にアクセスしたり、パフォーマンスが重要な場面で直接カーネルとやり取りしたりすることが可能になります。

dirent構造体とディレクトリ読み取り

Unix系OSでは、ディレクトリの内容を読み取る際に、getdents (Linux) や getdirentries (BSD/macOS) といったシステムコールを使用します。これらのシステムコールは、ディレクトリ内の各エントリ(ファイルやサブディレクトリ)に関する情報をdirent(directory entry)構造体の配列として返します。dirent構造体には、エントリのinode番号、エントリのタイプ(ファイル、ディレクトリなど)、エントリ名などの情報が含まれます。

  • d_ino (Ino): ファイルのinode番号。
  • d_off (Off): 次のディレクトリエントリへのオフセット。
  • d_reclen (Reclen): このディレクトリエントリのレコード長(バイト単位)。
  • d_type (Type): ファイルタイプ(例: DT_REG (通常ファイル), DT_DIR (ディレクトリ))。
  • d_name (Name): エントリ名。ヌル終端文字列。

Readdirnames関数

Go言語のosパッケージに導入されたReaddirnames関数は、特定のディレクトリの内容を読み取り、その中に含まれるファイルおよびディレクトリの名前のリスト(文字列のスライス)を返します。この関数は、内部的にOS固有のシステムコール(GetdirentriesGetdents)を呼び出して、低レベルなディレクトリ情報を取得し、それをGoの文字列スライスに変換してユーザーに提供します。

Seekシステムコール

Seekシステムコール(lseek)は、オープンされたファイルの読み書き位置(ファイルオフセット)を変更するために使用されます。ディレクトリもファイルの一種として扱われるため、ディレクトリの内容を順次読み取る際には、このSeekシステムコールを使って読み取り位置を管理することがあります。特に、Getdirentriesのようなシステムコールは、ファイルオフセットを引数として受け取り、その位置からディレクトリの内容を読み取ります。

unsafe.Pointer

Go言語のunsafeパッケージは、Goの型システムが提供する安全性をバイパスする機能を提供します。unsafe.Pointerは、任意の型のポインタを保持できる特殊なポインタ型であり、C言語のvoid*に似ています。これを使用することで、Goのメモリモデルに直接アクセスしたり、異なる型のポインタ間で変換を行ったりすることが可能になります。システムコールを呼び出す際には、OSのAPIが期待するメモリレイアウトに合わせてGoのデータを操作する必要があるため、unsafe.Pointerがしばしば利用されます。

MakefileとGoのビルドシステム

Go言語の初期のビルドシステムは、現在のようなgo buildコマンドとは異なり、Makefileに大きく依存していました。このコミットでも、新しく追加されたGoソースファイル(dir_amd64_linux.goなど)をビルドプロセスに組み込むためにMakefileが変更されています。これは、Goの標準ライブラリがどのようにコンパイルされ、アーカイブファイル(.a)にまとめられていたかを示す歴史的な証拠でもあります。

技術的詳細

このコミットの技術的な核心は、異なるオペレーティングシステム(DarwinとLinux)に対して、それぞれに特化したディレクトリ読み取りの実装を提供している点にあります。これは、OSごとにディレクトリ読み取りのシステムコールやdirent構造体の定義が異なるためです。

Darwin (macOS) の実装 (dir_amd64_darwin.go, file_darwin.go, types_amd64_darwin.go)

  • syscall.Seek: Darwinでは、Getdirentriesシステムコールがファイルオフセットを引数として受け取るため、Readdirnames関数内で最初にsyscall.Seek(fd.fd, 0, 1)を呼び出して現在のファイルオフセットを取得しています。これは、ディレクトリの読み取り位置を管理するために必要です。
  • syscall.Getdirentries: このシステムコールは、ファイルディスクリプタ、バッファのポインタ、バッファサイズ、そして現在のファイルオフセットのポインタを引数として受け取ります。読み取られたディレクトリエントリはバッファに書き込まれ、ファイルオフセットは更新されます。
  • syscall.Dirent構造体: DarwinのDirent構造体は、Ino (inode番号), Off (次のエントリへのオフセット), Reclen (レコード長), Namlen (名前の長さ), Type (ファイルタイプ), Name (エントリ名) を含みます。Nameフィールドは[NAME_MAX+1]byteの固定長配列として定義されており、ヌル終端されていない可能性があります。
  • バッファ処理とunsafe.Pointer: Readdirnames関数では、Getdirentriesから返されたバイト列バッファを、unsafe.Pointerとポインタ演算を用いてsyscall.Dirent構造体のスライスとして解釈しています。これは、C言語のポインタキャストに相当する操作であり、Goの型安全性を一時的にバイパスして低レベルなメモリ操作を行っています。dir.Reclenを使って次のエントリへのオフセットを計算し、バッファ内を移動します。
  • 名前の抽出: string(dir.Name[0:dir.Namlen])を使って、Dirent構造体からエントリ名を抽出しています。Namlenは名前の実際の長さを表します。

Linux の実装 (dir_amd64_linux.go, file_linux.go, types_amd64_linux.go)

  • syscall.Getdents: Linuxでは、getdents64システムコール(GoではGetdentsとしてラップ)が使用されます。このシステムコールは、ファイルディスクリプタ、dirent構造体のバッファのポインタ、バッファサイズを引数として受け取ります。Linuxのgetdentsは、ファイルオフセットを自動的に管理するため、明示的なSeek呼び出しは不要です。
  • syscall.Dirent構造体: LinuxのDirent構造体は、Darwinのものと似ていますが、フィールドの順序やサイズが異なる場合があります。特に、Nameフィールドは[NAME_MAX+1]byteの固定長配列として定義されており、clenヘルパー関数を使ってヌル終端文字列の実際の長さを計算しています。
  • バッファ処理とunsafe.Pointer: Linuxの実装でも、Getdentsから返されたバイト列バッファをunsafe.Pointerとポインタ演算を用いてsyscall.Dirent構造体のスライスとして解釈しています。dir.Reclenを使って次のエントリへのオフセットを計算し、バッファ内を移動します。
  • clen関数: Linuxのdirent構造体のd_nameフィールドはヌル終端されることが期待されるため、clen関数が導入されています。これは、バイトスライス内の最初のヌルバイトを見つけて、その位置までの長さを返すヘルパー関数です。これにより、正確なファイル名を抽出できます。

共通のロジック

  • バッファサイズ: 両方の実装で、ディレクトリ読み取りのためのバッファサイズとして8192バイト(8KB)が使用されています。これは、一般的なファイルシステムブロックサイズを考慮したものです。
  • count引数: Readdirnamescount引数は、読み取るエントリの最大数を指定します。-1が渡された場合は、EOF(End Of File)に達するまですべてのエントリを読み取ります。
  • スライス拡張: 読み取られた名前を格納するnamesスライスは、必要に応じて容量が2倍になるように拡張されます。これは、Goのスライスが動的にサイズ変更される一般的なパターンです。
  • エラーハンドリング: システムコールがエラーを返した場合、os.ErrnoToErrorを使ってGoのエラー型に変換し、それを返します。
  • テストケース: os_test.goTestReaddirnamesが追加され、カレントディレクトリ(.)の内容を読み取り、期待されるファイル名が含まれているかを確認しています。これは、新しく追加された機能が正しく動作することを検証するための基本的なテストです。

このコミットは、Go言語が低レベルなOS機能にアクセスし、それを高レベルなAPIとして提供する際の、OS固有の差異を吸収するアプローチを示しています。unsafeパッケージの使用は、Goがシステムプログラミング言語としての能力を確保するために、必要に応じてC言語のような低レベルな操作を許容していることを示唆しています。

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

src/lib/os/dir_amd64_darwin.go (新規追加)

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package os

import (
	"syscall";
	"os";
	"unsafe";
)

// Negative count means read until EOF.
func Readdirnames(fd *FD, count int) (names []string, err *os.Error) {
	// Getdirentries needs the file offset - it's too hard for the kernel to remember
	// a number it already has written down.
	base, err1 := syscall.Seek(fd.fd, 0, 1);
	if err1 != 0 {
		return nil, os.ErrnoToError(err1)
	}
	// The buffer must be at least a block long.
	// TODO(r): use fstatfs to find fs block size.
	var buf = make([]byte, 8192);
	names = make([]string, 0, 100);	// TODO: could be smarter about size
	for {
		if count == 0 {
			break
		}
		ret, err2 := syscall.Getdirentries(fd.fd, &buf[0], len(buf), &base);
		if ret < 0 || err2 != 0 {
			return names, os.ErrnoToError(err2)
		}
		if ret == 0 {
			break
		}
		for w, i := uintptr(0),uintptr(0); i < uintptr(ret); i += w {
			if count == 0 {
				break
			}
			dir := unsafe.Pointer((uintptr(unsafe.Pointer(&buf[0])) + i)).(*syscall.Dirent);
			w = uintptr(dir.Reclen);
			if dir.Ino == 0 {
				continue
			}
			count--;
			if len(names) == cap(names) {
				nnames := make([]string, len(names), 2*len(names));
				for i := 0; i < len(names); i++ {
					nnames[i] = names[i]
				}
				names = nnames;
			}
			names = names[0:len(names)+1];
			names[len(names)-1] = string(dir.Name[0:dir.Namlen]);
		}
	}
	return names, nil;
}

src/lib/os/dir_amd64_linux.go (新規追加)

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package os

import (
	"syscall";
	"os";
	"unsafe";
)

func clen(n []byte) int {
	for i := 0; i < len(n); i++ {
		if n[i] == 0 {
			return i
		}
	}
	return len(n)
}

// Negative count means read until EOF.
func Readdirnames(fd *FD, count int) (names []string, err *os.Error) {
	// The buffer should be at least a block long.
	// TODO(r): use fstatfs to find fs block size.
	var buf = make([]syscall.Dirent, 8192/unsafe.Sizeof(*new(syscall.Dirent)));
	names = make([]string, 0, 100);	// TODO: could be smarter about size
	for {
		if count == 0 {
			break
		}
		ret, err2 := syscall.Getdents(fd.fd, &buf[0], int64(len(buf) * unsafe.Sizeof(buf[0])));
		if ret < 0 || err2 != 0 {
			return names, os.ErrnoToError(err2)
		}
		if ret == 0 {
			break
		}
		for w, i := uintptr(0),uintptr(0); i < uintptr(ret); i += w {
			if count == 0 {
				break
			}
			dir := unsafe.Pointer((uintptr(unsafe.Pointer(&buf[0])) + i)).(*syscall.Dirent);
			w = uintptr(dir.Reclen);
			if dir.Ino == 0 {
				continue
			}
			count--;
			if len(names) == cap(names) {
				nnames := make([]string, len(names), 2*len(names));
				for i := 0; i < len(names); i++ {
					nnames[i] = names[i]
				}
				names = nnames;
			}
			names = names[0:len(names)+1];
			names[len(names)-1] = string(dir.Name[0:clen(dir.Name)]);
		}
	}
	return names, nil;
}

src/lib/syscall/file_darwin.go (変更箇所)

 func Seek(fd int64, offset int64, whence int64) (ret int64, errno int64) {
	r1, r2, err := Syscall(SYS_LSEEK, fd, offset, whence);
	return r1, err;
}

// ... (Pipe, Read, Write, Close, Fstat, Stat, Lstat, Dup2 など既存関数)

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;
}

src/lib/syscall/file_linux.go (変更箇所)

 func Seek(fd int64, offset int64, whence int64) (ret int64, errno int64) {
	r1, r2, err := Syscall(SYS_LSEEK, fd, offset, whence);
	return r1, err;
}

// ... (Pipe, Read, Write, Close, Fstat, Stat, Lstat, Dup2 など既存関数)

func Getdents(fd int64, buf *Dirent, nbytes int64) (ret int64, errno int64) {
	r1, r2, err := Syscall(SYS_GETDENTS64, fd, int64(uintptr(unsafe.Pointer(buf))), nbytes);
	return r1, err;
}

src/lib/syscall/types_amd64_darwin.go (変更箇所)

 const (
	F_SETFL = 4;

	FD_CLOEXEC = 1;

	NAME_MAX = 255; // 追加
)

// ... (Stat_t 構造体)

type Dirent struct { // 追加
	Ino	uint64;
	Off	uint64;
	Reclen	uint16;
	Namlen	uint16;
	Type	uint8;
	Name	[NAME_MAX+1]byte;
}

src/lib/syscall/types_amd64_linux.go (変更箇所)

 const (
	F_SETFL = 4;

	FD_CLOEXEC = 1;

	NAME_MAX = 255; // 追加
)

// ... (Stat_t 構造体)

type Dirent struct { // 追加
	Ino	uint64;
	Off	uint64;
	Reclen	uint16;
	Type	uint8;
	Name	[NAME_MAX+1]byte;
}

コアとなるコードの解説

Readdirnames関数 (Darwin/Linux共通ロジック)

Readdirnames関数は、os.FD型のファイルディスクリプタと、読み取るエントリの最大数count(-1はすべてを意味する)を受け取ります。

  1. バッファの準備: bufというバイトスライス(Darwin)またはsyscall.Direntスライス(Linux)を8KBのサイズで作成します。これは、システムコールがディレクトリの内容を書き込むための領域です。
  2. 名前スライスの初期化: 結果を格納するnamesスライスを、初期容量100で作成します。
  3. ループ処理: forループでディレクトリの内容を繰り返し読み取ります。countが0になったり、システムコールが0バイトを返したり(EOFに達した)場合はループを終了します。
  4. システムコール呼び出し:
    • Darwin: syscall.Getdirentriesを呼び出します。この際、syscall.Seekで取得したbaseオフセットを渡します。
    • Linux: syscall.Getdentsを呼び出します。
  5. エラーチェック: システムコールが負の値またはエラーを返した場合、エラーをGoのエラー型に変換して返します。
  6. エントリのパース: システムコールが成功した場合、返されたバッファの内容をsyscall.Dirent構造体として解釈します。
    • unsafe.Pointer((uintptr(unsafe.Pointer(&buf[0])) + i)).(*syscall.Dirent): これは、バッファの先頭アドレスにオフセットiを加算し、その結果をsyscall.Dirent型へのポインタにキャストする操作です。これにより、バッファ内の生バイト列をDirent構造体として扱えるようになります。
    • w = uintptr(dir.Reclen): Reclenフィールドは、現在のディレクトリエントリのレコード長を示します。次のエントリに進むために、この値を使ってiを更新します。
    • if dir.Ino == 0 { continue }: inode番号が0のエントリは無効なエントリ(例えば、削除されたエントリの残骸)である可能性があるためスキップします。
  7. 名前の抽出と追加:
    • namesスライスの容量が不足している場合、新しいスライスを作成して既存の要素をコピーし、容量を2倍に拡張します。
    • names = names[0:len(names)+1]でスライスの長さを1増やし、新しい要素を追加できるようにします。
    • names[len(names)-1] = string(dir.Name[0:dir.Namlen]) (Darwin) または string(dir.Name[0:clen(dir.Name)]) (Linux): Dirent構造体からエントリ名を抽出し、Goの文字列に変換してnamesスライスに追加します。Linuxではclen関数を使ってヌル終端を考慮します。
  8. 結果の返却: すべてのエントリを読み終えるか、指定されたcountに達したら、namesスライスとnilエラーを返します。

syscallパッケージの変更

  • Seek関数: SYS_LSEEKシステムコールを呼び出すラッパー関数です。ファイルディスクリプタ、オフセット、whence(シークの基準位置)を引数にとり、新しいオフセットとエラーを返します。
  • Getdirentries関数 (Darwin): SYS_GETDIRENTRIES64システムコールを呼び出すラッパー関数です。ファイルディスクリプタ、バッファのポインタ、バッファサイズ、ファイルオフセットのポインタを引数にとり、読み取られたバイト数とエラーを返します。
  • Getdents関数 (Linux): SYS_GETDENTS64システムコールを呼び出すラッパー関数です。ファイルディスクリプタ、Direntバッファのポインタ、バッファサイズを引数にとり、読み取られたバイト数とエラーを返します。
  • Dirent構造体: 各OSのdirent構造体に対応するGoの構造体定義です。OSによってフィールドの順序や型が異なるため、それぞれに合わせた定義が必要です。NAME_MAX定数は、ファイル名の最大長を定義しています。

これらの変更により、Go言語はOSの低レベルなファイルシステムAPIと直接連携し、ディレクトリの内容を効率的に読み取ることができるようになりました。

関連リンク

  • Go言語のosパッケージドキュメント: https://pkg.go.dev/os (現在のバージョン)
  • Go言語のsyscallパッケージドキュメント: https://pkg.go.dev/syscall (現在のバージョン)
  • Unix系OSにおけるgetdents / getdirentries システムコールに関する情報 (例: man 2 getdents または man 2 getdirentries)

参考にした情報源リンク