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

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

このコミットは、GoランタイムのPlan 9オペレーティングシステム向け実装において、usleep関数にNOSPLITフラグを付与する変更です。これにより、usleep関数がスタックチェックを行わないようになり、lockextra関数からの呼び出し時に安全性が確保されます。

コミット

commit 8b85a3d480d63c2700ac556f183fc05280380fab
Author: Anthony Martin <ality@pbrane.org>
Date:   Sat May 18 15:47:49 2013 -0700

    runtime: mark usleep as NOSPLIT on Plan 9
    
    Usleep is called from lockextra, also marked NOSPLIT.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/9258043

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

https://github.com/golang/go/commit/8b85a3d480d63c2700ac556f183fc05280380fab

元コミット内容

runtime: mark usleep as NOSPLIT on Plan 9 Usleep is called from lockextra, also marked NOSPLIT.

このコミットは、GoランタイムのPlan 9固有のコードにおいて、usleep関数をNOSPLITとしてマークするものです。その理由は、usleepが既にNOSPLITとしてマークされているlockextra関数から呼び出されるためです。

変更の背景

Goランタイムは、低レベルの同期プリミティブやスケジューリングなど、OSと密接に連携する部分でC言語やアセンブリ言語で書かれたコードを含んでいます。これらの低レベル関数の中には、スタックの拡張(スタックチェック)が許されない、あるいは望ましくないものがあります。スタックチェックは、関数呼び出し時に現在のスタックが十分なサイズを持っているかを確認し、必要であればスタックを拡張する処理です。しかし、非常にクリティカルなセクションや、ロックを保持している間など、スタックの拡張によって予期せぬ割り込みやデッドロックが発生する可能性がある場合には、このスタックチェックを無効にする必要があります。

このコミットの背景には、usleep関数がGoランタイムの内部関数であるlockextraから呼び出されるという事実があります。lockextraは、Goランタイムのロック機構に関連する非常に低レベルな関数であり、既にNOSPLITとしてマークされています。これは、lockextraの実行中にスタックチェックによる割り込みが発生すると、ランタイムの整合性が損なわれる可能性があるためです。

lockextraNOSPLITであるにもかかわらず、そこから呼び出されるusleepNOSPLITでない場合、usleepの内部でスタックチェックが発生し、予期せぬ動作やデッドロックを引き起こす可能性があります。このコミットは、この潜在的な問題を解消し、ランタイムの安定性と安全性を向上させることを目的としています。

前提知識の解説

Plan 9オペレーティングシステム

Plan 9 from Bell Labsは、Unixの後継としてベル研究所で開発された分散オペレーティングシステムです。1980年代後半に開発が始まり、Unixの「全てはファイルである」という哲学をさらに推し進めました。

主な特徴は以下の通りです。

  • 分散システム設計: ネットワーク上の異なるコンピュータが単一のシステムとして機能するように設計されており、ファイルストレージ、計算能力、アプリケーションなどのリソースを異なるサーバーが提供できます。
  • 「全てはファイルである」メタファー: ネットワークやユーザーインターフェースを含む全てのシステムインターフェースをファイルとして表現します。
  • 9Pプロトコル: ローカルおよびリモートのリソースにファイルとしてアクセスするためのシンプルなメッセージ指向ファイルシステムプロトコルです。
  • プロセスごとの名前空間: 各プロセスがシステムリソースの独自のビュー(名前空間)を持ちます。
  • UTF-8エンコーディング: 内部的にUTF-8をテキスト表現として完全にサポートし、使用した最初のOSの一つです。

Go言語は、Plan 9の開発者であるロブ・パイクやケン・トンプソンらが関わっており、GoランタイムにはPlan 9への実験的な移植が含まれています。

NOSPLITディレクティブ

NOSPLITは、Goコンパイラに対するディレクティブ(指示)の一つで、特定の関数にスタックオーバーフローチェックを挿入しないように指示します。通常、Goの関数は呼び出し時にスタックが十分なサイズを持っているかを確認し、不足していればスタックを自動的に拡張します。これはGoのゴルーチンが非常に軽量であり、スタックサイズが動的に変化するためです。

しかし、Goランタイムの非常に低レベルな部分、特にOSとのインタラクションやロック機構に関わるコードでは、スタックチェックによる割り込みやスタックの拡張が許されない場合があります。例えば、ロックを保持している間にスタックチェックが発生し、スタック拡張のためにスケジューラが介入すると、デッドロックやランタイムの破損につながる可能性があります。

NOSPLITが適用された関数は、スタックチェックのオーバーヘッドがないため、非常に高速に実行されますが、スタックが不足した場合にスタックオーバーフローを引き起こす可能性があります。そのため、このディレクティブは、スタック使用量が非常に少なく、かつスタックチェックが許されないクリティカルな関数にのみ慎重に適用されます。

Goのソースコードでは、//go:nosplitというコメント形式でGoのソースファイルに直接記述されることもありますが、C言語やアセンブリ言語で書かれたランタイムコードでは、#pragma textflag 7のようなディレクティブを通じてNOSPLITフラグが設定されることがあります。

lockextra関数

lockextraは、Goランタイムの内部にある低レベルな関数で、Goのロック機構(ミューテックスなど)に関連しています。これはGoの標準ライブラリのsyncパッケージで提供されるようなユーザー向けの関数ではなく、Goランタイムのコア部分でゴルーチンのスケジューリングや同期を管理するために使用されます。

