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

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

このコミットは、Go言語のsyscallパッケージにおけるPlan 9環境での環境変数管理メカニズムを改善するものです。具体的には、環境変数をキャッシュする機能を追加し、環境変数へのアクセス時に発生するシステムコール数を大幅に削減することを目的としています。これにより、環境変数を頻繁に参照するプログラムのパフォーマンスが向上します。

コミット

commit 1583931bcf522c4128087f0fb7dc84c4caa2af28
Author: Anthony Martin <ality@pbrane.org>
Date:   Tue Jan 31 18:14:02 2012 -0800

    syscall: cache environment variables on Plan 9.

    This can drastically reduce the number of system
    calls made by programs that repeatedly query the
    environment.

    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5599054

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

https://github.com/golang/go/commit/1583931bcf522c4128087f0fb7dc84c4caa2af28

元コミット内容

このコミットは、Go言語のsyscallパッケージ内のenv_plan9.goファイルに対して行われた変更です。主な目的は、Plan 9オペレーティングシステム上での環境変数へのアクセスを最適化することです。以前の実装では、環境変数を取得するたびにファイルシステムへのアクセス(システムコール)が発生していました。この変更により、環境変数をメモリ上にキャッシュすることで、繰り返し環境変数を参照する際のシステムコール数を劇的に削減し、パフォーマンスを向上させます。

変更の背景

Plan 9オペレーティングシステムでは、「全てがファイルである」という哲学が徹底されており、環境変数も例外ではありません。各環境変数は/envディレクトリ内のファイルとして表現されます。例えば、FOOという環境変数の値を取得するには、/env/FOOというファイルを開いてその内容を読み取るという操作が必要になります。

Go言語のsyscallパッケージは、オペレーティングシステムの低レベルな機能にアクセスするためのインターフェースを提供します。Plan 9環境におけるsyscallパッケージのGetenv関数は、この/envディレクトリのファイルシステム操作を直接行っていました。

しかし、環境変数を頻繁に読み取るようなアプリケーション(例えば、設定値を繰り返し参照する、あるいは多数のプロセスを起動するようなケース)では、このファイルシステムアクセスがボトルネックとなる可能性がありました。ファイルシステムアクセスは、メモリからのデータ読み出しに比べてはるかにコストの高い操作であり、システムコールを伴うため、CPUオーバーヘッドも大きくなります。

このコミットは、このようなパフォーマンス上の課題を解決するために導入されました。環境変数を一度読み込んでメモリ上にキャッシュすることで、2回目以降のアクセスでは高速なメモリ読み出しで対応できるようになり、システムコール数を削減することが可能になります。

前提知識の解説

  • Plan 9 from Bell Labs: ベル研究所で開発された分散オペレーティングシステムです。その設計思想の核となるのは「全てがファイルである」という原則で、デバイス、プロセス、ネットワークリソースなど、あらゆるものがファイルシステムを通じてアクセスされます。環境変数も/envという特殊なディレクトリ内のファイルとして扱われます。
  • Go言語のsyscallパッケージ: Go標準ライブラリの一部で、オペレーティングシステムが提供するシステムコールへの低レベルなインターフェースを提供します。これにより、GoプログラムはOSのカーネル機能に直接アクセスできます。
  • 環境変数: オペレーティングシステムがプロセスに提供する、キーと値のペアからなる設定情報です。プログラムの動作を外部から制御するために広く利用されます。
  • システムコール: ユーザー空間で動作するプログラムが、カーネル空間で動作するオペレーティングシステムのサービス(ファイルI/O、メモリ管理、プロセス管理など)を要求するためのメカニズムです。システムコールは、ユーザーモードからカーネルモードへのコンテキストスイッチを伴うため、比較的コストの高い操作です。
  • キャッシュ: 頻繁にアクセスされるデータを、より高速なストレージ(この場合はメモリ)に一時的に保存しておくことで、元のデータソースへのアクセス回数を減らし、全体的なパフォーマンスを向上させる技術です。
  • sync.Once: Go言語のsyncパッケージが提供するプリミティブの一つで、特定の関数が複数回呼び出されても、その関数が一度だけ実行されることを保証します。これは、遅延初期化(lazy initialization)や、リソースの単一インスタンス生成などに非常に有用です。
  • sync.RWMutex: Go言語のsyncパッケージが提供する読み書きロック(Reader-Writer Mutex)です。複数のゴルーチンが同時にデータを読み取ることは許可しますが、書き込みは一度に一つのゴルーチンのみに制限します。これにより、読み取りが頻繁で書き込みが少ないデータ構造において、高い並行性を維持しつつデータの一貫性を保証できます。

