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

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

このコミットは、GoランタイムにおけるPlan 9ビルドの不具合を修正するものです。具体的には、システムコール中に発生するスタック分割(stack split)に関連する致命的なエラーを解決するために、特定のランタイム関数に#pragma textflag 7ディレクティブを追加しています。

コミット

  • コミットハッシュ: 33bd9694cd1fcbfbba9f5ce51de5975e72696632
  • Author: David du Colombier 0intro@gmail.com
  • Date: Tue Aug 6 07:37:26 2013 -0700

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

https://github.com/golang/go/commit/33bd9694cd1fcbfbba9f5ce51de5975e72696632

元コミット内容

runtime: fix Plan 9 build

The current failures were:

fatal error: runtime: stack split during syscall
goroutine 1 [stack split]:
runtime.findnull(0x105a9080)
        /usr/go/src/pkg/runtime/string.goc:14 fp=0x305aefb8
runtime: unexpected return pc for runtime.errstr called from 0x80
runtime.errstr()
        /usr/go/src/pkg/runtime/sys_plan9_386.s:196 +0x2f fp=0x305aefc8

fatal error: runtime: stack split during syscall
goroutine 2 [stack split]:
runtime.nanotime(0x305bff3c)
        /usr/go/src/pkg/runtime/time_plan9_386.c:9 fp=0x305bff34
notetsleep(0x305bff9c, 0xf8475800, 0xd, 0x0, 0x0, ...)
        /usr/go/src/pkg/runtime/lock_sema.c:195 +0x87 fp=0x305bff48
runtime.notetsleepg(0x305bff9c, 0xf8475800, 0xd)
        /usr/go/src/pkg/runtime/lock_sema.c:266 +0xa4 fp=0x305bff68
runtime.MHeap_Scavenger()
        /usr/go/src/pkg/runtime/mheap.c:463 +0xc2 fp=0x305bffd0
runtime.goexit()
        /usr/go/src/pkg/runtime/proc.c:1332 fp=0x305bffd4
created by runtime.main
        /usr/go/src/pkg/runtime/proc.c:168

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

変更の背景

このコミットは、GoランタイムのPlan 9ビルドにおいて発生していた致命的なエラー「fatal error: runtime: stack split during syscall」を修正するために導入されました。このエラーは、runtime.findnull関数(string.goc内)とruntime.nanotime関数(time_plan9_386.c内)がシステムコール中にスタック分割を試みた際に発生していました。

Goのランタイムは、ゴルーチン(goroutine)のスタックを動的に管理します。通常、関数呼び出し時にスタックが不足すると、ランタイムはスタックを拡張(スタック分割)します。しかし、システムコールのような低レベルの操作中にスタック分割が発生すると、予期せぬ動作やデッドロックを引き起こす可能性があります。特に、runtime.findnullは文字列操作に関連し、runtime.nanotimeは高精度な時間取得に関連する関数であり、これらがシステムコールと密接に関わる状況でスタック分割が起こると、ランタイムの整合性が損なわれる問題がありました。

この問題は、Plan 9という特定のオペレーティングシステム環境下でのGoのビルドに特有のものでした。Plan 9はGo言語の設計に大きな影響を与えたOSであり、GoランタイムはPlan 9のシステムコールインターフェースと密接に連携しています。そのため、Plan 9環境での安定した動作を保証するために、この修正が必要とされました。

前提知識の解説

#pragma textflag 7 (NOSPLIT)

#pragma textflag 7は、Goのコンパイラおよびリンカに対するアセンブリディレクティブであり、NOSPLITフラグに対応します。このフラグは、マークされた関数に対してスタックチェックのプリアンブル(前処理)を挿入しないように指示します。

通常、Goの各関数は、実行に必要なスタック領域が十分にあるかを確認するための小さなプロローグを含んでいます。もしスタック領域が不足している場合、ランタイムにトラップして現在のスタック割り当てを拡張します。しかし、メモリ割り当てやクリティカルパスに関わる特定の低レベルランタイム関数は、スタックの拡張自体がメモリ割り当てを必要とする可能性があり、再帰的な動作やデッドロックにつながるため、安全にスタック分割を実行できません。

