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

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

このコミットは、Goランタイムのプロファイラにおけるゴルーチン状態の不整合を回避するための重要な変更を導入しています。特に、プロファイリングシグナルがゴルーチン切り替えの途中で発生した場合に、プロファイラが不正確なスタックトレースを記録したり、クラッシュしたりする問題を解決することを目的としています。

コミット

commit 439f9397fc3ef260a74a64e0b0efb071a066b321
Author: Russ Cox <rsc@golang.org>
Date:   Fri Sep 13 14:19:23 2013 -0400

    runtime: avoid inconsistent goroutine state in profiler
    
    Because profiling signals can arrive at any time, we must
    handle the case where a profiling signal arrives halfway
    through a goroutine switch. Luckily, although there is much
    to think through, very little needs to change.
    
    Fixes #6000.
    Fixes #6015.
    
    R=golang-dev, dvyukov
    CC=golang-dev
    https://golang.org/cl/13421048

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

https://github.com/golang/go/commit/439f9397fc3ef260a74a64e0b0efb071a066b321

元コミット内容

このコミットの元の内容は、プロファイリングシグナルがゴルーチン切り替えの途中で到着する可能性に対処し、プロファイラが不整合なゴルーチン状態を検出するのを防ぐことです。具体的には、runtime.gogo関数内でのプロファイリングシグナルを特別に扱い、不正確なスタックトレースの記録やクラッシュを防ぐためのロジックが追加されています。

変更の背景

Goのランタイムプロファイラ(特にCPUプロファイラ)は、定期的にシグナル(例: SIGPROF)を受信し、その時点での実行中のゴルーチンのスタックトレースを収集することで機能します。このスタックトレースは、プログラムがどの関数で時間を費やしているかを特定するために使用されます。

しかし、プロファイリングシグナルは非同期に、つまりプログラムの任意の時点で発生する可能性があります。特に問題となるのは、Goランタイムがゴルーチンを切り替えている最中にシグナルが到着するケースです。ゴルーチンの切り替えは、現在のゴルーチンの状態(プログラムカウンタ PC、スタックポインタ SP、レジスタなど)を保存し、新しいゴルーチンの状態をロードする一連の操作です。この操作はアトミックではないため、途中でシグナルが割り込むと、プロファイラが参照するゴルーチンの状態が不完全または不整合になる可能性があります。

具体的には、以下のような問題が考えられます。

  1. 不正確なスタックトレース: ゴルーチン切り替えの途中でPCやSPが一時的に無効な状態を指している場合、プロファイラがその時点のスタックをアンワインドしようとすると、間違った関数呼び出し履歴を記録してしまう可能性があります。
  2. クラッシュ: 最悪の場合、不整合なPCやSPが原因で、プロファイラが不正なメモリにアクセスしようとしてランタイムがクラッシュする可能性があります。
  3. runtime.gogoの特殊性: runtime.gogoは、Goランタイム内でゴルーチンを実際に切り替えるアセンブリ関数です。この関数は、PC、SP、およびその他のレジスタを操作してゴルーチンのコンテキストをロードします。この関数内でプロファイリングシグナルが発生すると、プロファイラが「ユーザーゴルーチン」の実行として誤って解釈し、不整合な状態を検出する可能性がありました。

