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

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

このコミットは、Go言語のNative Client (NaCl) 環境におけるファイルシステム (fs) の初期化を遅延させる(lazy initialization)ための変更を導入しています。これにより、Goプログラムの起動時パフォーマンスが向上し、特にファイルシステム操作が不要な場合のオーバーヘッドが削減されます。

コミット

commit bbe5c93e93922154d330cc4b7eecf148c830515c
Author: Shenghou Ma <minux@golang.org>
Date:   Tue Jul 1 18:24:43 2014 -0400

    misc/nacl, syscall: lazily initialize fs on nacl.
    On amd64, the real time is reduced from 176.76s to 140.26s.
    On ARM, the real time is reduced from 921.61s to 726.30s.
    
    LGTM=rsc
    R=rsc
    CC=golang-codereviews
    https://golang.org/cl/101580043

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

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

元コミット内容

このコミットは、GoのNative Client (NaCl) ターゲットにおいて、ファイルシステムの初期化を遅延実行するように変更します。これにより、amd64環境では実行時間が176.76秒から140.26秒に、ARM環境では921.61秒から726.30秒に短縮されるというパフォーマンス改善が報告されています。

変更の背景

GoプログラムがNative Client (NaCl) 環境で実行される際、ファイルシステムがプログラム起動時に常に初期化されていました。しかし、全てのGoプログラムがファイルシステムを必要とするわけではありません。ファイルシステムを必要としないプログラムの場合でも、その初期化処理が起動時間に不要なオーバーヘッドとして加算されていました。このコミットの目的は、ファイルシステムの初期化を、実際にファイルシステム操作が要求されるまで遅延させることで、Goプログラムの起動時間を短縮し、全体的なパフォーマンスを向上させることにあります。特に、NaCl環境のようなリソースが限られた、あるいは起動時間が重要な環境において、この最適化は大きな意味を持ちます。

前提知識の解説

Native Client (NaCl)

Native Client (NaCl) は、Googleが開発したサンドボックス技術で、ウェブブラウザ内でネイティブコード(C/C++など)を安全に実行することを可能にします。Go言語もNaClをターゲットとしてサポートしており、GoプログラムをNaClモジュールとしてコンパイルし、ウェブアプリケーション内で実行することができます。NaCl環境は、通常のオペレーティングシステムとは異なる独自のサンドボックス化されたファイルシステムやシステムコールインターフェースを持っています。

遅延初期化 (Lazy Initialization)

遅延初期化とは、オブジェクトやリソースの初期化を、それが実際に必要とされる時点まで遅らせる設計パターンです。これにより、プログラムの起動時や、特定の機能が全く使用されない場合に、不要なリソースの割り当てや計算を避けることができます。結果として、起動時間の短縮、メモリ使用量の削減、パフォーマンスの向上が期待できます。

Go言語の init() 関数

Go言語では、各パッケージにinit()関数を定義することができます。init()関数は、パッケージがインポートされた際に、main()関数が実行されるよりも前に自動的に実行されます。複数のinit()関数がある場合、それらは定義された順序で実行されます。通常、init()関数は、プログラムの起動時に必要な初期設定やリソースの準備に使用されます。

sync.Once

Go言語のsyncパッケージに含まれるOnce型は、特定の処理が一度だけ実行されることを保証するためのメカニズムです。Once.Do(f func())メソッドを呼び出すと、引数として渡された関数fが、Doが初めて呼び出されたときに一度だけ実行されます。その後のDoの呼び出しでは、fは実行されません。これは、複数のゴルーチンから同時に呼び出された場合でもスレッドセーフに一度だけ実行されることを保証するため、遅延初期化のパターンで非常に有用です。

Go言語の syscall パッケージ

syscallパッケージは、Goプログラムが基盤となるオペレーティングシステムのシステムコールに直接アクセスするためのインターフェースを提供します。ファイル操作、プロセス管理、ネットワーク通信など、OSレベルの機能を利用する際に使用されます。NaCl環境では、このパッケージを通じてNaCl独自のシステムコールインターフェースとやり取りします。