技術的詳細

このコミットの技術的な核心は、Plan 9の環境変数をメモリ上にキャッシュし、そのキャッシュへのアクセスを並行性セーフにするためのGo言語の並行性プリミティブの活用です。

  1. 環境変数キャッシュの導入:
    • env map[string]stringというグローバルマップが導入され、環境変数のキーと値をメモリ上に保持します。
  2. 遅延初期化と単一実行の保証:
    • envOnce sync.Onceが導入され、copyenv()関数が一度だけ実行されることを保証します。copyenv()は、/envディレクトリを読み込み、全ての環境変数をenvマップにロードする役割を担います。これにより、環境変数のキャッシュは、最初にGetenvSetenvClearenv、またはEnvironが呼び出されたときに一度だけ初期化されます。
  3. 並行性セーフなアクセス:
    • envLock sync.RWMutexが導入され、envマップへの並行アクセスを保護します。
      • GetenvおよびEnviron(読み取り操作)では、envLock.RLock()envLock.RUnlock()を使用して読み取りロックを取得・解放します。これにより、複数のゴルーチンが同時に環境変数を読み取ることができます。
      • SetenvおよびClearenv(書き込み操作)では、envLock.Lock()envLock.Unlock()を使用して書き込みロックを取得・解放します。これにより、書き込み中は他の読み取りや書き込みがブロックされ、データの一貫性が保たれます。
  4. システムコールとキャッシュの連携:
    • readenv(key string) (string, error): これは、元のGetenvのファイルシステム読み取りロジックを抽出したヘルパー関数です。キャッシュミス時やcopyenvの初期化時にのみ使用されます。
    • writeenv(key, value string) error: これは、元のSetenvのファイルシステム書き込みロジックを抽出したヘルパー関数です。Setenvが呼び出された際に、実際のPlan 9環境変数ファイルに書き込むために使用されます。
    • Setenvは、writeenvを呼び出して実際の環境変数を更新した後、キャッシュ(envマップ)も更新します。
    • Clearenvは、キャッシュ(envマップ)をクリアするとともに、RawSyscall(SYS_RFORK, RFCENVG, 0, 0)を呼び出して実際のPlan 9環境もクリアします。

この設計により、環境変数の読み取りはほとんどの場合、高速なメモリキャッシュから行われるようになり、システムコールによるオーバーヘッドが大幅に削減されます。書き込み操作は依然としてシステムコールを伴いますが、読み取り操作に比べて頻度が低いことが多いため、全体的なパフォーマンス向上に寄与します。

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

変更はsrc/pkg/syscall/env_plan9.goファイルに集中しています。