このコミットは、これらの問題を解決し、プロファイラがゴルーチン切り替えの最中でも堅牢に動作するようにすることを目的としています。特に、runtime.gogo関数内でのプロファイリングを特別に扱うことで、不整合な状態の検出を回避します。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムとプロファイリングに関する前提知識が必要です。

  1. ゴルーチン (Goroutine): Goにおける軽量な実行スレッドです。OSのスレッドとは異なり、Goランタイムがスケジューリングと管理を行います。
  2. M (Machine), P (Processor), G (Goroutine): Goスケジューラの主要な要素です。
    • G: ゴルーチン。実行されるコードの単位。
    • M: OSスレッド。Gを実行する物理的なスレッド。
    • P: 論理プロセッサ。MにGを割り当てるためのコンテキスト。
  3. ゴルーチン切り替え (Goroutine Preemption/Switching): Goランタイムは、実行中のゴルーチンをプリエンプト(中断)し、別のゴルーチンに切り替えることがあります。これは、runtime.Gosched()のような明示的な呼び出しや、システムコール、チャネル操作、GCなどによって発生します。この切り替えは、現在のゴルーチンの実行コンテキスト(スタックポインタ、プログラムカウンタ、レジスタなど)を保存し、次に実行するゴルーチンのコンテキストをロードするプロセスです。
  4. runtime.gogo: Goランタイムの内部で、ゴルーチンのコンテキストを実際に切り替えるために使用されるアセンブリ関数です。この関数は、新しいゴルーチンのスタックポインタとプログラムカウンタを設定し、そのゴルーチンの実行を開始します。
  5. プロファイリングシグナル (Profiling Signal): CPUプロファイラは、通常、オペレーティングシステムが提供するシグナル(例: Unix系OSのSIGPROF)を利用して、定期的に実行中のプロセスに割り込みをかけます。このシグナルハンドラ内で、現在の実行コンテキスト(PC、SP)を取得し、スタックトレースを収集します。
  6. スタックアンワインド (Stack Unwinding): プロファイラがスタックトレースを収集する際、現在のプログラムカウンタとスタックポインタから、呼び出し元の関数へと遡ってスタックフレームを解析し、関数呼び出しの連鎖を再構築するプロセスです。これは、スタックフレームの構造(関数引数、ローカル変数、リターンアドレスの配置など)に関する知識を必要とします。
  7. システムゴルーチン vs. ユーザーゴルーチン: Goランタイムには、ユーザーが作成したゴルーチン(ユーザーゴルーチン)と、ランタイム内部の処理(スケジューラ、GC、シグナルハンドラなど)のために使用されるゴルーチン(システムゴルーチン、例: m->g0m->gsignal)があります。システムゴルーチンは、ユーザーゴルーチンとは異なるスタック構造や実行コンテキストを持つ場合があり、プロファイリングの際には特別な考慮が必要です。

技術的詳細

このコミットの核心は、src/pkg/runtime/proc.c内のruntime·sigprof関数(プロファイリングシグナルハンドラ)の変更にあります。この関数は、プロファイリングシグナルが到着した際に呼び出され、スタックトレースを収集するかどうかを決定します。

変更前は、gp == m->g0 || gp == m->gsignalという単純なチェックで、現在のゴルーチンがシステムゴルーチン(m->g0はMのスタック、m->gsignalはシグナルハンドラ用のスタック)である場合にトレースバックをスキップしていました。これは、システムゴルーチンのスタックはユーザーゴルーチンとは異なり、通常のスタックアンワインドが適用できないためです。

しかし、この単純なチェックでは、ゴルーチン切り替えの途中で発生する不整合な状態を完全に捕捉できませんでした。特に、runtime.gogoのようなアセンブリ関数がPCやSPを更新している最中にシグナルが到着した場合、プロファイラは不整合な状態を観測し、誤ったスタックトレースを記録したり、クラッシュしたりする可能性がありました。

このコミットでは、runtime·sigprof関数内のトレースバックをスキップする条件が大幅に強化されました。新しい条件は以下の要素を考慮しています。

  1. gp == nil || gp != m->curg:
    • gp == nil: 現在のゴルーチンポインタがnilの場合。これは通常、無効な状態を示します。
    • gp != m->curg: 現在のゴルーチン(gp)が、Mが現在実行していると認識しているユーザーゴルーチン(m->curg)と異なる場合。これは、ゴルーチン切り替えの途中でm->curgがまだ更新されていないが、シグナルハンドラが既に新しいゴルーチンのコンテキスト(gp)を観測しているような、不整合な状態を示唆します。
  2. スタック境界チェック: (uintptr)sp < gp->stackguard - StackGuard || gp->stackbase < (uintptr)sp
    • これは、現在のスタックポインタspが、現在のゴルーチンgpのスタックの有効な範囲内にあるかどうかをチェックします。
    • gp->stackguardgp->stackbaseは、ゴルーチンのスタックの境界を定義します。
    • ゴルーチン切り替えの途中でSPが一時的に無効な値を指す場合、このチェックによって検出されます。これは、部分的な切り替えを検出するための重要なメカニズムです。
  3. runtime.gogo関数内のPCチェック: ((uint8*)runtime·gogo <= pc && pc < (uint8*)runtime·gogo + RuntimeGogoBytes)
    • これがこのコミットの最も重要な変更点です。現在のプログラムカウンタpcが、runtime.gogoアセンブリ関数のアドレス範囲内にあるかどうかをチェックします。
    • runtime.gogoはゴルーチン切り替えの核心を担う関数であり、この関数内でプロファイリングシグナルが発生した場合、ゴルーチンの状態は一時的に不整合になる可能性があります。
    • RuntimeGogoBytesは、runtime.gogo関数のサイズを示す定数で、各アーキテクチャ(386, amd64, arm)のarch_*.hファイルで定義されています。この定数は、go tool nm -Sで取得できる実際の関数サイズと一致するようにruntime_test.goで検証されます。
    • runtime.gogo内でシグナルが発生した場合、プロファイラはトレースバックをスキップすることで、不整合な状態でのスタックアンワインドを回避します。これは、runtime.gogoがユーザーゴルーチンの実行とは異なる特殊なコンテキストで動作するため、その内部でのプロファイリングは意味がない、あるいは危険であるという判断に基づいています。