技術的詳細

このコミットの核心は、GoのNaCl環境におけるファイルシステム初期化のタイミングを、プログラム起動時(init()関数内)から、実際にファイルシステム操作が要求される時点まで遅延させることです。

具体的には、以下のメカニズムが導入されています。

  1. fsinit 関数の導入: src/pkg/syscall/fs_nacl.govar fsinit = func() {} というグローバル変数が導入されました。この関数は、ファイルシステムの実際の初期化ロジックをカプセル化します。
  2. mkzip.go による fsinit の設定: misc/nacl/mkzip.go は、NaCl向けにGoプログラムをパッケージ化する際に、fsinit 関数を生成されるGoパッケージの init() 関数内で設定するように変更されました。この設定では、sync.Once を使用して、fsinit が呼び出された際に、内部のファイルシステム初期化処理(unzip関数によるファイルシステムデータの展開など)が一度だけ実行されるように保証します。
  3. システムコールからの fsinit 呼び出し: src/pkg/syscall/fs_nacl.go 内のOpen, Stat, unlink, Chmod, Chown, UtimesNano, Link, Rename, Truncate, Chdirといった主要なファイルシステム関連のシステムコール関数が、処理の開始時に fsinit() を呼び出すように変更されました。これにより、これらの関数が初めて呼び出されたときにのみ、ファイルシステムの初期化が実行されます。
  4. init() 関数内の保護: src/pkg/syscall/fs_nacl.goinit() 関数自体も変更され、一時的に fsinit を空の関数に設定し、defer を使って元の fsinit に戻すという処理が追加されました。これは、init() 関数内で実行される他の初期化処理(例: /dev/tmp ディレクトリの作成)が、意図せずファイルシステムの初期化をトリガーしてしまうことを防ぐための安全策です。

この変更により、ファイルシステムを全く使用しないGoプログラムは、ファイルシステムの初期化にかかる時間を完全にスキップできるようになり、起動時間が大幅に短縮されます。ファイルシステムを使用するプログラムであっても、初期化は最初のファイルシステム操作時に一度だけ行われるため、全体的なパフォーマンスへの影響は最小限に抑えられます。

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

misc/nacl/mkzip.go

mkzip.go は、GoのNaClバイナリをパッケージ化する際に、ファイルシステム初期化ロジックを生成されるGoコードに埋め込む役割を担っています。