したがって、NOSPLIT(またはアセンブリにおける#pragma textflag 7)でマークされた関数は、スタックの拡張をトリガーしないことが保証されます。これは、これらの関数が非常に少ない固定量のスタック領域を使用するか、十分なスタックが既に保証されているコンテキストで呼び出される必要があることを意味します。このプラグマは、主にGoの標準ライブラリとランタイムの内部で、ガベージコレクタのようなコア機能を実装するために使用されます。

Goのスタック管理 (Stack Split / Segmented Stacks vs. Contiguous Stacks)

Goのランタイムは、ゴルーチンのスタックを効率的に管理するために進化してきました。

  • セグメントスタック (Segmented Stacks): Goの初期のバージョン(Go 1.3まで)では、セグメントスタックと呼ばれるアプローチが採用されていました。このモデルでは、各ゴルーチンは小さく固定サイズのスタック(例:8KB)で開始します。関数呼び出しが現在のセグメントで利用可能なスタック領域よりも多くのスペースを必要とする場合、ランタイムは新しいより大きなスタックセグメントを割り当て、それを前のセグメントにリンクします。関数が戻ると、新しいセグメントは解放される可能性がありました。 このアプローチは、ゴルーチンが最小限のメモリで開始できるという利点がありましたが、「ホットスプリット問題」として知られる重大なパフォーマンス問題に悩まされました。これは、関数が繰り返しスタック分割を引き起こし、その後すぐに戻る場合に発生し、特にタイトなループ内でスタックセグメントの頻繁で高価な割り当てと解放につながりました。

  • スタックコピー (Stack Copying / Contiguous Stacks): この問題を解決するため、Go 1.4ではセグメントスタックからスタックコピー(または連続スタック)へと移行しました。この新しいモデルでは、ゴルーチンのスタックが拡張する必要がある場合、ランタイムは新しいより大きな連続したメモリブロック(多くの場合、元のサイズの2倍)を割り当て、古いスタックの内容を新しいスタックにコピーします。これにより、セグメントのリンクとアンリンクのオーバーヘッドが排除され、スタックの縮小が「無料」の操作になります。スタックが再び拡張する場合でも、割り当てられたより大きな領域を単に再利用できるため、パフォーマンスが大幅に向上し、「ホットスプリット」問題が回避されました。

このコミットは、Go 1.4でのスタックコピーへの移行以前の、セグメントスタックモデルにおける問題に対処しています。

Plan 9とGo

Go言語は、Bell LabsのPlan 9オペレーティングシステムから大きな影響を受けています。Goの作成者の一部(Rob PikeやKen Thompsonなど)はPlan 9の開発者でもありました。この影響は、Goのアセンブリ構文や、歴史的にはシステムコール(syscall)の実装など、Goのいくつかの側面に現れています。

Goのアセンブラの構文は、Plan 9のアセンブラに基づいています。Goのアセンブリ言語は「Plan 9」とは呼ばれませんが、そのスタイルと慣習はPlan 9に由来しています。

システムコールに関して、Goのランタイムはオペレーティングシステムのカーネルと直接対話し、ファイルI/O、ネットワーク通信、プロセス管理などの操作を実行します。Plan 9オペレーティングシステムの場合、Goはこれらのシステムコールに対して特定の独自の実装を提供しています。例えば、golang.org/x/sys/plan9パッケージには、Plan 9に特有の低レベルのオペレーティングシステムプリミティブへのインターフェースが含まれています。Goのソースコード内のasm_plan9_arm.sのようなアセンブリファイルは、SWI $0(ソフトウェア割り込み)のような命令を使用して、特定のアーキテクチャのPlan 9でシステムコールがどのように呼び出されるかを示しています。

Goランタイムの設計、特にスケジューラとゴルーチン管理は、Plan 9の概念に影響を受けていますが、スケジューラの直接的なコードや設計の継承については議論があります。しかし、低レベルのシステムコールインターフェースとアセンブリ言語の構文は、Plan 9からの強い系統を明確に示しています。

技術的詳細

このコミットの技術的な核心は、Goランタイムがシステムコールを実行する際に、特定の関数がスタック分割を試みないようにすることです。前述の通り、システムコール中にスタック分割が発生すると、ランタイムの整合性が損なわれ、致命的なエラーにつながる可能性があります。

runtime.findnull関数は、C言語で書かれたGoランタイムの一部であり、文字列の終端を示すNULLバイトを検索する役割を担っています。この関数は、Goの文字列操作の基盤となる低レベルな処理に関わります。同様に、runtime.nanotime関数は、Plan 9環境でナノ秒単位の時間を取得するための低レベルなシステムコールをラップしています。

これらの関数がシステムコールと密接に連携する性質上、実行中にスタックを拡張しようとすると問題が発生します。スタック拡張のプロセス自体が、システムコールが期待するコンテキストを破壊したり、デッドロックを引き起こしたりする可能性があるためです。

#pragma textflag 7NOSPLIT)ディレクティブをこれらの関数に追加することで、コンパイラはこれらの関数にスタックチェックのプリアンブルを挿入しなくなります。これにより、これらの関数は実行中にスタック分割を試みなくなり、システムコール中のスタック関連の致命的なエラーが回避されます。これは、これらの関数が呼び出される際には、既に十分なスタック領域が確保されているか、あるいは非常に少ないスタック領域しか必要としないという前提に基づいています。

この修正は、Goランタイムの堅牢性を高め、特にPlan 9のような特定のOS環境での安定した動作を保証するために不可欠でした。

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

このコミットでは、以下の2つのファイルにそれぞれ1行ずつ変更が加えられています。

  1. src/pkg/runtime/string.goc

    --- a/src/pkg/runtime/string.goc
    +++ b/src/pkg/runtime/string.goc
    @@ -10,6 +10,7 @@ package runtime
    
     String	runtime·emptystring;
    
    +#pragma textflag 7
     intgo
     runtime·findnull(byte *s)
     {
    
  2. src/pkg/runtime/time_plan9_386.c

    --- a/src/pkg/runtime/time_plan9_386.c
    +++ b/src/pkg/runtime/time_plan9_386.c
    @@ -5,6 +5,7 @@
     #include "runtime.h"
     #include "os_GOOS.h"
    
    +#pragma textflag 7
     int64
     runtime·nanotime(void)
     {
    

コアとなるコードの解説

変更は非常にシンプルで、runtime.findnull関数とruntime.nanotime関数の定義の直前に#pragma textflag 7が追加されています。

  • src/pkg/runtime/string.gocにおける変更: runtime.findnull関数は、Goランタイム内部で文字列の終端を見つけるために使用される低レベルな関数です。この関数は、C言語で記述されており、Goの文字列が内部的にどのように表現されているかに関連します。この関数に#pragma textflag 7を追加することで、runtime.findnullが呼び出された際にスタックチェックのプリアンブルが生成されなくなり、システムコール中にスタック分割を試みることを防ぎます。

  • src/pkg/runtime/time_plan9_386.cにおける変更: runtime.nanotime関数は、Plan 9オペレーティングシステム上でナノ秒単位の現在時刻を取得するための関数です。この関数もC言語で記述されており、Plan 9のシステムコールを介してOSから直接時間を取得します。この関数に#pragma textflag 7を追加することで、runtime.nanotimeがシステムコールを実行する際にスタック分割が発生するのを防ぎ、安定した時間取得を保証します。

これらの変更により、GoランタイムはPlan 9環境下でシステムコールをより安全に実行できるようになり、以前発生していた「fatal error: runtime: stack split during syscall」という致命的なエラーが解消されました。

関連リンク

参考にした情報源リンク