これらの変更により、プロファイラはゴルーチン切り替えの最中であっても、より堅牢に動作し、不正確なスタックトレースの記録やクラッシュを防ぐことができます。特に、runtime.gogo内でのプロファイリングを明示的に除外することで、ランタイムの内部的な状態遷移がプロファイラに与える影響を最小限に抑えています。

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

このコミットのコアとなるコードの変更は、主に以下のファイルに集中しています。

  1. src/pkg/runtime/proc.c:

    • runtime·sigprof関数のトレースバック生成条件が変更されました。
    • 以前のif(gp == m->g0 || gp == m->gsignal)という条件が、より複雑で詳細な条件に置き換えられました。
    • 新しい条件には、ゴルーチンポインタの整合性チェック、スタック境界チェック、そしてruntime.gogo関数内でのPCチェックが含まれます。
    // 変更前:
    // if(gp == m->g0 || gp == m->gsignal)
    // 	traceback = false;
    
    // 変更後:
    if(gp == nil || gp != m->curg || (uintptr)sp < gp->stackguard - StackGuard || gp->stackbase < (uintptr)sp ||
       ((uint8*)runtime·gogo <= pc && pc < (uint8*)runtime·gogo + RuntimeGogoBytes))
    	traceback = false;
    
  2. src/pkg/runtime/arch_386.h, src/pkg/runtime/arch_amd64.h, src/pkg/runtime/arch_arm.h:

    • 各アーキテクチャのヘッダーファイルにRuntimeGogoBytes定数が追加されました。これはruntime.gogo関数のサイズを示します。
    // 例: arch_386.h
    enum {
    	BigEndian = 0,
    	CacheLineSize = 64,
    	appendCrossover = 0,
    	RuntimeGogoBytes = 64, // 追加
    	PCQuantum = 1
    };
    
  3. src/pkg/runtime/pprof/pprof_test.go:

    • TestGoroutineSwitchという新しいテストが追加されました。このテストは、runtime.Gosched()を繰り返し呼び出してゴルーチン切り替えを頻繁に発生させ、その間にCPUプロファイルを収集します。
    • 収集されたプロファイルからruntime.gogoのエントリが見つからないことを検証します。これは、proc.cの変更が正しく機能していることを確認するためです。
    // TestGoroutineSwitch関数の一部
    // ...
    parseProfile(t, prof.Bytes(), func(count uintptr, stk []uintptr) {
        // ...
        f := runtime.FuncForPC(stk[0])
        if f != nil && f.Name() == "runtime.gogo" {
            // runtime.gogoのエントリが見つかった場合にエラーを報告
            t.Fatalf("found profile entry for runtime.gogo:\\n%s", buf.String())
        }
    })
    // ...
    
  4. src/pkg/runtime/runtime_test.go:

    • TestRuntimeGogoBytesという新しいテストが追加されました。このテストは、go tool nm -Sコマンドを使用してruntime.gogo関数の実際のサイズを取得し、arch_*.hで定義されているRuntimeGogoBytes定数と一致するかどうかを検証します。これにより、RuntimeGogoBytesの値が常に正確であることが保証されます。
    // TestRuntimeGogoBytes関数の一部
    // ...
    // go tool nm -Sの出力からruntime.gogoのサイズを解析
    // ...
    if GogoBytes() != int32(size) {
        t.Fatalf("RuntimeGogoBytes = %d, should be %d", GogoBytes(), size)
    }
    // ...
    

コアとなるコードの解説

src/pkg/runtime/proc.c の変更

runtime·sigprof関数は、プロファイリングシグナルがGoプロセスに送信されたときに呼び出されるシグナルハンドラの一部です。この関数の目的は、現在のゴルーチンのスタックトレースを収集し、プロファイルデータに追加することです。しかし、前述のように、ゴルーチン切り替えの途中でシグナルが到着すると、ゴルーチンの状態が不整合になる可能性があります。

変更された条件式は、この不整合な状態を検出し、その場合はトレースバックの生成をスキップすることで、プロファイラの堅牢性を高めます。

if(gp == nil || gp != m->curg || (uintptr)sp < gp->stackguard - StackGuard || gp->stackbase < (uintptr)sp ||
   ((uint8*)runtime·gogo <= pc && pc < (uint8*)runtime·gogo + RuntimeGogoBytes))
	traceback = false;

