[インデックス 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向けのSeek
とGetdirentries
システムコールラッパーが追加されました。src/lib/syscall/file_linux.go
: Linux向けのSeek
とGetdents
システムコールラッパーが追加されました。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固有のシステムコール(Getdirentries
やGetdents
)を呼び出して、低レベルなディレクトリ情報を取得し、それを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
引数:Readdirnames
のcount
引数は、読み取るエントリの最大数を指定します。-1
が渡された場合は、EOF(End Of File)に達するまですべてのエントリを読み取ります。- スライス拡張: 読み取られた名前を格納する
names
スライスは、必要に応じて容量が2倍になるように拡張されます。これは、Goのスライスが動的にサイズ変更される一般的なパターンです。 - エラーハンドリング: システムコールがエラーを返した場合、
os.ErrnoToError
を使ってGoのエラー型に変換し、それを返します。 - テストケース:
os_test.go
にTestReaddirnames
が追加され、カレントディレクトリ(.
)の内容を読み取り、期待されるファイル名が含まれているかを確認しています。これは、新しく追加された機能が正しく動作することを検証するための基本的なテストです。
このコミットは、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はすべてを意味する)を受け取ります。
- バッファの準備:
buf
というバイトスライス(Darwin)またはsyscall.Dirent
スライス(Linux)を8KBのサイズで作成します。これは、システムコールがディレクトリの内容を書き込むための領域です。 - 名前スライスの初期化: 結果を格納する
names
スライスを、初期容量100で作成します。 - ループ処理:
for
ループでディレクトリの内容を繰り返し読み取ります。count
が0になったり、システムコールが0バイトを返したり(EOFに達した)場合はループを終了します。 - システムコール呼び出し:
- Darwin:
syscall.Getdirentries
を呼び出します。この際、syscall.Seek
で取得したbase
オフセットを渡します。 - Linux:
syscall.Getdents
を呼び出します。
- Darwin:
- エラーチェック: システムコールが負の値またはエラーを返した場合、エラーをGoのエラー型に変換して返します。
- エントリのパース: システムコールが成功した場合、返されたバッファの内容を
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のエントリは無効なエントリ(例えば、削除されたエントリの残骸)である可能性があるためスキップします。
- 名前の抽出と追加:
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
関数を使ってヌル終端を考慮します。
- 結果の返却: すべてのエントリを読み終えるか、指定された
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
)
参考にした情報源リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Go言語の初期のコミット履歴 (GitHub): https://github.com/golang/go/commits/master?after=b5aba026f8070354f01c61a7f5c6127f0e929a44+1
- Go言語の
unsafe
パッケージに関するドキュメント: https://pkg.go.dev/unsafe - Go言語の歴史に関する情報 (例: Go blog, Go design documents)
- The Go Programming Language (initial announcement): https://go.dev/blog/go-language-announcement
- Go at Google: Language Design in the Service of Software Engineering: https://go.dev/talks/2012/go4progs.slide#1
- Unix系OSのシステムコールに関する一般的な情報 (例: Wikipedia, man pages)
lseek(2)
man page (Linux): https://man7.org/linux/man-pages/man2/lseek.2.htmlgetdents(2)
man page (Linux): https://man7.org/linux/man-pages/man2/getdents.2.htmlgetdirentries(2)
man page (macOS/FreeBSD): https://www.freebsd.org/cgi/man.cgi?query=getdirentries&sektion=2
- Go言語の
Makefile
に関する情報 (初期のビルドシステム): https://go.dev/doc/install/source (古い情報を含む可能性がある)