--- a/misc/nacl/mkzip.go
+++ b/misc/nacl/mkzip.go
@@ -71,7 +71,13 @@ func main() {
 
 	var w io.Writer = zf
 	if *gopackage != "" {
-		fmt.Fprintf(zf, "package %s\\n\\nfunc init() {\\n\\tunzip(\\\"\", *gopackage)\n
+		fmt.Fprintf(zf, `package %s
+import "sync"
+func init() {
+	var once sync.Once
+	fsinit = func() {
+		once.Do(func() {
+			unzip("`, *gopackage)
 		gw := &goWriter{b: bufio.NewWriter(w)}\
 		defer func() {
 			if err := gw.Close(); err != nil {
@@ -214,7 +220,7 @@ func (w *goWriter) Write(b []byte) (int, error) {
 }
 
 func (w *goWriter) Close() error {
-	fmt.Fprintf(w.b, "\\\")\\n}\\n")
+	fmt.Fprintf(w.b, "\\\")\\n\\t\\t})\\n\\t}\\n}\")
 	w.b.Flush()
 	return nil
 }

この変更により、生成されるGoパッケージのinit()関数は、syncパッケージをインポートし、sync.Onceインスタンスonceを宣言します。そして、fsinitグローバル変数に、once.Doを使ってunzip関数を呼び出す匿名関数を代入します。これにより、unzipfsinitが初めて呼び出されたときに一度だけ実行されるようになります。

src/pkg/syscall/fs_nacl.go

fs_nacl.go は、NaCl環境におけるファイルシステム関連のシステムコールを実装しています。

--- a/src/pkg/syscall/fs_nacl.go
+++ b/src/pkg/syscall/fs_nacl.go
@@ -79,8 +79,13 @@ func newFsys() *fsys {
 }
 
 var fs = newFsys()
+var fsinit = func() {}\n
 
 func init() {
+\t// do not trigger loading of zipped file system here
+\toldFsinit := fsinit
+\tdefer func() { fsinit = oldFsinit }()
+\tfsinit = func() {}\n
 	Mkdir("/dev", 0555)
 	Mkdir("/tmp\", 0777)
 	mkdev("/dev/null", 0666, openNull)
@@ -93,7 +98,7 @@ func chdirEnv() {
 \tpwd, ok := Getenv(\"NACLPWD\")
 \tif ok {
-\t\tChdir(pwd)
+\t\tchdir(pwd)
 \t}\n
 }\n
@@ -465,6 +470,7 @@ func (f *fsysFile) pwriteLocked(b []byte, offset int64) (int, error) {
 // Standard Unix system calls.\n
 
 func Open(path string, openmode int, perm uint32) (fd int, err error) {
+\tfsinit()\n
 \tfs.mu.Lock()
 \tdefer fs.mu.Unlock()
 \tf, err := fs.open(path, openmode, perm&0777|S_IFREG)\n
@@ -487,6 +493,7 @@ func Getcwd(buf []byte) (n int, err error) {
 }\n
 
 func Stat(path string, st *Stat_t) error {
+\tfsinit()\n
 \tfs.mu.Lock()
 \tdefer fs.mu.Unlock()
 \tip, _, err := fs.namei(path, false)\n
@@ -502,6 +509,7 @@ func Lstat(path string, st *Stat_t) error {
 }\n
 
 func unlink(path string, isdir bool) error {
+\tfsinit()\n
 \tfs.mu.Lock()
 \tdefer fs.mu.Unlock()
 \tdp, elem, err := fs.namei(path, true)\n
@@ -543,6 +551,7 @@ func Rmdir(path string) error {
 }\n
 
 func Chmod(path string, mode uint32) error {
+\tfsinit()\n
 \tfs.mu.Lock()
 \tdefer fs.mu.Unlock()
 \tip, _, err := fs.namei(path, false)\n
@@ -565,6 +574,7 @@ func Fchmod(fd int, mode uint32) error {
 }\n
 
 func Chown(path string, uid, gid int) error {
+\tfsinit()\n
 \tfs.mu.Lock()
 \tdefer fs.mu.Unlock()
 \tip, _, err := fs.namei(path, false)\n
@@ -598,6 +608,7 @@ func UtimesNano(path string, ts []Timespec) error {
 \tif len(ts) != 2 {\n
 \t\treturn EINVAL\n
 \t}\n+\tfsinit()\n
 \tfs.mu.Lock()
 \tdefer fs.mu.Unlock()
 \tip, _, err := fs.namei(path, false)\n
@@ -612,6 +623,7 @@ func UtimesNano(path string, ts []Timespec) error {
 }\n
 
 func Link(path, link string) error {
+\tfsinit()\n
 \tip, _, err := fs.namei(path, false)\n
 \tif err != nil {\n
 \t\treturn err\n
@@ -628,6 +640,7 @@ func Link(path, link string) error {
 }\n
 
 func Rename(from, to string) error {
+\tfsinit()\n
 \tfdp, felem, err := fs.namei(from, true)\n
 \tif err != nil {\n
 \t\treturn err\n
@@ -664,6 +677,7 @@ func (fs *fsys) truncate(ip *inode, length int64) error {\n }\n
 
 func Truncate(path string, length int64) error {
+\tfsinit()\n
 \tfs.mu.Lock()
 \tdefer fs.mu.Unlock()
 \tip, _, err := fs.namei(path, false)\n
@@ -684,6 +698,11 @@ func Ftruncate(fd int, length int64) error {
 }\n
 
 func Chdir(path string) error {
+\tfsinit()\n
+\treturn chdir(path)\n
+}\n+\n+func chdir(path string) error {\n
 \tfs.mu.Lock()
 \tdefer fs.mu.Unlock()
 \tip, _, err := fs.namei(path, false)\n
@@ -723,8 +742,6 @@ func Fsync(fd int) error {
 // Special devices.\n
 
 func mkdev(path string, mode uint32, open func() (devFile, error)) error {
-\tfs.mu.Lock()\n
-\tfs.mu.Unlock()\n
 \tf, err := fs.open(path, O_CREATE|O_RDONLY|O_EXCL, S_IFCHR|mode)\n
 \tif err != nil {\n
 \t\treturn err\n

このファイルでは、fsinitグローバル変数の宣言、init()関数でのfsinitの一時的な無効化と復元、そしてOpen, Statなどの多くのファイルシステム関連関数でのfsinit()の呼び出しが追加されています。また、Chdir関数がfsinit()を呼び出す新しいchdir関数に処理を委譲するように変更され、mkdev関数から不要なロックが削除されています。

コアとなるコードの解説

このコミットの主要な変更は、GoのNaCl環境におけるファイルシステムの初期化ロジックを、init()関数からfsinitという新しいグローバル関数に移動し、そのfsinit関数がsync.Onceによって一度だけ実行されるように制御することです。

  1. fsinitの導入とsync.Onceによる制御:

    • src/pkg/syscall/fs_nacl.govar fsinit = func() {} が追加されました。これは、ファイルシステムの初期化処理をカプセル化するための関数ポインタです。
    • misc/nacl/mkzip.go で生成されるGoコードのinit()関数内で、このfsinitに実際の初期化ロジックが設定されます。具体的には、sync.Onceのインスタンスonceが作成され、fsinitonce.Do(func() { unzip(...) })を呼び出す関数として定義されます。これにより、fsinitが複数回呼び出されても、unzip(ファイルシステムデータの展開)は一度だけ実行されることが保証されます。
  2. システムコールからの遅延初期化のトリガー:

    • src/pkg/syscall/fs_nacl.go 内のOpen, Stat, unlink, Chmodなど、ファイルシステムにアクセスするほぼ全てのシステムコール関数が、その処理の冒頭でfsinit()を呼び出すように変更されました。
    • これにより、Goプログラムがファイルシステム操作を初めて実行する際に、fsinit()が呼び出され、sync.Onceのメカニズムによってファイルシステムの初期化が実行されます。一度初期化が完了すれば、それ以降のfsinit()の呼び出しは何も行いません。
  3. init()関数内の保護メカニズム:

    • src/pkg/syscall/fs_nacl.goinit()関数自体も変更されました。このinit()関数は、/dev/tmpなどの基本的なディレクトリを作成しますが、これらの操作が間接的にfsinitを呼び出してしまう可能性を考慮し、一時的にfsinitを空の関数に設定し、deferを使って元のfsinitに戻すという処理が追加されました。これは、init()関数が実行されている間は、ファイルシステムの完全な初期化が意図せずトリガーされないようにするための防御的なプログラミングです。
  4. Chdir関数のリファクタリング:

    • Chdir関数は、fsinit()を呼び出した後、実際のディレクトリ変更ロジックを新しい内部関数chdirに委譲するように変更されました。これは、他のシステムコール関数と同様に、Chdirが呼び出されたときにファイルシステムの初期化が確実に行われるようにするためです。

これらの変更により、GoのNaClプログラムは、ファイルシステムを必要としない限り、その初期化コストを完全に回避できるようになり、起動時間の短縮という明確なパフォーマンス上の利点が得られました。

関連リンク

  • Go Native Client (NaCl) の公式ドキュメント (当時の情報に基づく)
  • Go言語の sync パッケージに関するドキュメント
  • Go言語の init 関数に関する公式ドキュメント

参考にした情報源リンク

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

このコミットは、Go言語のNative Client (NaCl) 環境におけるファイルシステム (fs) の初期化を遅延させる(lazy initialization)ための変更を導入しています。これにより、Goプログラムの起動時パフォーマンスが向上し、特にファイルシステム操作が不要な場合のオーバーヘッドが削減されます。

コミット

commit bbe5c93e93922154d330cc4b7eecf148c830515c
Author: Shenghou Ma <minux@golang.org>
Date:   Tue Jul 1 18:24:43 2014 -0400

    misc/nacl, syscall: lazily initialize fs on nacl.
    On amd64, the real time is reduced from 176.76s to 140.26s.
    On ARM, the real time is reduced from 921.61s to 726.30s.
    
    LGTM=rsc
    R=rsc
    CC=golang-codereviews
    https://golang.org/cl/101580043

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

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

元コミット内容

このコミットは、GoのNative Client (NaCl) ターゲットにおいて、ファイルシステムの初期化を遅延実行するように変更します。これにより、amd64環境では実行時間が176.76秒から140.26秒に、ARM環境では921.61秒から726.30秒に短縮されるというパフォーマンス改善が報告されています。

変更の背景

GoプログラムがNative Client (NaCl) 環境で実行される際、ファイルシステムがプログラム起動時に常に初期化されていました。しかし、全てのGoプログラムがファイルシステムを必要とするわけではありません。ファイルシステムを必要としないプログラムの場合でも、その初期化処理が起動時間に不要なオーバーヘッドとして加算されていました。このコミットの目的は、ファイルシステムの初期化を、実際にファイルシステム操作が要求されるまで遅延させることで、Goプログラムの起動時間を短縮し、全体的なパフォーマンスを向上させることにあります。特に、NaCl環境のようなリソースが限られた、あるいは起動時間が重要な環境において、この最適化は大きな意味を持ちます。

前提知識の解説

Native Client (NaCl)

Native Client (NaCl) は、Googleが開発したサンドボックス技術で、ウェブブラウザ内でネイティブコード(C/C++など)を安全に実行することを可能にします。Go言語もNaClをターゲットとしてサポートしており、GoプログラムをNaClモジュールとしてコンパイルし、ウェブアプリケーション内で実行することができます。NaCl環境は、通常のオペレーティングシステムとは異なる独自のサンドボックス化されたファイルシステムやシステムコールインターフェースを持っています。

遅延初期化 (Lazy Initialization)

遅延初期化とは、オブジェクトやリソースの初期化を、それが実際に必要とされる時点まで遅らせる設計パターンです。これにより、プログラムの起動時や、特定の機能が全く使用されない場合に、不要なリソースの割り当てや計算を避けることができます。結果として、起動時間の短縮、メモリ使用量の削減、パフォーマンスの向上が期待できます。

Go言語の init() 関数

Go言語では、各パッケージにinit()関数を定義することができます。init()関数は、パッケージがインポートされた際に、main()関数が実行されるよりも前に自動的に実行されます。複数のinit()関数がある場合、それらは定義された順序で実行されます。通常、init()関数は、プログラムの起動時に必要な初期設定やリソースの準備に使用されます。

sync.Once

Go言語のsyncパッケージに含まれるOnce型は、特定の処理が一度だけ実行されることを保証するためのメカニズムです。Once.Do(f func())メソッドを呼び出すと、引数として渡された関数fが、Doが初めて呼び出されたときに一度だけ実行されます。その後のDoの呼び出しでは、fは実行されません。これは、複数のゴルーチンから同時に呼び出された場合でもスレッドセーフに一度だけ実行されることを保証するため、遅延初期化のパターンで非常に有用です。

Go言語の syscall パッケージ

syscallパッケージは、Goプログラムが基盤となるオペレーティングシステムのシステムコールに直接アクセスするためのインターフェースを提供します。ファイル操作、プロセス管理、ネットワーク通信など、OSレベルの機能を利用する際に使用されます。NaCl環境では、このパッケージを通じてNaCl独自のシステムコールインターフェースとやり取りします。

技術的詳細

このコミットの核心は、GoのNaCl環境におけるファイルシステム初期化のタイミングを、プログラム起動時(init()関数内)から、実際にファイルシステム操作が要求される時点まで遅延させることです。

具体的には、以下のメカニズムが導入されています。

  1. fsinit 関数の導入: src/pkg/syscall/fs_nacl.govar fsinit = func() {} というグローバル変数が導入されました。この関数は、ファイルシステムの実際の初期化ロジックをカプセル化します。
  2. mkzip.go による fsinit の設定: misc/nacl/mkzip.go は、NaCl向けにGoプログラムをパッケージ化する際に、fsinit 関数を生成されるGoパッケージの init() 関数内で設定するように変更されました。この設定では、sync.Once を使用して、fsinit が呼び出された際に、内部のファイルシステム初期化処理(unzip関数によるファイルシステムデータの展開など)が一度だけ実行されるように保証します。
  3. システムコールからの fsinit 呼び出し: src/pkg/syscall/fs_nacl.go 内のOpen, Stat, unlink, Chmod, Chown, UtimesNano, Link, Rename, Truncate, Chdirといった主要なファイルシステム関連のシステムコール関数が、処理の開始時に fsinit() を呼び出すように変更されました。これにより、これらの関数が初めて呼び出されたときにのみ、ファイルシステムの初期化が実行されます。
  4. init() 関数内の保護: src/pkg/syscall/fs_nacl.goinit() 関数自体も変更され、一時的に fsinit を空の関数に設定し、defer を使って元の fsinit に戻すという処理が追加されました。これは、init() 関数内で実行される他の初期化処理(例: /dev/tmp ディレクトリの作成)が、意図せずファイルシステムの初期化をトリガーしてしまうことを防ぐための安全策です。

この変更により、ファイルシステムを全く使用しないGoプログラムは、ファイルシステムの初期化にかかる時間を完全にスキップできるようになり、起動時間が大幅に短縮されます。ファイルシステムを使用するプログラムであっても、初期化は最初のファイルシステム操作時に一度だけ行われるため、全体的なパフォーマンスへの影響は最小限に抑えられます。

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

misc/nacl/mkzip.go

mkzip.go は、GoのNaClバイナリをパッケージ化する際に、ファイルシステム初期化ロジックを生成されるGoコードに埋め込む役割を担っています。

--- a/misc/nacl/mkzip.go
+++ b/misc/nacl/mkzip.go
@@ -71,7 +71,13 @@ func main() {
 
 	var w io.Writer = zf
 	if *gopackage != "" {
-		fmt.Fprintf(zf, "package %s\\n\\nfunc init() {\\n\\tunzip(\\\"\", *gopackage)\n
+		fmt.Fprintf(zf, `package %s
+import "sync"
+func init() {
+	var once sync.Once
+	fsinit = func() {
+		once.Do(func() {
+			unzip("`, *gopackage)
 		gw := &goWriter{b: bufio.NewWriter(w)}\
 		defer func() {
 			if err := gw.Close(); err != nil {
@@ -214,7 +220,7 @@ func (w *goWriter) Write(b []byte) (int, error) {
 }
 
 func (w *goWriter) Close() error {
-	fmt.Fprintf(w.b, "\\\")\\n}\\n")
+	fmt.Fprintf(w.b, "\\\")\\n\\t\\t})\\n\\t}\\n}\")
 	w.b.Flush()
 	return nil
 }

この変更により、生成されるGoパッケージのinit()関数は、syncパッケージをインポートし、sync.Onceインスタンスonceを宣言します。そして、fsinitグローバル変数に、once.Doを使ってunzip関数を呼び出す匿名関数を代入します。これにより、unzipfsinitが初めて呼び出されたときに一度だけ実行されるようになります。

src/pkg/syscall/fs_nacl.go

fs_nacl.go は、NaCl環境におけるファイルシステム関連のシステムコールを実装しています。

--- a/src/pkg/syscall/fs_nacl.go
+++ b/src/pkg/syscall/fs_nacl.go
@@ -79,8 +79,13 @@ func newFsys() *fsys {
 }
 
 var fs = newFsys()
+var fsinit = func() {}\n
 
 func init() {
+\t// do not trigger loading of zipped file system here
+\toldFsinit := fsinit
+\tdefer func() { fsinit = oldFsinit }()
+\tfsinit = func() {}\n
 	Mkdir("/dev", 0555)
 	Mkdir("/tmp\", 0777)
 	mkdev("/dev/null", 0666, openNull)
@@ -93,7 +98,7 @@ func chdirEnv() {
 \tpwd, ok := Getenv(\"NACLPWD\")
 \tif ok {
-\t\tChdir(pwd)
+\t\tchdir(pwd)
 \t}\n
 }\n
@@ -465,6 +470,7 @@ func (f *fsysFile) pwriteLocked(b []byte, offset int64) (int, error) {
 // Standard Unix system calls.\n
 
 func Open(path string, openmode int, perm uint32) (fd int, err error) {
+\tfsinit()\n
 \tfs.mu.Lock()
 \tdefer fs.mu.Unlock()
 \tf, err := fs.open(path, openmode, perm&0777|S_IFREG)\n
@@ -487,6 +493,7 @@ func Getcwd(buf []byte) (n int, err error) {
 }\n
 
 func Stat(path string, st *Stat_t) error {
+\tfsinit()\n
 \tfs.mu.Lock()
 \tdefer fs.mu.Unlock()
 \tip, _, err := fs.namei(path, false)\n
@@ -502,6 +509,7 @@ func Lstat(path string, st *Stat_t) error {
 }\n
 
 func unlink(path string, isdir bool) error {
+\tfsinit()\n
 \tfs.mu.Lock()
 \tdefer fs.mu.Unlock()
 \tdp, elem, err := fs.namei(path, true)\n
@@ -543,6 +551,7 @@ func Rmdir(path string) error {
 }\n
 
 func Chmod(path string, mode uint32) error {
+\tfsinit()\n
 \tfs.mu.Lock()
 \tdefer fs.mu.Unlock()
 \tip, _, err := fs.namei(path, false)\n
@@ -565,6 +574,7 @@ func Fchmod(fd int, mode uint32) error {
 }\n
 
 func Chown(path string, uid, gid int) error {
+\tfsinit()\n
 \tfs.mu.Lock()
 \tdefer fs.mu.Unlock()
 \tip, _, err := fs.namei(path, false)\n
@@ -598,6 +608,7 @@ func UtimesNano(path string, ts []Timespec) error {
 \tif len(ts) != 2 {\n
 \t\treturn EINVAL\n
 \t}\n+\tfsinit()\n
 \tfs.mu.Lock()
 \tdefer fs.mu.Unlock()
 \tip, _, err := fs.namei(path, false)\n
@@ -612,6 +623,7 @@ func UtimesNano(path string, ts []Timespec) error {
 }\n
 
 func Link(path, link string) error {
+\tfsinit()\n
 \tip, _, err := fs.namei(path, false)\n
 \tif err != nil {\n
 \t\treturn err\n
@@ -628,6 +640,7 @@ func Link(path, link string) error {
 }\n
 
 func Rename(from, to string) error {
+\tfsinit()\n
 \tfdp, felem, err := fs.namei(from, true)\n
 \tif err != nil {\n
 \t\treturn err\n
@@ -664,6 +677,7 @@ func (fs *fsys) truncate(ip *inode, length int64) error {\n }\n
 
 func Truncate(path string, length int64) error {
+\tfsinit()\n
 \tfs.mu.Lock()
 \tdefer fs.mu.Unlock()
 \tip, _, err := fs.namei(path, false)\n
@@ -684,6 +698,11 @@ func Ftruncate(fd int, length int64) error {
 }\n
 

 func Chdir(path string) error {
+\tfsinit()\n
+\treturn chdir(path)\n
+}\n+\n+func chdir(path string) error {\n
 \tfs.mu.Lock()
 \tdefer fs.mu.Unlock()
 \tip, _, err := fs.namei(path, false)\n
@@ -723,8 +742,6 @@ func Fsync(fd int) error {
 // Special devices.\n
 
 func mkdev(path string, mode uint32, open func() (devFile, error)) error {
-\tfs.mu.Lock()\n
-\tfs.mu.Unlock()\n
 \tf, err := fs.open(path, O_CREATE|O_RDONLY|O_EXCL, S_IFCHR|mode)\n
 \tif err != nil {\n
 \t\treturn err\n

このファイルでは、fsinitグローバル変数の宣言、init()関数でのfsinitの一時的な無効化と復元、そしてOpen, Statなどの多くのファイルシステム関連関数でのfsinit()の呼び出しが追加されています。また、Chdir関数がfsinit()を呼び出す新しいchdir関数に処理を委譲するように変更され、mkdev関数から不要なロックが削除されています。

コアとなるコードの解説

このコミットの主要な変更は、GoのNaCl環境におけるファイルシステムの初期化ロジックを、init()関数からfsinitという新しいグローバル関数に移動し、そのfsinit関数がsync.Onceによって一度だけ実行されるように制御することです。

  1. fsinitの導入とsync.Onceによる制御:

    • src/pkg/syscall/fs_nacl.govar fsinit = func() {} が追加されました。これは、ファイルシステムの初期化処理をカプセル化するための関数ポインタです。
    • misc/nacl/mkzip.go で生成されるGoコードのinit()関数内で、このfsinitに実際の初期化ロジックが設定されます。具体的には、sync.Onceのインスタンスonceが作成され、fsinitonce.Do(func() { unzip(...) })を呼び出す関数として定義されます。これにより、fsinitが複数回呼び出されても、unzip(ファイルシステムデータの展開)は一度だけ実行されることが保証されます。
  2. システムコールからの遅延初期化のトリガー:

    • src/pkg/syscall/fs_nacl.go 内のOpen, Stat, unlink, Chmodなど、ファイルシステムにアクセスするほぼ全てのシステムコール関数が、その処理の冒頭でfsinit()を呼び出すように変更されました。
    • これにより、Goプログラムがファイルシステム操作を初めて実行する際に、fsinit()が呼び出され、sync.Onceのメカニズムによってファイルシステムの初期化が実行されます。一度初期化が完了すれば、それ以降のfsinit()の呼び出しは何も行いません。
  3. init()関数内の保護メカニズム:

    • src/pkg/syscall/fs_nacl.goinit()関数自体も変更されました。このinit()関数は、/dev/tmpなどの基本的なディレクトリを作成しますが、これらの操作が間接的にfsinitを呼び出してしまう可能性を考慮し、一時的にfsinitを空の関数に設定し、deferを使って元のfsinitに戻すという処理が追加されました。これは、init()関数が実行されている間は、ファイルシステムの完全な初期化が意図せずトリガーされないようにするための防御的なプログラミングです。
  4. Chdir関数のリファクタリング:

    • Chdir関数は、fsinit()を呼び出した後、実際のディレクトリ変更ロジックを新しい内部関数chdirに委譲するように変更されました。これは、他のシステムコール関数と同様に、Chdirが呼び出されたときにファイルシステムの初期化が確実に行われるようにするためです。

これらの変更により、GoのNaClプログラムは、ファイルシステムを必要としない限り、その初期化コストを完全に回避できるようになり、起動時間の短縮という明確なパフォーマンス上の利点が得られました。

関連リンク

  • Go Native Client (NaCl) の公式ドキュメント (当時の情報に基づく)
  • Go言語の sync パッケージに関するドキュメント
  • Go言語の init 関数に関する公式ドキュメント

参考にした情報源リンク