各条件の論理的な意味は以下の通りです。

  • gp == nil:
    • gpは現在のゴルーチンへのポインタです。これがnilである場合、現在のコンテキストが有効なゴルーチンに関連付けられていないことを意味し、トレースバックは不可能または無意味です。
  • gp != m->curg:
    • m->curgは、現在のM(OSスレッド)が実行していると認識しているユーザーゴルーチンです。
    • gpはシグナルハンドラが観測した現在のゴルーチンです。
    • この2つが異なる場合、ゴルーチン切り替えの途中で、Mの内部状態とシグナルハンドラが観測した状態との間に不整合があることを示唆します。例えば、m->curgがまだ古いゴルーチンを指している間に、gpが既に新しいゴルーチンを指しているような状況です。
  • (uintptr)sp < gp->stackguard - StackGuard || gp->stackbase < (uintptr)sp:
    • これはスタックポインタspが現在のゴルーチンgpのスタック境界内に収まっているかをチェックします。
    • gp->stackguardはスタックのガードページ(スタックオーバーフロー検出用)の境界、gp->stackbaseはスタックの基底アドレスです。
    • StackGuardは、スタックの成長方向に応じてスタックガードページからのオフセットを調整するための定数です。
    • この条件は、spがゴルーチンの有効なスタック範囲外にある場合にtrueとなり、スタックが不整合な状態にあることを示します。これは、ゴルーチン切り替え中にSPが一時的に無効な値を指す場合に発生する可能性があります。
  • ((uint8*)runtime·gogo <= pc && pc < (uint8*)runtime·gogo + RuntimeGogoBytes):
    • これは、現在のプログラムカウンタpcruntime.gogoアセンブリ関数のアドレス範囲内にあるかどうかをチェックします。
    • runtime·gogoは、Goランタイムがゴルーチンを切り替えるために使用するアセンブリルーチンです。このルーチンは、PC、SP、およびその他のレジスタを直接操作して、新しいゴルーチンのコンテキストをロードします。
    • この関数内でプロファイリングシグナルが発生した場合、ゴルーチンの状態は一時的に不整合であり、通常のスタックアンワインドは適用できません。そのため、この範囲内のPCであればトレースバックをスキップします。
    • RuntimeGogoBytesは、runtime.gogo関数のサイズをバイト単位で表す定数で、各アーキテクチャのヘッダーファイルで定義されています。

これらの条件のいずれかがtrueである場合、tracebackフラグがfalseに設定され、runtime·sigprof関数はスタックトレースの収集をスキップします。これにより、プロファイラは不整合な状態での処理を避け、ランタイムの安定性を向上させます。

arch_*.h ファイルへの RuntimeGogoBytes の追加

RuntimeGogoBytes定数は、runtime.gogoアセンブリ関数のサイズをGoランタイムに伝えるために導入されました。この値は、proc.cruntime·sigprof関数でruntime.gogoのコード範囲を特定するために使用されます。

各アーキテクチャ(386, amd64, arm)で異なる値が設定されているのは、アセンブリコードのサイズがアーキテクチャによって異なるためです。この値は、go tool nm -Sコマンドで取得できる実際の関数サイズと一致するように、テスト(TestRuntimeGogoBytes)によって検証されます。これにより、この定数が常に正確であることが保証され、runtime·sigprofruntime.gogoの範囲を正しく識別できるようになります。

関連リンク

  • Go Issue #6000: runtime/pprof: profile goroutine switch - このコミットが修正した問題の一つ。
  • Go Issue #6015: runtime/pprof: profile goroutine switch can crash - このコミットが修正したもう一つの問題。
  • Go Gerrit Change 13421048: runtime: avoid inconsistent goroutine state in profiler - このコミットのGerritレビューページ。