--- a/src/pkg/syscall/env_plan9.go
+++ b/src/pkg/syscall/env_plan9.go
@@ -6,69 +6,123 @@

  package syscall

 -import "errors"
 +import (
 +	"errors"
 +	"sync"
 +)

 -func Getenv(key string) (value string, found bool) {
 --	if len(key) == 0 {
 --		return "", false
 -	}
 --	f, e := Open("/env/"+key, O_RDONLY)
 --	if e != nil {
 --		return "", false
 -	}
 --	defer Close(f)
 -
 --	l, _ := Seek(f, 0, 2)
 --	Seek(f, 0, 0)
 --	buf := make([]byte, l)
 --	n, e := Read(f, buf)
 --	if e != nil {
 -		return "", false
 -	}
 -
 --	if n > 0 && buf[n-1] == 0 {
 --		buf = buf[:n-1]
 -	}
 --	return string(buf), true
 -}
 +var (
 +	// envOnce guards initialization by copyenv, which populates env.
 +	envOnce sync.Once
 +
 +	// envLock guards env.
 +	envLock sync.RWMutex
 +
 +	// env maps from an environment variable to its value.
 +	env map[string]string
 +)
 +
 +func readenv(key string) (string, error) {
 +	fd, err := Open("/env/"+key, O_RDONLY)
 +	if err != nil {
 +		return "", err
 +	}
 +	defer Close(fd)
 +	l, _ := Seek(fd, 0, 2)
 +	Seek(fd, 0, 0)
 +	buf := make([]byte, l)
 +	n, err := Read(fd, buf)
 +	if err != nil {
 +		return "", err
 +	}
 +	if n > 0 && buf[n-1] == 0 {
 +		buf = buf[:n-1]
 +	}
 +	return string(buf), nil
 +}

  func Setenv(key, value string) error {
 +	fd, err := Create("/env/"+key, O_RDWR, 0666)
 +	if err != nil {
 +		return err
 +	}
 +	defer Close(fd)
 +	_, err = Write(fd, []byte(value))
 +	return err
 +}
 +
 +func copyenv() {
 +	env = make(map[string]string)
 +	fd, err := Open("/env", O_RDONLY)
 +	if err != nil {
 +		return
 +	}
 +	defer Close(fd)
 +	files, err := readdirnames(fd)
 +	if err != nil {
 +		return
 +	}
 +	for _, key := range files {
 +		v, err := readenv(key)
 +		if err != nil {
 +			continue
 +		}
 +		env[key] = v
 +	}
 +}
 +
 +func Getenv(key string) (value string, found bool) {
 +	envOnce.Do(copyenv)
 +	if len(key) == 0 {
 +		return "", false
 +	}
 +
 +	envLock.RLock()
 +	defer envLock.RUnlock()
 +
 +	v, ok := env[key]
 +	if !ok {
 +		return "", false
 +	}
 +	return v, true
 +}
 +
 +func Setenv(key, value string) error {
 +	envOnce.Do(copyenv)
  	if len(key) == 0 {
 -		return errors.New("bad arg in system call")
 +		return errors.New("zero length key")
  	}

 -	f, e := Create("/env/"+key, O_RDWR, 0666)
 -	if e != nil {
 -		return e
 -	}
 -	defer Close(f)
 -
 --	_, e = Write(f, []byte(value))
 +	envLock.Lock()
 +	defer envLock.Unlock()
 +
 +	err := writeenv(key, value)
 +	if err != nil {
 +		return err
 +	}
 +	env[key] = value
  	return nil
  }

  func Clearenv() {
 +	envOnce.Do(copyenv) // prevent copyenv in Getenv/Setenv
 +
 +	envLock.Lock()
 +	defer envLock.Unlock()
 +
 +	env = make(map[string]string)
  	RawSyscall(SYS_RFORK, RFCENVG, 0, 0)
  }

  func Environ() []string {
 -	env := make([]string, 0, 100)
 -
 --	f, e := Open("/env", O_RDONLY)
 --	if e != nil {
 --		panic(e)
 --	}
 --	defer Close(f)
 --
 --	names, e := readdirnames(f)
 --	if e != nil {
 --		panic(e)
 --	}
 --
 --	for _, k := range names {
 --		if v, ok := Getenv(k); ok {
 --			env = append(env, k+"="+v)
 --		}
 -	}
 --	return env[0:len(env)]
 +	envOnce.Do(copyenv)
 +	envLock.RLock()
 +	defer envLock.RUnlock()
 +	a := make([]string, len(env))
 +	i := 0
 +	for k, v := range env {
 +		a[i] = k + "=" + v
 +		i++
 +	}
 +	return a
  }

