[インデックス 1639] ファイルの概要
このコミットは、Go言語の標準ライブラリにおけるファイルシステム操作とシステムコールインターフェースの重要な改善を導入しています。主な目的は、stat
システムコール(ファイルやディレクトリのメタデータ取得)のクロスプラットフォーム対応を強化し、ファイルディスクリプタ(os.FD
)に名前フィールドを追加することで、より堅牢で使いやすいAPIを提供することです。また、システムコールにおける文字列の扱い方を根本的に変更し、より効率的で安全な方法に移行しています。
コミット
commit 704bc9d5c95858fd43151f19e7e4de6c99142f1c
Author: Rob Pike <r@golang.org>
Date: Fri Feb 6 17:54:26 2009 -0800
portable stat for os
add name to os.FD
clean up some interfaces
R=rsc
DELTA=318 (231 added, 44 deleted, 43 changed)
OCL=24624
CL=24627
---
src/lib/io/io.go | 30 +++++++---------\
src/lib/net/fd.go | 10 +++---\
src/lib/os/Makefile | 22 ++++++++----\
src/lib/os/os_file.go | 66 +++++++++++++++++++++++++---------\
src/lib/os/os_test.go | 79 +++++++++++++++++++++++++++++++++++++++++
src/lib/os/os_types.go | 26 ++++++++++++++\
src/lib/os/stat_amd64_darwin.go | 34 ++++++++++++++++++\
src/lib/os/stat_amd64_linux.go | 34 ++++++++++++++++++\
src/lib/syscall/file_linux.go | 40 +++++++--------------
src/lib/syscall/syscall.go | 11 +++---\
src/lib/utf8_test.go | 11 +++---\
11 files changed, 277 insertions(+), 86 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/704bc9d5c95858fd43151f19e7e4de6c99142f1c
元コミット内容
portable stat for os
add name to os.FD
clean up some interfaces
変更の背景
このコミットは、Go言語の非常に初期の段階(2009年2月)に行われたもので、Goがまだオープンソース化されて間もない頃の基盤的な改善を反映しています。当時のGoは、異なるオペレーティングシステム(特にLinuxとDarwin/macOS)上での動作を安定させ、標準ライブラリのAPIを洗練させる途上にありました。
主な変更の背景には以下の点が挙げられます。
-
クロスプラットフォーム対応の強化:
stat
システムコールは、ファイルやディレクトリに関する詳細な情報(サイズ、パーミッション、タイムスタンプなど)を取得するために不可欠です。しかし、その構造体(stat_t
)やフィールドはOSによって異なります。Goが真にポータブルな言語となるためには、これらのOS固有の差異を抽象化し、統一されたインターフェースを提供する必要がありました。このコミットでは、os.Dir
というOS非依存の構造体を導入し、各OS(amd64/darwin, amd64/linux)向けのstat
実装でこれを埋めることで、この課題に対処しています。 -
ファイルディスクリプタの利便性向上:
os.FD
はファイルディスクリプタをラップするGoの型です。これまでの実装では、ファイルディスクリプタの数値(Fd
)しか持っていませんでした。しかし、デバッグやロギング、あるいは特定の操作において、そのファイルディスクリプタがどのファイル名に対応しているかを知ることは非常に有用です。name
フィールドの追加は、この利便性を向上させ、APIの使いやすさを改善することを目的としています。 -
システムコールにおける文字列処理の改善: 従来のGoのシステムコールでは、Goの文字列をC言語スタイルのNULL終端バイト配列に変換するために
syscall.StringToBytes
のような関数を使用していました。この方法は、固定サイズのバッファを必要とし、バッファオーバーフローのリスクや、文字列長がバッファサイズを超える場合の扱いに問題がありました。より効率的で安全なsyscall.StringBytePtr
への移行は、システムコール層の堅牢性を高めるための重要なリファクタリングです。これは、Goの文字列が内部的にUTF-8でエンコードされており、C言語のAPIと連携する際に適切な変換が必要となるため、特に重要でした。
これらの変更は、Go言語が多様な環境で安定して動作し、開発者が低レベルのOSインターフェースを意識することなく、高レベルなファイルシステム操作を行えるようにするための基盤を築くものでした。
前提知識の解説
このコミットを理解するためには、以下の概念についての基本的な知識が必要です。
-
ファイルディスクリプタ (File Descriptor, FD): Unix系OSにおいて、ファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。プログラムはファイル名ではなく、このFDを使ってI/O操作を行います。標準入力(stdin)は0、標準出力(stdout)は1、標準エラー出力(stderr)は2というFDが予約されています。
-
stat
システムコール: Unix系OSで提供されるシステムコールの一つで、指定されたファイルやディレクトリのメタデータ(ファイルの種類、サイズ、パーミッション、所有者、最終アクセス/変更時刻など)を取得するために使用されます。stat
はパス名を引数にとり、fstat
はファイルディスクリプタを引数にとり、lstat
はシンボリックリンク自体を対象とします(stat
はリンク先のファイルを対象とする)。これらのシステムコールは、通常、stat_t
という構造体に結果を格納します。 -
システムコール (Syscall): オペレーティングシステムが提供するサービスを、ユーザー空間のプログラムが利用するためのインターフェースです。ファイルI/O、メモリ管理、プロセス制御など、OSのカーネルが管理するリソースへのアクセスは、システムコールを介して行われます。Go言語では、
syscall
パッケージがこれらの低レベルなインターフェースを提供します。 -
Go言語の
os
パッケージ: Goの標準ライブラリの一部で、OSに依存しないインターフェースを提供し、ファイルシステム操作、プロセス管理、環境変数へのアクセスなどを可能にします。このパッケージは、内部的にOS固有のシステムコールを呼び出すことで、クロスプラットフォームな抽象化を実現しています。 -
Go言語の
io
パッケージ: Goの標準ライブラリの一部で、I/Oプリミティブ(Reader
,Writer
インターフェースなど)を提供します。ファイルやネットワーク接続など、様々なI/Oソースとシンクに対して統一的なインターフェースを提供します。 -
Go言語の文字列とC言語の文字列: Go言語の文字列は、不変のバイトスライスであり、UTF-8でエンコードされています。文字列の長さはバイト数で管理され、NULL終端ではありません。一方、C言語の文字列は、文字の配列であり、文字列の終端をNULL文字(
\0
)で示します。システムコールは通常C言語のAPIとして提供されるため、Goの文字列をシステムコールに渡す際には、C言語の文字列形式に変換する必要があります。 -
Makefile: ソフトウェアのビルドプロセスを自動化するためのツールである
make
が使用する設定ファイルです。依存関係に基づいてコマンドを実行し、ソースコードのコンパイル、リンク、テストなどのタスクを効率的に管理します。このコミットでは、新しいソースファイル(os_types.go
,stat_amd64_darwin.go
,stat_amd64_linux.go
)をビルドプロセスに含めるためにMakefile
が更新されています。
技術的詳細
このコミットにおける技術的な変更は多岐にわたりますが、特に以下の点が重要です。
-
os.FD
構造体の変更とメソッドの追加:src/lib/os/os_file.go
において、os.FD
構造体にプライベートフィールドfd int64
とname string
が追加されました。これにより、ファイルディスクリプタの数値だけでなく、そのファイルディスクリプタが関連付けられているファイル名も保持できるようになりました。Fd() int64
とName() string
というパブリックなメソッドが追加され、FD
オブジェクトからこれらの情報にアクセスできるようになりました。これにより、FD
の内部実装が隠蔽され、よりクリーンなAPIが提供されます。NewFD
関数もname
引数を取るように変更され、Stdin
,Stdout
,Stderr
といった標準FDも初期化時に適切な名前を持つようになりました(例:NewFD(0, "/dev/stdin")
)。os.Open
,os.Pipe
などの関数も、NewFD
の新しいシグネチャに合わせて更新されています。
-
stat
関連関数の導入とOS依存実装の分離:src/lib/os/os_types.go
が新規作成され、Dir
というOS非依存の構造体が定義されました。この構造体は、stat
システムコールが返すファイルメタデータ(デバイスID、inode番号、リンク数、モード、サイズ、タイムスタンプなど)を抽象化します。src/lib/os/os_file.go
にStat
,Fstat
,Lstat
という新しい関数が追加されました。これらの関数は、それぞれパス名、ファイルディスクリプタ、シンボリックリンクのパス名を受け取り、os.Dir
構造体を返します。src/lib/os/stat_amd64_darwin.go
とsrc/lib/os/stat_amd64_linux.go
が新規作成されました。これらは、それぞれのOS(DarwinとLinux)におけるsyscall.Stat_t
構造体からos.Dir
構造体への変換を行うdirFromStat
関数を実装しています。これにより、OS固有のstat
構造体の差異がGoのos.Dir
によって吸収され、クロスプラットフォームなstat
操作が可能になります。
-
システムコールにおける文字列処理の変更:
src/lib/syscall/syscall.go
において、StringToBytes
関数が削除され、代わりにStringBytePtr
関数が導入されました。StringToBytes
は、Goの文字列を固定サイズのバイト配列にコピーし、NULL終端を追加するものでした。これはバッファサイズの問題や、len(s) >= len(b)
の場合にfalse
を返すという扱いにくさがありました。StringBytePtr
は、Goの文字列を新しいバイトスライスにコピーし、NULL終端を追加した上で、そのバイトスライスの先頭要素へのポインタ(*byte
)を返します。これにより、システムコールに直接渡せるC言語スタイルの文字列ポインタを、より安全かつ柔軟に生成できるようになりました。
src/lib/syscall/file_linux.go
内のOpen
,Creat
,Stat
,Lstat
,Unlink
,Mkdir
といったファイルシステム関連のシステムコールラッパーが、StringToBytes
の代わりに新しく導入されたStringBytePtr
を使用するように変更されました。これにより、システムコールへの文字列引数の渡し方が統一され、コードの簡潔性と堅牢性が向上しています。
-
ビルドシステムの更新:
src/lib/os/Makefile
が更新され、新しいos_types.go
、stat_amd64_darwin.go
、stat_amd64_linux.go
ファイルがビルドプロセスに含まれるようになりました。特に、stat_$(GOARCH)_$(GOOS).$O
というパターンを使用することで、ビルドターゲットのアーキテクチャとOSに基づいて適切なstat
実装がリンクされるように設定されています。これは、Goのクロスコンパイルとポータビリティをサポートするための典型的なアプローチです。
これらの変更は、Go言語のファイルシステム抽象化層を大幅に改善し、OS間の差異を吸収しながら、より表現力豊かで安全なAPIを提供するための重要なステップでした。
コアとなるコードの変更箇所
src/lib/os/os_file.go
における FD
構造体と関連関数の変更
--- a/src/lib/os/os_file.go
+++ b/src/lib/os/os_file.go
@@ -9,20 +9,29 @@ import os "os"
// FDs are wrappers for file descriptors
type FD struct {
- Fd int64
+ fd int64;
+ name string;
}
-func NewFD(fd int64) *FD {
+func (fd *FD) Fd() int64 {
+ return fd.fd
+}
+
+func (fd *FD) Name() string {
+ return fd.name
+}
+
+func NewFD(fd int64, name string) *FD {
if fd < 0 {
return nil
}
- return &FD{fd}
+ return &FD{fd, name}
}
var (
- Stdin = NewFD(0);
- Stdout = NewFD(1);
- Stderr = NewFD(2);
+ Stdin = NewFD(0, "/dev/stdin");
+ Stdout = NewFD(1, "/dev/stdout");
+ Stderr = NewFD(2, "/dev/stderr");
)
const (
@@ -41,15 +50,15 @@ const (
func Open(name string, mode int, flags int) (fd *FD, err *Error) {
r, e := syscall.Open(name, int64(mode), int64(flags));
- return NewFD(r), ErrnoToError(e)
+ return NewFD(r, name), ErrnoToError(e)
}
func (fd *FD) Close() *Error {
if fd == nil {
return EINVAL
}
- r, e := syscall.Close(fd.Fd);
- fd.Fd = -1; // so it can't be closed again
+ r, e := syscall.Close(fd.fd);
+ fd.fd = -1; // so it can't be closed again
return ErrnoToError(e)
}
@@ -59,7 +68,7 @@ func (fd *FD) Read(b []byte) (ret int, err *Error) {
}
var r, e int64;
if len(b) > 0 { // because we access b[0]
- r, e = syscall.Read(fd.Fd, &b[0], int64(len(b)));
+ r, e = syscall.Read(fd.fd, &b[0], int64(len(b)));
if r < 0 {
r = 0
}
@@ -73,7 +82,7 @@ func (fd *FD) Write(b []byte) (ret int, err *Error) {
}
var r, e int64;
if len(b) > 0 { // because we access b[0]
- r, e = syscall.Write(fd.Fd, &b[0], int64(len(b)));
+ r, e = syscall.Write(fd.fd, &b[0], int64(len(b)));
if r < 0 {
r = 0
}
@@ -85,11 +94,7 @@ func (fd *FD) WriteString(s string) (ret int, err *Error) {\n if fd == nil {\n return 0, EINVAL
}
- b := make([]byte, len(s)+1);
- if !syscall.StringToBytes(b, s) {
- return 0, EINVAL
- }
- r, e := syscall.Write(fd.Fd, &b[0], int64(len(s)));
+ r, e := syscall.Write(fd.fd, syscall.StringBytePtr(s), int64(len(s)));
if r < 0 {
r = 0
}
@@ -102,10 +107,37 @@ func Pipe() (fd1 *FD, fd2 *FD, err *Error) {\
if e != 0 {
return nil, nil, ErrnoToError(e)
}
- return NewFD(p[0]), NewFD(p[1]), nil
+ return NewFD(p[0], "|0"), NewFD(p[1], "|1"), nil
}
func Mkdir(name string, perm int) *Error {
r, e := syscall.Mkdir(name, int64(perm));
return ErrnoToError(e)
}
+
+func Stat(name string) (dir *Dir, err *Error) {
+ stat := new(syscall.Stat_t);
+ r, e := syscall.Stat(name, stat);
+ if e != 0 {
+ return nil, ErrnoToError(e)
+ }
+ return dirFromStat(name, new(Dir), stat), nil
+}
+
+func Fstat(fd *FD) (dir *Dir, err *Error) {
+ stat := new(syscall.Stat_t);
+ r, e := syscall.Fstat(fd.fd, stat);
+ if e != 0 {
+ return nil, ErrnoToError(e)
+ }
+ return dirFromStat(fd.name, new(Dir), stat), nil
+}
+
+func Lstat(name string) (dir *Dir, err *Error) {
+ stat := new(syscall.Stat_t);
+ r, e := syscall.Lstat(name, stat);
+ if e != 0 {
+ return nil, ErrnoToError(e)
+ }
+ return dirFromStat(name, new(Dir), stat), nil
+}
src/lib/syscall/syscall.go
における StringBytePtr
の導入
--- a/src/lib/syscall/syscall.go
+++ b/src/lib/syscall/syscall.go
@@ -16,13 +16,10 @@ func RawSyscall(trap int64, a1, a2, a3 int64) (r1, r2, err int64);
* Used to convert file names to byte arrays for passing to kernel,
* but useful elsewhere too.
*/
-func StringToBytes(b []byte, s string) bool {
- if len(s) >= len(b) {
- return false
- }
+func StringBytePtr(s string) *byte {
+ a := make([]byte, len(s)+1);
for i := 0; i < len(s); i++ {
- b[i] = s[i]
+ a[i] = s[i];
}
- b[len(s)] = '\000'; // not necessary - memory is zeroed - but be explicit
- return true
+ return &a[0];
}
src/lib/os/os_types.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
// An operating-system independent representation of Unix data structures.
// OS-specific routines in this directory convert the OS-local versions to these.
// Result of stat64(2) etc.
type Dir struct {
Dev uint64;
Ino uint64;
Nlink uint64;
Mode uint32;
Uid uint32;
Gid uint32;
Rdev uint64;
Size uint64;
Blksize uint64;
Blocks uint64;
Atime_ns uint64; // nanoseconds since 1970
Mtime_ns uint64; // nanoseconds since 1970
Ctime_ns uint64; // nanoseconds since 1970
Name string;
}
src/lib/os/stat_amd64_linux.go
(新規ファイル、stat_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.
// AMD64, Linux
package os
import syscall "syscall"
import os "os"
func dirFromStat(name string, dir *Dir, stat *syscall.Stat_t) *Dir {
dir.Dev = stat.Dev;
dir.Ino = stat.Ino;
dir.Nlink = stat.Nlink;
dir.Mode = stat.Mode;
dir.Uid = stat.Uid;
dir.Gid = stat.Gid;
dir.Rdev = stat.Rdev;
dir.Size = uint64(stat.Size);
dir.Blksize = uint64(stat.Blksize);
dir.Blocks = uint64(stat.Blocks);
dir.Atime_ns = uint64(stat.Atime.Sec) * 1e9 + stat.Atime.Nsec;
dir.Mtime_ns = uint64(stat.Mtime.Sec) * 1e9 + stat.Mtime.Nsec;
dir.Ctime_ns = uint64(stat.Ctime.Sec) * 1e9 + stat.Atime.Nsec;
for i := len(name) - 1; i >= 0; i-- {
if name[i] == '/' {
name = name[i+1:len(name)];
break;
}
}
dir.Name = name;
return dir;
}
コアとなるコードの解説
os.FD
構造体と関連関数の変更 (src/lib/os/os_file.go
)
FD
構造体の変更: 以前はFd int64
のみを持っていたFD
構造体に、fd int64
(プライベート化)とname string
が追加されました。これにより、ファイルディスクリプタの数値だけでなく、そのファイルがオープンされた際のパス名もFD
オブジェクトに紐付けられるようになりました。fd
フィールドがプライベートになったことで、外部からは直接アクセスできなくなり、カプセル化が強化されています。Fd()
とName()
メソッドの追加:fd
とname
フィールドへのアクセスを提供するために、それぞれFd()
とName()
というゲッターメソッドが追加されました。これにより、FD
の内部表現に依存せずに、これらの情報を取得できるようになります。NewFD
関数の変更:NewFD
関数は、ファイルディスクリプタの数値だけでなく、そのファイルディスクリプタに関連するname
(通常はファイルパス)も引数として受け取るようになりました。これにより、FD
オブジェクトがより完全な情報を持つことができるようになります。- 標準FDの初期化:
Stdin
,Stdout
,Stderr
といったGoの標準ファイルディスクリプタも、新しいNewFD
のシグネチャに合わせて、それぞれ/dev/stdin
,/dev/stdout
,/dev/stderr
という名前で初期化されるようになりました。これは、これらのFDがどのリソースに対応しているかを明確にするのに役立ちます。 Open
,Close
,Read
,Write
,WriteString
,Pipe
の変更: これらの関数やメソッドは、FD
構造体の変更に合わせて、内部でfd.fd
(プライベートなフィールド)を使用するように修正されました。特にWriteString
では、後述のsyscall.StringBytePtr
が使用されるようになり、文字列のシステムコールへの渡し方が改善されています。Pipe
関数も、生成されるパイプのFDに|0
や|1
といった名前を付与するようになりました。
syscall.StringBytePtr
の導入 (src/lib/syscall/syscall.go
)
StringToBytes
の削除とStringBytePtr
の導入: 以前のStringToBytes
関数は、Goの文字列をC言語のNULL終端バイト配列に変換する際に、固定サイズのバッファを必要とし、バッファオーバーフローの可能性や、文字列が長すぎる場合の扱いに問題がありました。- 新しく導入された
StringBytePtr
関数は、Goの文字列s
を受け取り、その内容を新しいバイトスライスa
にコピーし、最後にNULL終端を追加します。そして、そのバイトスライスa
の先頭要素へのポインタ(*byte
)を返します。 - このアプローチにより、呼び出し側は固定サイズのバッファを事前に用意する必要がなくなり、Goの文字列の長さに応じて動的にメモリが確保されるため、より安全で柔軟な文字列変換が可能になります。システムコールは通常、C言語の関数として定義されており、文字列引数として
char*
(バイトポインタ)を期待するため、この*byte
型はシステムコールに直接渡すのに適しています。
os.Dir
構造体 (src/lib/os/os_types.go
)
- このファイルで定義された
Dir
構造体は、OSに依存しないファイルやディレクトリのメタデータ表現です。Unix系OSのstat_t
構造体から主要なフィールド(デバイスID、inode番号、リンク数、モード、UID、GID、サイズ、ブロックサイズ、ブロック数、アクセス/変更/作成タイムスタンプ、名前)を抽出し、Goの型にマッピングしています。 - この抽象化層により、Goのアプリケーションは、基盤となるOSがLinuxであろうとDarwinであろうと、統一された
Dir
構造体を通じてファイルメタデータにアクセスできるようになります。
OS固有の dirFromStat
関数 (src/lib/os/stat_amd64_linux.go
など)
stat_amd64_linux.go
とstat_amd64_darwin.go
(および他のOS/アーキテクチャ固有のファイル)は、それぞれのOSのsyscall.Stat_t
構造体からos.Dir
構造体への変換を行うdirFromStat
関数を実装しています。- これらの関数は、OS固有の
stat_t
フィールドをos.Dir
の対応するフィールドにマッピングします。例えば、Linuxではstat.Size
がint64
ですが、os.Dir.Size
はuint64
であるため、型変換が行われます。 - また、
name
引数からファイル名部分を抽出してdir.Name
に設定するロジックも含まれています。これは、stat
システムコールがパス名全体を受け取るのに対し、Dir.Name
にはファイル名のみを格納するためです。 - この分離された実装により、Goの
os
パッケージは、異なるOSのstat
システムコールの差異を透過的に扱うことができ、Go開発者はOSの違いを意識することなくファイルメタデータにアクセスできます。
これらの変更は、Go言語の初期段階におけるシステムプログラミングの基盤を固め、クロスプラットフォーム対応とAPIの使いやすさを大幅に向上させるための重要なステップでした。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語の
os
パッケージ: https://pkg.go.dev/os - Go言語の
syscall
パッケージ: https://pkg.go.dev/syscall - Unix
stat
man page (例: Linux):man 2 stat
(シェルで実行)
参考にした情報源リンク
- Go言語の初期のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語の設計に関する議論 (初期のメーリングリストなど): Goの初期の設計に関する情報は、主にGoの公式ブログやメーリングリストのアーカイブに散在しています。具体的なURLはコミットから直接特定できませんが、Goの歴史を辿る上で重要な情報源です。
- Unix系OSのシステムコールに関する一般的な情報源 (例: Wikipedia, man pages)。
- Go言語のソースコード自体。
- Go言語の
io
パッケージのStringBytes
の歴史的な変更に関する情報。 I have generated the detailed technical explanation of the commit in Markdown format, following all the specified instructions and including all required sections. The output is in Japanese and is as detailed as possible, covering the background, prerequisite knowledge, technical details, core code changes, and explanations. I have also included relevant and reference links. The response is ready to be outputted to standard output.# [インデックス 1639] ファイルの概要
このコミットは、Go言語の標準ライブラリにおけるファイルシステム操作とシステムコールインターフェースの重要な改善を導入しています。主な目的は、stat
システムコール(ファイルやディレクトリのメタデータ取得)のクロスプラットフォーム対応を強化し、ファイルディスクリプタ(os.FD
)に名前フィールドを追加することで、より堅牢で使いやすいAPIを提供することです。また、システムコールにおける文字列の扱い方を根本的に変更し、より効率的で安全な方法に移行しています。
コミット
commit 704bc9d5c95858fd43151f19e7e4de6c99142f1c
Author: Rob Pike <r@golang.org>
Date: Fri Feb 6 17:54:26 2009 -0800
portable stat for os
add name to os.FD
clean up some interfaces
R=rsc
DELTA=318 (231 added, 44 deleted, 43 changed)
OCL=24624
CL=24627
---
src/lib/io/io.go | 30 +++++++---------\
src/lib/net/fd.go | 10 +++---\
src/lib/os/Makefile | 22 ++++++++----\
src/lib/os/os_file.go | 66 +++++++++++++++++++++++++---------\
src/lib/os/os_test.go | 79 +++++++++++++++++++++++++++++++++++++++++
src/lib/os/os_types.go | 26 ++++++++++++++\
src/lib/os/stat_amd64_darwin.go | 34 ++++++++++++++++++\
src/lib/os/stat_amd64_linux.go | 34 ++++++++++++++++++\
src/lib/syscall/file_linux.go | 40 +++++++--------------
src/lib/syscall/syscall.go | 11 +++---\
src/lib/utf8_test.go | 11 +++---\
11 files changed, 277 insertions(+), 86 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/704bc9d5c95858fd43151f19e7e4de6c99142f1c
元コミット内容
portable stat for os
add name to os.FD
clean up some interfaces
変更の背景
このコミットは、Go言語の非常に初期の段階(2009年2月)に行われたもので、Goがまだオープンソース化されて間もない頃の基盤的な改善を反映しています。当時のGoは、異なるオペレーティングシステム(特にLinuxとDarwin/macOS)上での動作を安定させ、標準ライブラリのAPIを洗練させる途上にありました。
主な変更の背景には以下の点が挙げられます。
-
クロスプラットフォーム対応の強化:
stat
システムコールは、ファイルやディレクトリに関する詳細な情報(サイズ、パーミッション、タイムスタンプなど)を取得するために不可欠です。しかし、その構造体(stat_t
)やフィールドはOSによって異なります。Goが真にポータブルな言語となるためには、これらのOS固有の差異を抽象化し、統一されたインターフェースを提供する必要がありました。このコミットでは、os.Dir
というOS非依存の構造体を導入し、各OS(amd64/darwin, amd64/linux)向けのstat
実装でこれを埋めることで、この課題に対処しています。 -
ファイルディスクリプタの利便性向上:
os.FD
はファイルディスクリプタをラップするGoの型です。これまでの実装では、ファイルディスクリプタの数値(Fd
)しか持っていませんでした。しかし、デバッグやロギング、あるいは特定の操作において、そのファイルディスクリプタがどのファイル名に対応しているかを知ることは非常に有用です。name
フィールドの追加は、この利便性を向上させ、APIの使いやすさを改善することを目的としています。 -
システムコールにおける文字列処理の改善: 従来のGoのシステムコールでは、Goの文字列をC言語スタイルのNULL終端バイト配列に変換するために
syscall.StringToBytes
のような関数を使用していました。この方法は、固定サイズのバッファを必要とし、バッファオーバーフローのリスクや、文字列長がバッファサイズを超える場合の扱いに問題がありました。より効率的で安全なsyscall.StringBytePtr
への移行は、システムコール層の堅牢性を高めるための重要なリファクタリングです。これは、Goの文字列が内部的にUTF-8でエンコードされており、C言語のAPIと連携する際に適切な変換が必要となるため、特に重要でした。
これらの変更は、Go言語が多様な環境で安定して動作し、開発者が低レベルのOSインターフェースを意識することなく、高レベルなファイルシステム操作を行えるようにするための基盤を築くものでした。
前提知識の解説
このコミットを理解するためには、以下の概念についての基本的な知識が必要です。
-
ファイルディスクリプタ (File Descriptor, FD): Unix系OSにおいて、ファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。プログラムはファイル名ではなく、このFDを使ってI/O操作を行います。標準入力(stdin)は0、標準出力(stdout)は1、標準エラー出力(stderr)は2というFDが予約されています。
-
stat
システムコール: Unix系OSで提供されるシステムコールの一つで、指定されたファイルやディレクトリのメタデータ(ファイルの種類、サイズ、パーミッション、所有者、最終アクセス/変更時刻など)を取得するために使用されます。stat
はパス名を引数にとり、fstat
はファイルディスクリプタを引数にとり、lstat
はシンボリックリンク自体を対象とします(stat
はリンク先のファイルを対象とする)。これらのシステムコールは、通常、stat_t
という構造体に結果を格納します。 -
システムコール (Syscall): オペレーティングシステムが提供するサービスを、ユーザー空間のプログラムが利用するためのインターフェースです。ファイルI/O、メモリ管理、プロセス制御など、OSのカーネルが管理するリソースへのアクセスは、システムコールを介して行われます。Go言語では、
syscall
パッケージがこれらの低レベルなインターフェースを提供します。 -
Go言語の
os
パッケージ: Goの標準ライブラリの一部で、OSに依存しないインターフェースを提供し、ファイルシステム操作、プロセス管理、環境変数へのアクセスなどを可能にします。このパッケージは、内部的にOS固有のシステムコールを呼び出すことで、クロスプラットフォームな抽象化を実現しています。 -
Go言語の
io
パッケージ: Goの標準ライブラリの一部で、I/Oプリミティブ(Reader
,Writer
インターフェースなど)を提供します。ファイルやネットワーク接続など、様々なI/Oソースとシンクに対して統一的なインターフェースを提供します。 -
Go言語の文字列とC言語の文字列: Go言語の文字列は、不変のバイトスライスであり、UTF-8でエンコードされています。文字列の長さはバイト数で管理され、NULL終端ではありません。一方、C言語の文字列は、文字の配列であり、文字列の終端をNULL文字(
\0
)で示します。システムコールは通常C言語のAPIとして提供されるため、Goの文字列をシステムコールに渡す際には、C言語の文字列形式に変換する必要があります。 -
Makefile: ソフトウェアのビルドプロセスを自動化するためのツールである
make
が使用する設定ファイルです。依存関係に基づいてコマンドを実行し、ソースコードのコンパイル、リンク、テストなどのタスクを効率的に管理します。このコミットでは、新しいソースファイル(os_types.go
,stat_amd64_darwin.go
,stat_amd64_linux.go
)をビルドプロセスに含めるためにMakefile
が更新されています。
技術的詳細
このコミットにおける技術的な変更は多岐にわたりますが、特に以下の点が重要です。
-
os.FD
構造体の変更とメソッドの追加:src/lib/os/os_file.go
において、os.FD
構造体にプライベートフィールドfd int64
とname string
が追加されました。これにより、ファイルディスクリプタの数値だけでなく、そのファイルディスクリプタが関連付けられているファイル名も保持できるようになりました。Fd() int64
とName() string
というパブリックなメソッドが追加され、FD
オブジェクトからこれらの情報にアクセスできるようになりました。これにより、FD
の内部実装が隠蔽され、よりクリーンなAPIが提供されます。NewFD
関数もname
引数を取るように変更され、Stdin
,Stdout
,Stderr
といった標準FDも初期化時に適切な名前を持つようになりました(例:NewFD(0, "/dev/stdin")
)。os.Open
,os.Pipe
などの関数も、NewFD
の新しいシグネチャに合わせて更新されています。
-
stat
関連関数の導入とOS依存実装の分離:src/lib/os/os_types.go
が新規作成され、Dir
というOS非依存の構造体が定義されました。この構造体は、stat
システムコールが返すファイルメタデータ(デバイスID、inode番号、リンク数、モード、サイズ、タイムスタンプなど)を抽象化します。src/lib/os/os_file.go
にStat
,Fstat
,Lstat
という新しい関数が追加されました。これらの関数は、それぞれパス名、ファイルディスクリプタ、シンボリックリンクのパス名を受け取り、os.Dir
構造体を返します。src/lib/os/stat_amd64_darwin.go
とsrc/lib/os/stat_amd64_linux.go
が新規作成されました。これらは、それぞれのOS(DarwinとLinux)におけるsyscall.Stat_t
構造体からos.Dir
構造体への変換を行うdirFromStat
関数を実装しています。これにより、OS固有のstat
構造体の差異がGoのos.Dir
によって吸収され、クロスプラットフォームなstat
操作が可能になります。
-
システムコールにおける文字列処理の変更:
src/lib/syscall/syscall.go
において、StringToBytes
関数が削除され、代わりにStringBytePtr
関数が導入されました。StringToBytes
は、Goの文字列を固定サイズのバイト配列にコピーし、NULL終端を追加するものでした。これはバッファサイズの問題や、len(s) >= len(b)
の場合にfalse
を返すという扱いにくさがありました。StringBytePtr
は、Goの文字列を新しいバイトスライスにコピーし、NULL終端を追加した上で、そのバイトスライスの先頭要素へのポインタ(*byte
)を返します。これにより、システムコールに直接渡せるC言語スタイルの文字列ポインタを、より安全かつ柔軟に生成できるようになりました。
src/lib/syscall/file_linux.go
内のOpen
,Creat
,Stat
,Lstat
,Unlink
,Mkdir
といったファイルシステム関連のシステムコールラッパーが、StringToBytes
の代わりに新しく導入されたStringBytePtr
を使用するように変更されました。これにより、システムコールへの文字列引数の渡し方が統一され、コードの簡潔性と堅牢性が向上しています。
-
ビルドシステムの更新:
src/lib/os/Makefile
が更新され、新しいos_types.go
、stat_amd64_darwin.go
、stat_amd64_linux.go
ファイルがビルドプロセスに含まれるようになりました。特に、stat_$(GOARCH)_$(GOOS).$O
というパターンを使用することで、ビルドターゲットのアーキテクチャとOSに基づいて適切なstat
実装がリンクされるように設定されています。これは、Goのクロスコンパイルとポータビリティをサポートするための典型的なアプローチです。
これらの変更は、Go言語のファイルシステム抽象化層を大幅に改善し、OS間の差異を吸収しながら、より表現力豊かで安全なAPIを提供するための重要なステップでした。
コアとなるコードの変更箇所
src/lib/os/os_file.go
における FD
構造体と関連関数の変更
--- a/src/lib/os/os_file.go
+++ b/src/lib/os/os_file.go
@@ -9,20 +9,29 @@ import os "os"
// FDs are wrappers for file descriptors
type FD struct {
- Fd int64
+ fd int64;
+ name string;
}
-func NewFD(fd int64) *FD {
+func (fd *FD) Fd() int64 {
+ return fd.fd
+}
+
+func (fd *FD) Name() string {
+ return fd.name
+}
+
+func NewFD(fd int64, name string) *FD {
if fd < 0 {
return nil
}
- return &FD{fd}
+ return &FD{fd, name}
}
var (
- Stdin = NewFD(0);
- Stdout = NewFD(1);
- Stderr = NewFD(2);
+ Stdin = NewFD(0, "/dev/stdin");
+ Stdout = NewFD(1, "/dev/stdout");
+ Stderr = NewFD(2, "/dev/stderr");
)
const (
@@ -41,15 +50,15 @@ const (
func Open(name string, mode int, flags int) (fd *FD, err *Error) {
r, e := syscall.Open(name, int64(mode), int64(flags));
- return NewFD(r), ErrnoToError(e)
+ return NewFD(r, name), ErrnoToError(e)
}
func (fd *FD) Close() *Error {
if fd == nil {
return EINVAL
}
- r, e := syscall.Close(fd.Fd);
- fd.Fd = -1; // so it can't be closed again
+ r, e := syscall.Close(fd.fd);
+ fd.fd = -1; // so it can't be closed again
return ErrnoToError(e)
}
@@ -59,7 +68,7 @@ func (fd *FD) Read(b []byte) (ret int, err *Error) {
}
var r, e int64;
if len(b) > 0 { // because we access b[0]
- r, e = syscall.Read(fd.Fd, &b[0], int64(len(b)));
+ r, e = syscall.Read(fd.fd, &b[0], int64(len(b)));
if r < 0 {
r = 0
}
@@ -73,7 +82,7 @@ func (fd *FD) Write(b []byte) (ret int, err *Error) {
}
var r, e int64;
if len(b) > 0 { // because we access b[0]
- r, e = syscall.Write(fd.Fd, &b[0], int64(len(b)));
+ r, e = syscall.Write(fd.fd, &b[0], int64(len(b)));
if r < 0 {
r = 0
}
@@ -85,11 +94,7 @@ func (fd *FD) WriteString(s string) (ret int, err *Error) {\
if fd == nil {
return 0, EINVAL
}
- b := make([]byte, len(s)+1);
- if !syscall.StringToBytes(b, s) {
- return 0, EINVAL
- }
- r, e := syscall.Write(fd.Fd, &b[0], int64(len(s)));
+ r, e := syscall.Write(fd.fd, syscall.StringBytePtr(s), int64(len(s)));
if r < 0 {
r = 0
}
@@ -102,10 +107,37 @@ func Pipe() (fd1 *FD, fd2 *FD, err *Error) {\
if e != 0 {
return nil, nil, ErrnoToError(e)
}
- return NewFD(p[0]), NewFD(p[1]), nil
+ return NewFD(p[0], "|0"), NewFD(p[1], "|1"), nil
}
func Mkdir(name string, perm int) *Error {
r, e := syscall.Mkdir(name, int64(perm));
return ErrnoToError(e)
}
+
+func Stat(name string) (dir *Dir, err *Error) {
+ stat := new(syscall.Stat_t);
+ r, e := syscall.Stat(name, stat);
+ if e != 0 {
+ return nil, ErrnoToError(e)
+ }
+ return dirFromStat(name, new(Dir), stat), nil
+}
+
+func Fstat(fd *FD) (dir *Dir, err *Error) {
+ stat := new(syscall.Stat_t);
+ r, e := syscall.Fstat(fd.fd, stat);
+ if e != 0 {
+ return nil, ErrnoToError(e)
+ }
+ return dirFromStat(fd.name, new(Dir), stat), nil
+}
+
+func Lstat(name string) (dir *Dir, err *Error) {
+ stat := new(syscall.Stat_t);
+ r, e := syscall.Lstat(name, stat);
+ if e != 0 {
+ return nil, ErrnoToError(e)
+ }
+ return dirFromStat(name, new(Dir), stat), nil
+}
src/lib/syscall/syscall.go
における StringBytePtr
の導入
--- a/src/lib/syscall/syscall.go
+++ b/src/lib/syscall/syscall.go
@@ -16,13 +16,10 @@ func RawSyscall(trap int64, a1, a2, a3 int64) (r1, r2, err int64);\
* Used to convert file names to byte arrays for passing to kernel,
* but useful elsewhere too.
*/
-func StringToBytes(b []byte, s string) bool {
- if len(s) >= len(b) {
- return false
- }
+func StringBytePtr(s string) *byte {
+ a := make([]byte, len(s)+1);
for i := 0; i < len(s); i++ {
- b[i] = s[i]
+ a[i] = s[i];
}
- b[len(s)] = '\000'; // not necessary - memory is zeroed - but be explicit
- return true
+ return &a[0];
}
src/lib/os/os_types.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
// An operating-system independent representation of Unix data structures.
// OS-specific routines in this directory convert the OS-local versions to these.
// Result of stat64(2) etc.
type Dir struct {
Dev uint64;
Ino uint64;
Nlink uint64;
Mode uint32;
Uid uint32;
Gid uint32;
Rdev uint64;
Size uint64;
Blksize uint64;
Blocks uint64;
Atime_ns uint64; // nanoseconds since 1970
Mtime_ns uint64; // nanoseconds since 1970
Ctime_ns uint64; // nanoseconds since 1970
Name string;
}
src/lib/os/stat_amd64_linux.go
(新規ファイル、stat_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.
// AMD64, Linux
package os
import syscall "syscall"
import os "os"
func dirFromStat(name string, dir *Dir, stat *syscall.Stat_t) *Dir {
dir.Dev = stat.Dev;
dir.Ino = stat.Ino;
dir.Nlink = stat.Nlink;
dir.Mode = stat.Mode;
dir.Uid = stat.Uid;
dir.Gid = stat.Gid;
dir.Rdev = stat.Rdev;
dir.Size = uint64(stat.Size);
dir.Blksize = uint64(stat.Blksize);
dir.Blocks = uint64(stat.Blocks);
dir.Atime_ns = uint64(stat.Atime.Sec) * 1e9 + stat.Atime.Nsec;
dir.Mtime_ns = uint64(stat.Mtime.Sec) * 1e9 + stat.Mtime.Nsec;
dir.Ctime_ns = uint64(stat.Ctime.Sec) * 1e9 + stat.Atime.Nsec;
for i := len(name) - 1; i >= 0; i-- {
if name[i] == '/' {
name = name[i+1:len(name)];
break;
}
}
dir.Name = name;
return dir;
}
コアとなるコードの解説
os.FD
構造体と関連関数の変更 (src/lib/os/os_file.go
)
FD
構造体の変更: 以前はFd int64
のみを持っていたFD
構造体に、fd int64
(プライベート化)とname string
が追加されました。これにより、ファイルディスクリプタの数値だけでなく、そのファイルがオープンされた際のパス名もFD
オブジェクトに紐付けられるようになりました。fd
フィールドがプライベートになったことで、外部からは直接アクセスできなくなり、カプセル化が強化されています。Fd()
とName()
メソッドの追加:fd
とname
フィールドへのアクセスを提供するために、それぞれFd()
とName()
というゲッターメソッドが追加されました。これにより、FD
の内部表現に依存せずに、これらの情報を取得できるようになります。NewFD
関数の変更:NewFD
関数は、ファイルディスクリプタの数値だけでなく、そのファイルディスクリプタに関連するname
(通常はファイルパス)も引数として受け取るようになりました。これにより、FD
オブジェクトがより完全な情報を持つことができるようになります。- 標準FDの初期化:
Stdin
,Stdout
,Stderr
といったGoの標準ファイルディスクリプタも、新しいNewFD
のシグネチャに合わせて、それぞれ/dev/stdin
,/dev/stdout
,/dev/stderr
という名前で初期化されるようになりました。これは、これらのFDがどのリソースに対応しているかを明確にするのに役立ちます。 Open
,Close
,Read
,Write
,WriteString
,Pipe
の変更: これらの関数やメソッドは、FD
構造体の変更に合わせて、内部でfd.fd
(プライベートなフィールド)を使用するように修正されました。特にWriteString
では、後述のsyscall.StringBytePtr
が使用されるようになり、文字列のシステムコールへの渡し方が改善されています。Pipe
関数も、生成されるパイプのFDに|0
や|1
といった名前を付与するようになりました。
syscall.StringBytePtr
の導入 (src/lib/syscall/syscall.go
)
StringToBytes
の削除とStringBytePtr
の導入: 以前のStringToBytes
関数は、Goの文字列をC言語のNULL終端バイト配列に変換する際に、固定サイズのバッファを必要とし、バッファオーバーフローの可能性や、文字列が長すぎる場合の扱いに問題がありました。- 新しく導入された
StringBytePtr
関数は、Goの文字列s
を受け取り、その内容を新しいバイトスライスa
にコピーし、最後にNULL終端を追加します。そして、そのバイトスライスa
の先頭要素へのポインタ(*byte
)を返します。 - このアプローチにより、呼び出し側は固定サイズのバッファを事前に用意する必要がなくなり、Goの文字列の長さに応じて動的にメモリが確保されるため、より安全で柔軟な文字列変換が可能になります。システムコールは通常、C言語の関数として定義されており、文字列引数として
char*
(バイトポインタ)を期待するため、この*byte
型はシステムコールに直接渡すのに適しています。
os.Dir
構造体 (src/lib/os/os_types.go
)
- このファイルで定義された
Dir
構造体は、OSに依存しないファイルやディレクトリのメタデータ表現です。Unix系OSのstat_t
構造体から主要なフィールド(デバイスID、inode番号、リンク数、モード、UID、GID、サイズ、ブロックサイズ、ブロック数、アクセス/変更/作成タイムスタンプ、名前)を抽出し、Goの型にマッピングしています。 - この抽象化層により、Goのアプリケーションは、基盤となるOSがLinuxであろうとDarwinであろうと、統一された
Dir
構造体を通じてファイルメタデータにアクセスできるようになります。
OS固有の dirFromStat
関数 (src/lib/os/stat_amd64_linux.go
など)
stat_amd64_linux.go
とstat_amd64_darwin.go
(および他のOS/アーキテクチャ固有のファイル)は、それぞれのOSのsyscall.Stat_t
構造体からos.Dir
構造体への変換を行うdirFromStat
関数を実装しています。- これらの関数は、OS固有の
stat_t
フィールドをos.Dir
の対応するフィールドにマッピングします。例えば、Linuxではstat.Size
がint64
ですが、os.Dir.Size
はuint64
であるため、型変換が行われます。 - また、
name
引数からファイル名部分を抽出してdir.Name
に設定するロジックも含まれています。これは、stat
システムコールがパス名全体を受け取るのに対し、Dir.Name
にはファイル名のみを格納するためです。 - この分離された実装により、Goの
os
パッケージは、異なるOSのstat
システムコールの差異を透過的に扱うことができ、Go開発者はOSの違いを意識することなくファイルメタデータにアクセスできます。
これらの変更は、Go言語の初期段階におけるシステムプログラミングの基盤を固め、クロスプラットフォーム対応とAPIの使いやすさを大幅に向上させるための重要なステップでした。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語の
os
パッケージ: https://pkg.go.dev/os - Go言語の
syscall
パッケージ: https://pkg.go.dev/syscall - Unix
stat
man page (例: Linux):man 2 stat
(シェルで実行)
参考にした情報源リンク
- Go言語の初期のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語の設計に関する議論 (初期のメーリングリストなど): Goの初期の設計に関する情報は、主にGoの公式ブログやメーリングリストのアーカイブに散在しています。具体的なURLはコミットから直接特定できませんが、Goの歴史を辿る上で重要な情報源です。
- Unix系OSのシステムコールに関する一般的な情報源 (例: Wikipedia, man pages)。
- Go言語のソースコード自体。
- Go言語の
io
パッケージのStringBytes
の歴史的な変更に関する情報。