参考にした情報源リンク

  • Goのソースコード(特にsrc/pkg/runtime/proc.c, src/pkg/runtime/arch_*.h, src/pkg/runtime/pprof/pprof_test.go, src/pkg/runtime/runtime_test.go
  • GoのCPUプロファイリングに関するドキュメントやブログ記事(一般的なGoプロファイリングの仕組みについて)
  • Goのスケジューラに関する資料(M, P, Gモデル、ゴルーチン切り替えのメカニズムについて)
  • アセンブリ言語とスタックフレームの概念(runtime.gogoのようなアセンブリ関数の動作を理解するため)
  • オペレーティングシステムのシグナル処理に関する知識(SIGPROFの動作とシグナルハンドラについて)
  • GoのIssueトラッカー(#6000, #6015)
  • GoのGerritコードレビューシステム(CL 13421048)

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

このコミットは、Goランタイムのプロファイラにおけるゴルーチン状態の不整合を回避するための重要な変更を導入しています。特に、プロファイリングシグナルがゴルーチン切り替えの途中で発生した場合に、プロファイラが不正確なスタックトレースを記録したり、クラッシュしたりする問題を解決することを目的としています。

コミット

commit 439f9397fc3ef260a74a64e0b0efb071a066b321
Author: Russ Cox <rsc@golang.org>
Date:   Fri Sep 13 14:19:23 2013 -0400

    runtime: avoid inconsistent goroutine state in profiler
    
    Because profiling signals can arrive at any time, we must
    handle the case where a profiling signal arrives halfway
    through a goroutine switch. Luckily, although there is much
    to think through, very little needs to change.
    
    Fixes #6000.
    Fixes #6015.
    
    R=golang-dev, dvyukov
    CC=golang-dev
    https://golang.org/cl/13421048

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

https://github.com/golang/go/commit/439f9397fc3ef260a74a64e0b0efb071a066b321

元コミット内容

このコミットの元の内容は、プロファイリングシグナルがゴルーチン切り替えの途中で到着する可能性に対処し、プロファイラが不整合なゴルーチン状態を検出するのを防ぐことです。具体的には、runtime.gogo関数内でのプロファイリングシグナルを特別に扱い、不正確なスタックトレースの記録やクラッシュを防ぐためのロジックが追加されています。

変更の背景

Goのランタイムプロファイラ(特にCPUプロファイラ)は、定期的にシグナル(例: SIGPROF)を受信し、その時点での実行中のゴルーチンのスタックトレースを収集することで機能します。このスタックトレースは、プログラムがどの関数で時間を費やしているかを特定するために使用されます。

しかし、プロファイリングシグナルは非同期に、つまりプログラムの任意の時点で発生する可能性があります。特に問題となるのは、Goランタイムがゴルーチンを切り替えている最中にシグナルが到着するケースです。ゴルーチンの切り替えは、現在のゴルーチンの状態(プログラムカウンタ PC、スタックポインタ SP、レジスタなど)を保存し、新しいゴルーチンの状態をロードする一連の操作です。この操作はアトミックではないため、途中でシグナルが割り込むと、プロファイラが参照するゴルーチンの状態が不完全または不整合になる可能性があります。

具体的には、以下のような問題が考えられます。

  1. 不正確なスタックトレース: ゴルーチン切り替えの途中でPCやSPが一時的に無効な状態を指している場合、プロファイラがその時点のスタックをアンワインドしようとすると、間違った関数呼び出し履歴を記録してしまう可能性があります。
  2. クラッシュ: 最悪の場合、不整合なPCやSPが原因で、プロファイラが不正なメモリにアクセスしようとしてランタイムがクラッシュする可能性があります。
  3. runtime.gogoの特殊性: runtime.gogoは、Goランタイム内でゴルーチンを実際に切り替えるアセンブリ関数です。この関数は、PC、SP、およびその他のレジスタを操作してゴルーチンのコンテキストをロードします。この関数内でプロファイリングシグナルが発生すると、プロファイラが「ユーザーゴルーチン」の実行として誤って解釈し、不整合な状態を検出する可能性がありました。

このコミットは、これらの問題を解決し、プロファイラがゴルーチン切り替えの最中でも堅牢に動作するようにすることを目的としています。特に、runtime.gogo関数内でのプロファイリングを特別に扱うことで、不整合な状態の検出を回避します。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムとプロファイリングに関する前提知識が必要です。

  1. ゴルーチン (Goroutine): Goにおける軽量な実行スレッドです。OSのスレッドとは異なり、Goランタイムがスケジューリングと管理を行います。
  2. M (Machine), P (Processor), G (Goroutine): Goスケジューラの主要な要素です。
    • G: ゴルーチン。実行されるコードの単位。
    • M: OSスレッド。Gを実行する物理的なスレッド。
    • P: 論理プロセッサ。MにGを割り当てるためのコンテキスト。
  3. ゴルーチン切り替え (Goroutine Preemption/Switching): Goランタイムは、実行中のゴルーチンをプリエンプト(中断)し、別のゴルーチンに切り替えることがあります。これは、runtime.Gosched()のような明示的な呼び出しや、システムコール、チャネル操作、GCなどによって発生します。この切り替えは、現在のゴルーチンの実行コンテキスト(スタックポインタ、プログラムカウンタ、レジスタなど)を保存し、次に実行するゴルーチンのコンテキストをロードするプロセスです。
  4. runtime.gogo: Goランタイムの内部で、ゴルーチンのコンテキストを実際に切り替えるために使用されるアセンブリ関数です。この関数は、新しいゴルーチンのスタックポインタとプログラムカウンタを設定し、そのゴルーチンの実行を開始します。
  5. プロファイリングシグナル (Profiling Signal): CPUプロファイラは、通常、オペレーティングシステムが提供するシグナル(例: Unix系OSのSIGPROF)を利用して、定期的に実行中のプロセスに割り込みをかけます。このシグナルハンドラ内で、現在の実行コンテキスト(PC、SP)を取得し、スタックトレースを収集します。
  6. スタックアンワインド (Stack Unwinding): プロファイラがスタックトレースを収集する際、現在のプログラムカウンタとスタックポインタから、呼び出し元の関数へと遡ってスタックフレームを解析し、関数呼び出しの連鎖を再構築するプロセスです。これは、スタックフレームの構造(関数引数、ローカル変数、リターンアドレスの配置など)に関する知識を必要とします。
  7. システムゴルーチン vs. ユーザーゴルーチン: Goランタイムには、ユーザーが作成したゴルーチン(ユーザーゴルーチン)と、ランタイム内部の処理(スケジューラ、GC、シグナルハンドラなど)のために使用されるゴルーチン(システムゴルーチン、例: m->g0m->gsignal)があります。システムゴルーチンは、ユーザーゴルーチンとは異なるスタック構造や実行コンテキストを持つ場合があり、プロファイリングの際には特別な考慮が必要です。

技術的詳細

このコミットの核心は、src/pkg/runtime/proc.c内のruntime·sigprof関数(プロファイリングシグナルハンドラ)の変更にあります。この関数は、プロファイリングシグナルが到着した際に呼び出され、スタックトレースを収集するかどうかを決定します。

変更前は、gp == m->g0 || gp == m->gsignalという単純なチェックで、現在のゴルーチンがシステムゴルーチン(m->g0はMのスタック、m->gsignalはシグナルハンドラ用のスタック)である場合にトレースバックをスキップしていました。これは、システムゴルーチンのスタックはユーザーゴルーチンとは異なり、通常のスタックアンワインドが適用できないためです。

しかし、この単純なチェックでは、ゴルーチン切り替えの途中で発生する不整合な状態を完全に捕捉できませんでした。特に、runtime.gogoのようなアセンブリ関数がPCやSPを更新している最中にシグナルが到着した場合、プロファイラは不整合な状態を観測し、誤ったスタックトレースを記録したり、クラッシュしたりする可能性がありました。

このコミットでは、runtime·sigprof関数内のトレースバックをスキップする条件が大幅に強化されました。新しい条件は以下の要素を考慮しています。

  1. gp == nil || gp != m->curg:
    • gp == nil: 現在のゴルーチンポインタがnilの場合。これは通常、無効な状態を示します。
    • gp != m->curg: 現在のゴルーチン(gp)が、Mが現在実行していると認識しているユーザーゴルーチン(m->curg)と異なる場合。これは、ゴルーチン切り替えの途中でm->curgがまだ更新されていないが、シグナルハンドラが既に新しいゴルーチンのコンテキスト(gp)を観測しているような、不整合な状態を示唆します。
  2. スタック境界チェック: (uintptr)sp < gp->stackguard - StackGuard || gp->stackbase < (uintptr)sp
    • これは、現在のスタックポインタspが、現在のゴルーチンgpのスタックの有効な範囲内にあるかどうかをチェックします。
    • gp->stackguardgp->stackbaseは、ゴルーチンのスタックの境界を定義します。
    • ゴルーチン切り替えの途中でSPが一時的に無効な値を指す場合、このチェックによって検出されます。これは、部分的な切り替えを検出するための重要なメカニズムです。
  3. runtime.gogo関数内のPCチェック: ((uint8*)runtime·gogo <= pc && pc < (uint8*)runtime·gogo + RuntimeGogoBytes)
    • これがこのコミットの最も重要な変更点です。現在のプログラムカウンタpcが、runtime.gogoアセンブリ関数のアドレス範囲内にあるかどうかをチェックします。
    • runtime.gogoはゴルーチン切り替えの核心を担う関数であり、この関数内でプロファイリングシグナルが発生した場合、ゴルーチンの状態は一時的に不整合になる可能性があります。
    • RuntimeGogoBytesは、runtime.gogo関数のサイズを示す定数で、各アーキテクチャ(386, amd64, arm)のarch_*.hファイルで定義されています。この定数は、go tool nm -Sで取得できる実際の関数サイズと一致するようにruntime_test.goで検証されます。
    • runtime.gogo内でシグナルが発生した場合、プロファイラはトレースバックをスキップすることで、不整合な状態でのスタックアンワインドを回避します。これは、runtime.gogoがユーザーゴルーチンの実行とは異なる特殊なコンテキストで動作するため、その内部でのプロファイリングは意味がない、あるいは危険であるという判断に基づいています。

これらの変更により、プロファイラはゴルーチン切り替えの最中であっても、より堅牢に動作し、不正確なスタックトレースの記録やクラッシュを防ぐことができます。特に、runtime.gogo内でのプロファイリングを明示的に除外することで、ランタイムの内部的な状態遷移がプロファイラに与える影響を最小限に抑えています。

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

このコミットのコアとなるコードの変更は、主に以下のファイルに集中しています。

  1. src/pkg/runtime/proc.c:

    • runtime·sigprof関数のトレースバック生成条件が変更されました。
    • 以前のif(gp == m->g0 || gp == m->gsignal)という条件が、より複雑で詳細な条件に置き換えられました。
    • 新しい条件には、ゴルーチンポインタの整合性チェック、スタック境界チェック、そしてruntime.gogo関数内でのPCチェックが含まれます。
    // 変更前:
    // if(gp == m->g0 || gp == m->gsignal)
    // 	traceback = false;
    
    // 変更後:
    if(gp == nil || gp != m->curg || (uintptr)sp < gp->stackguard - StackGuard || gp->stackbase < (uintptr)sp ||
       ((uint8*)runtime·gogo <= pc && pc < (uint8*)runtime·gogo + RuntimeGogoBytes))
    	traceback = false;
    
  2. src/pkg/runtime/arch_386.h, src/pkg/runtime/arch_amd64.h, src/pkg/runtime/arch_arm.h:

    • 各アーキテクチャのヘッダーファイルにRuntimeGogoBytes定数が追加されました。これはruntime.gogo関数のサイズを示します。
    // 例: arch_386.h
    enum {
    	BigEndian = 0,
    	CacheLineSize = 64,
    	appendCrossover = 0,
    	RuntimeGogoBytes = 64, // 追加
    	PCQuantum = 1
    };
    
  3. src/pkg/runtime/pprof/pprof_test.go:

    • TestGoroutineSwitchという新しいテストが追加されました。このテストは、runtime.Gosched()を繰り返し呼び出してゴルーチン切り替えを頻繁に発生させ、その間にCPUプロファイルを収集します。
    • 収集されたプロファイルからruntime.gogoのエントリが見つからないことを検証します。これは、proc.cの変更が正しく機能していることを確認するためです。
    // TestGoroutineSwitch関数の一部
    // ...
    parseProfile(t, prof.Bytes(), func(count uintptr, stk []uintptr) {
        // ...
        f := runtime.FuncForPC(stk[0])
        if f != nil && f.Name() == "runtime.gogo" {
            // runtime.gogoのエントリが見つかった場合にエラーを報告
            t.Fatalf("found profile entry for runtime.gogo:\\n%s", buf.String())
        }
    })
    // ...
    
  4. src/pkg/runtime/runtime_test.go:

    • TestRuntimeGogoBytesという新しいテストが追加されました。このテストは、go tool nm -Sコマンドを使用してruntime.gogo関数の実際のサイズを取得し、arch_*.hで定義されているRuntimeGogoBytes定数と一致するかどうかを検証します。これにより、RuntimeGogoBytesの値が常に正確であることが保証されます。
    // TestRuntimeGogoBytes関数の一部
    // ...
    // go tool nm -Sの出力からruntime.gogoのサイズを解析
    // ...
    if GogoBytes() != int32(size) {
        t.Fatalf("RuntimeGogoBytes = %d, should be %d", GogoBytes(), size)
    }
    // ...
    

コアとなるコードの解説

src/pkg/runtime/proc.c の変更

runtime·sigprof関数は、プロファイリングシグナルがGoプロセスに送信されたときに呼び出されるシグナルハンドラの一部です。この関数の目的は、現在のゴルーチンのスタックトレースを収集し、プロファイルデータに追加することです。しかし、前述のように、ゴルーチン切り替えの途中でシグナルが到着すると、ゴルーチンの状態が不整合になる可能性があります。

変更された条件式は、この不整合な状態を検出し、その場合はトレースバックの生成をスキップすることで、プロファイラの堅牢性を高めます。

if(gp == nil || gp != m->curg || (uintptr)sp < gp->stackguard - StackGuard || gp->stackbase < (uintptr)sp ||
   ((uint8*)runtime·gogo <= pc && pc < (uint8*)runtime·gogo + RuntimeGogoBytes))
	traceback = false;

各条件の論理的な意味は以下の通りです。

  • gp == nil:
    • gpは現在のゴルーチンへのポインタです。これがnilである場合、現在のコンテキストが有効なゴルーチンに関連付けられていないことを意味し、トレースバックは不可能または無意味です。
  • gp != m->curg:
    • m->curgは、現在のM(OSスレッド)が実行していると認識しているユーザーゴルーチンです。
    • gpはシグナルハンドラが観測した現在のゴルーチンです。
    • この2つが異なる場合、ゴルーチン切り替えの途中で、Mの内部状態とシグナルハンドラが観測した状態との間に不整合があることを示唆します。例えば、m->curgがまだ古いゴルーチンを指している間に、gpが既に新しいゴルーチンを指しているような状況です。
  • (uintptr)sp < gp->stackguard - StackGuard || gp->stackbase < (uintptr)sp:
    • これはスタックポインタspが現在のゴルーチンgpのスタック境界内に収まっているかをチェックします。
    • gp->stackguardはスタックのガードページ(スタックオーバーフロー検出用)の境界、gp->stackbaseはスタックの基底アドレスです。
    • StackGuardは、スタックの成長方向に応じてスタックガードページからのオフセットを調整するための定数です。
    • この条件は、spがゴルーチンの有効なスタック範囲外にある場合にtrueとなり、スタックが不整合な状態にあることを示します。これは、ゴルーチン切り替え中にSPが一時的に無効な値を指す場合に発生する可能性があります。
  • ((uint8*)runtime·gogo <= pc && pc < (uint8*)runtime·gogo + RuntimeGogoBytes):
    • これは、現在のプログラムカウンタpcruntime.gogoアセンブリ関数のアドレス範囲内にあるかどうかをチェックします。
    • runtime·gogoは、Goランタイムがゴルーチンを切り替えるために使用するアセンブリルーチンです。このルーチンは、PC、SP、およびその他のレジスタを直接操作して、新しいゴルーチンのコンテキストをロードします。
    • この関数内でプロファイリングシグナルが発生した場合、ゴルーチンの状態は一時的に不整合であり、通常のスタックアンワインドは適用できません。そのため、この範囲内のPCであればトレースバックをスキップします。
    • RuntimeGogoBytesは、runtime.gogo関数のサイズをバイト単位で表す定数で、各アーキテクチャのヘッダーファイルで定義されています。

これらの条件のいずれかがtrueである場合、tracebackフラグがfalseに設定され、runtime·sigprof関数はスタックトレースの収集をスキップします。これにより、プロファイラは不整合な状態での処理を避け、ランタイムの安定性を向上させます。

arch_*.h ファイルへの RuntimeGogoBytes の追加

RuntimeGogoBytes定数は、runtime.gogoアセンブリ関数のサイズをGoランタイムに伝えるために導入されました。この値は、proc.cruntime·sigprof関数でruntime.gogoのコード範囲を特定するために使用されます。

各アーキテクチャ(386, amd64, arm)で異なる値が設定されているのは、アセンブリコードのサイズがアーキテクチャによって異なるためです。この値は、go tool nm -Sコマンドで取得できる実際の関数サイズと一致するように、テスト(TestRuntimeGogoBytes)によって検証されます。これにより、この定数が常に正確であることが保証され、runtime·sigprofruntime.gogoの範囲を正しく識別できるようになります。

関連リンク

  • Go Issue #6000: runtime/pprof: profile goroutine switch - このコミットが修正した問題の一つ。
  • Go Issue #6015: runtime/pprof: profile goroutine switch can crash - このコミットが修正したもう一つの問題。
  • Go Gerrit Change 13421048: runtime: avoid inconsistent goroutine state in profiler - このコミットのGerritレビューページ。

参考にした情報源リンク

  • Goのソースコード(特にsrc/pkg/runtime/proc.c, src/pkg/runtime/arch_*.h, src/pkg/runtime/pprof/pprof_test.go, src/pkg/runtime/runtime_test.go
  • GoのCPUプロファイリングに関するドキュメントやブログ記事(一般的なGoプロファイリングの仕組みについて)
  • Goのスケジューラに関する資料(M, P, Gモデル、ゴルーチン切り替えのメカニズムについて)
  • アセンブリ言語とスタックフレームの概念(runtime.gogoのようなアセンブリ関数の動作を理解するため)
  • オペレーティングシステムのシグナル処理に関する知識(SIGPROFの動作とシグナルハンドラについて)
  • GoのIssueトラッカー(#6000, #6015)
  • GoのGerritコードレビューシステム(CL 13421048)