コアとなるコードの解説

  • import ("errors", "sync"): syncパッケージが新しくインポートされ、sync.Oncesync.RWMutexが使用可能になります。
  • var (envOnce sync.Once, envLock sync.RWMutex, env map[string]string):
    • envOnce: copyenv関数の初回実行を保証するためのsync.Onceインスタンス。
    • envLock: envマップへの並行アクセスを制御するための読み書きロック。
    • env: 環境変数をキャッシュするためのstringからstringへのマップ。
  • readenv(key string) (string, error):
    • 元のGetenv関数から、単一の環境変数ファイルを読み取るロジックが抽出されました。これは、Plan 9の/env/keyファイルを開き、その内容を読み取って文字列として返す関数です。
  • writeenv(key, value string) error:
    • 元のSetenv関数から、単一の環境変数ファイルに書き込むロジックが抽出されました。これは、Plan 9の/env/keyファイルを作成または開き、指定された値を書き込む関数です。
  • copyenv():
    • この新しい関数は、/envディレクトリ内の全ての環境変数ファイルを読み込み、その内容をenvマップに格納します。これは、キャッシュの初期化処理です。
  • Getenv(key string) (value string, found bool):
    • envOnce.Do(copyenv): Getenvが最初に呼び出されたときにcopyenvが一度だけ実行され、キャッシュが初期化されます。
    • envLock.RLock() / defer envLock.RUnlock(): envマップからの読み取り操作中に読み取りロックを取得し、関数終了時に解放します。これにより、複数の読み取りが同時に行われても安全です。
    • v, ok := env[key]: キャッシュから直接環境変数の値を取得しようとします。キャッシュに存在すれば、ファイルシステムアクセスなしで値を返します。
  • Setenv(key, value string) error:
    • envOnce.Do(copyenv): Setenvが最初に呼び出されたときもキャッシュが初期化されます。
    • envLock.Lock() / defer envLock.Unlock(): envマップへの書き込み操作中に書き込みロックを取得し、関数終了時に解放します。これにより、書き込み中は他の読み取りや書き込みがブロックされ、データの一貫性が保証されます。
    • err := writeenv(key, value): 実際のPlan 9環境変数ファイルに値を書き込みます。
    • env[key] = value: ファイルへの書き込みが成功した後、キャッシュも更新します。
  • Clearenv():
    • envOnce.Do(copyenv): Clearenvが呼び出された後でも、Getenv/Setenvcopyenvを再度実行しないようにします。
    • envLock.Lock() / defer envLock.Unlock(): キャッシュをクリアする前に書き込みロックを取得します。
    • env = make(map[string]string): キャッシュを空のマップで上書きし、クリアします。
    • RawSyscall(SYS_RFORK, RFCENVG, 0, 0): 実際のPlan 9環境をクリアするためのシステムコールを呼び出します。
  • Environ() []string:
    • envOnce.Do(copyenv): キャッシュを初期化します。
    • envLock.RLock() / defer envLock.RUnlock(): キャッシュからの読み取り操作中に読み取りロックを取得します。
    • for k, v := range env: キャッシュされたenvマップをイテレートし、key=value形式の文字列スライスを構築して返します。以前のように環境変数ファイルを一つずつ読み込む必要がなくなりました。

これらの変更により、Plan 9環境におけるGoプログラムの環境変数アクセス性能が大幅に向上し、特に環境変数を頻繁に参照するアプリケーションにおいてその効果が顕著に現れます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Plan 9の環境変数に関する情報(/envファイルシステム)
  • Go言語の並行性プリミティブ(sync.Once, sync.RWMutex)に関する一般的な解説
  • システムコールとパフォーマンスに関する一般的な知識