lockextraの正確な内部動作はGoランタイムの実装詳細に依存しますが、一般的には、ミューテックスの取得や解放、あるいはその他の低レベルな同期プリミティブの操作中に呼び出される可能性があります。この関数は、Goランタイムの整合性を保つ上で非常に重要であり、その実行中に予期せぬ割り込みやスタックの拡張が発生することは望ましくありません。そのため、lockextra自体もNOSPLITとしてマークされていることが多いです。

#pragma textflag 7

#pragma textflag 7は、Goのツールチェイン(コンパイラ、アセンブラ、リンカ)に対して、続くコードブロックに特定のフラグを適用するアセンブリディレクティブです。7という数値はビットマスクであり、複数のフラグの組み合わせを表します。Goランタイムのsrc/runtime/textflag.hで定義されているフラグを組み合わせたものです。

7は以下のフラグの合計です。

  • 1 (NOPROF): プロファイリングの対象外とすることを示します(現在は非推奨)。
  • 2 (DUPOK): リンカが同じ名前のシンボルを複数検出することを許可します。リンカは重複の中から一つを選択します。
  • 4 (NOSPLIT): 関数にスタックチェックのプリアンブルを挿入しないことを示します。これは、スタックチェックのオーバーヘッドが望ましくない、非常に小さいパフォーマンスクリティカルな関数で通常使用されます。

したがって、#pragma textflag 7は、続くコードにNOPROFDUPOKNOSPLITの3つのフラグを適用することを意味します。このコミットでは、特にNOSPLITフラグがusleep関数に適用されることが重要です。

技術的詳細

このコミットの技術的詳細の中心は、usleep関数にNOSPLIT属性を付与することです。Goランタイムでは、usleepのような低レベルな時間遅延関数は、OSのスケジューリングや同期プリミティブと密接に関連しています。

lockextra関数は、Goランタイムの内部ロックメカニズムの一部であり、その性質上、非常に短い時間で完了し、かつ実行中にゴルーチンのプリエンプション(横取り)やスタックの拡張が発生しないことが求められます。そのため、lockextra自体はNOSPLITとしてマークされています。

もしlockextraNOSPLITであるにもかかわらず、そこから呼び出されるusleepNOSPLITでない場合、usleepの内部でスタックチェックが発生する可能性があります。スタックチェックは、現在のゴルーチンのスタックが不足している場合に、より大きなスタックを割り当ててコピーする処理をトリガーします。この処理は、Goスケジューラを呼び出し、現在のゴルーチンを一時停止させる可能性があります。

lockextraがロックを保持している間にこのようなスタック拡張とスケジューリングが発生すると、以下のような問題が起こりえます。

  1. デッドロック: lockextraが保持しているロックが解放されないまま、別のゴルーチンがそのロックを待機し、結果としてシステム全体が停止する可能性があります。
  2. ランタイムの破損: クリティカルなデータ構造がロックによって保護されているにもかかわらず、スタック拡張による割り込みでそのデータ構造が不整合な状態になる可能性があります。
  3. パフォーマンスの低下: 意図しないスタックチェックと拡張は、低レベルな同期操作のパフォーマンスに悪影響を与えます。

usleepNOSPLITを適用することで、usleepの実行中にはスタックチェックが行われなくなります。これにより、lockextraからusleepが呼び出されても、スタック拡張による予期せぬスケジューリングや割り込みが発生するリスクが排除され、ランタイムの安定性と安全性が向上します。

この変更は、GoランタイムがPlan 9のような特定のOS環境で、その低レベルな特性を考慮して適切に動作するための調整の一例です。OS固有のコードでは、このような細かなチューニングがシステムの安定性に大きく寄与します。

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

--- a/src/pkg/runtime/os_plan9.c
+++ b/src/pkg/runtime/os_plan9.c
@@ -122,6 +122,7 @@ runtime·osyield(void)\n 	runtime·sleep(0);\n }\n \n+#pragma textflag 7
 void
 runtime·usleep(uint32 µs)\n {\n```

## コアとなるコードの解説

変更は`src/pkg/runtime/os_plan9.c`ファイルに対して行われています。このファイルは、GoランタイムのPlan 9オペレーティングシステム固有の実装を含んでいます。

追加された行は以下の通りです。

```c
#pragma textflag 7

この一行がruntime·usleep関数の定義の直前に追加されています。

  • #pragma: コンパイラに対する特別な指示を与えるプリプロセッサディレクティブです。
  • textflag 7: これはGoのツールチェインに特有のディレクティブで、続く関数(この場合はruntime·usleep)に特定のフラグを適用します。7はビットマスクで、NOPROF (1), DUPOK (2), NOSPLIT (4) の合計です。

この変更により、runtime·usleep関数はNOSPLIT属性を持つことになります。つまり、この関数が呼び出される際には、Goランタイムはスタックのサイズチェックを行わず、スタックの拡張も試みません。

これは、コミットメッセージにあるように、usleepが既にNOSPLITとしてマークされているlockextra関数から呼び出されるためです。lockextraのようなクリティカルな関数がスタックチェックを伴う関数を呼び出すと、ランタイムの整合性が損なわれる可能性があるため、呼び出し元と呼び出し先の両方がNOSPLITであることが望ましいと判断された結果の変更です。

関連リンク

  • Go CL 9258043: https://golang.org/cl/9258043 (このCLはcmd/go: add -C flag to 'go run'という別のコミットにマージされているようです。元のコミットメッセージに記載されているCL番号が、このコミットの変更内容と直接一致しない可能性があります。これはGoのコードレビューシステムとGitの統合の特性によるものかもしれません。)

参考にした情報源リンク