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

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

このコミットは、GoランタイムにおけるCPUプロファイリング中に発生するクラッシュを修正するものです。具体的には、mp->mcacheという重要なランタイム内部データ構造が、runtime·helpgcsigprof(シグナルプロファイラ)によって同時に変更されることによって引き起こされる競合状態を解消します。この競合状態により、mcachenilに設定されたままになり、ガベージコレクション(GC)がクラッシュする可能性がありました。

コミット

Author: Dmitriy Vyukov dvyukov@google.com Date: Mon Feb 10 20:24:47 2014 +0400

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

https://github.com/golang/go/commit/373e1e94d8bf4985bb1e0452d95ed500106dc631

元コミット内容

runtime: fix crash during cpu profiling
mp->mcache can be concurrently modified by runtime·helpgc.
In such case sigprof can remember mcache=nil, then helpgc sets it to non-nil,
then sigprof restores it back to nil, GC crashes with nil mcache.

R=rsc
CC=golang-codereviews
https://golang.org/cl/58860044

変更の背景

このコミットの背景には、GoプログラムのCPUプロファイリング中にランタイムがクラッシュするという深刻なバグがありました。このクラッシュは、Goランタイムの内部で利用されるmcacheというデータ構造の取り扱いにおける競合状態が原因でした。

CPUプロファイリングは、プログラムがCPU時間をどこで消費しているかを特定するために非常に重要なツールです。Goでは、SIGPROFシグナル(Unix系システムにおけるプロファイリングシグナル)を利用して、定期的にプログラムの実行状態をサンプリングします。このサンプリングは、runtime·sigprof関数によって処理されます。

問題は、runtime·sigprofがプロファイリング情報を収集する際に、現在のM(OSスレッドを表すランタイムの構造体)に関連付けられたmcacheを一時的にnilに設定し、プロファイリング処理後に元の値に戻すという操作を行っていた点にありました。mcacheは、メモリ割り当てを高速化するためのスレッドローカルなキャッシュであり、ガベージコレクタ(GC)がメモリを管理する上で不可欠なものです。

同時に、Goランタイムにはruntime·helpgcという関数が存在します。これは、GCが実行されている際に、他のM(OSスレッド)がGCを手伝うために呼び出されることがあります。runtime·helpgcもまた、mcacheを操作する可能性があります。

この二つの操作(sigprofによるmcacheの一時的なnil化と、helpgcによるmcacheの利用/設定)が同時に発生すると、以下のような競合状態が発生し、mcachenilのままGCが実行されてクラッシュするという事態を招いていました。

  1. sigprofが実行され、mp->mcacheの現在の値を保存し、mp->mcachenilに設定する。
  2. sigprofがプロファイリング処理を行っている間に、runtime·helpgcが別のゴルーチンで実行され、mp->mcachenilでないことを期待して操作を行う、あるいはmp->mcacheを非nilの値に設定する。
  3. sigprofがプロファイリング処理を終え、保存しておいたmcacheの値(この場合はnil)をmp->mcacheに復元してしまう。
  4. 結果として、mp->mcachenilのままになり、その後にメモリ割り当てやGCがmcacheを利用しようとすると、nilポインタ参照が発生し、ランタイムがクラッシュする。

このバグは、CPUプロファイリングというデバッグ・最適化に不可欠な機能を使用する際に、ランタイムの安定性を著しく損なうものであったため、早急な修正が必要とされました。

前提知識の解説

このコミットを理解するためには、Goランタイムのいくつかの重要な概念と、並行処理における一般的な問題についての知識が必要です。

Goランタイム (Go Runtime)

Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。ランタイムは、ガベージコレクション、スケジューリング(ゴルーチンの管理)、メモリ管理、システムコールインターフェースなど、プログラムの実行に必要な多くの低レベルな機能を提供します。C言語で書かれた部分が多く、OSとのインタラクションやパフォーマンスクリティカルな処理を担当します。

M, P, G スケジューラ (M, P, G Scheduler)

Goの並行処理モデルは、M, P, Gという3つの主要な抽象概念に基づいています。

  • G (Goroutine): Goにおける軽量なスレッドのようなものです。数千、数万のゴルーチンを同時に実行できます。
  • M (Machine): OSスレッドを表します。Goランタイムは、OSスレッドをMとして抽象化し、その上でGを実行します。MはOSのスケジューラによって管理されます。
  • P (Processor): 論理プロセッサを表します。GはP上で実行されます。PはMにアタッチされ、MがGを実行するためのコンテキストを提供します。Pの数は通常、CPUのコア数に制限されます。

このモデルにより、Goランタイムは効率的にゴルーチンをOSスレッドにマッピングし、並行実行を実現します。

CPUプロファイリングと SIGPROF

CPUプロファイリングは、プログラムの実行中にCPUがどの関数で時間を費やしているかを分析する手法です。Goでは、pprofツールがこの機能を提供します。Unix系システムでは、SIGPROFシグナルがプロファイリングのために使用されます。

SIGPROFは、タイマーに基づいて定期的にプロセスに送信されるシグナルです。このシグナルを受信すると、Goランタイムはシグナルハンドラ(runtime·sigprof)を実行し、現在の実行スタックをサンプリングして、どの関数が実行中であったかを記録します。これにより、時間の経過とともにどの関数がCPUを多く使用しているかの統計情報を収集できます。

mcache (Memory Cache)

mcacheは、Goランタイムのメモリ管理システムの一部です。これは、各M(OSスレッド)に紐付けられたスレッドローカルなメモリキャッシュです。小さなオブジェクトの割り当てを高速化するために使用されます。

Goランタイムは、メモリをページ単位で管理し、これらのページをmspanと呼ばれる構造体で追跡します。mcacheは、特定のサイズのオブジェクトを割り当てるためのmspanのリストを保持しています。これにより、メモリ割り当てのたびにグローバルなヒープロックを取得する必要がなくなり、並行処理のパフォーマンスが向上します。

mcacheは、ガベージコレクション(GC)の際に重要な役割を果たします。GCは、不要になったメモリを回収するためにヒープをスキャンしますが、この際にmcacheの内容も考慮に入れる必要があります。

ガベージコレクション (Garbage Collection, GC)

Goは自動メモリ管理(ガベージコレクション)を採用しています。GCは、プログラムが動的に割り当てたメモリのうち、もはや到達不可能(使用されていない)なものを自動的に解放するプロセスです。GoのGCは、並行かつインクリメンタルに動作し、プログラムの実行を長時間停止させることなくメモリを回収しようとします。

競合状態 (Race Condition)

競合状態は、複数の並行プロセスやスレッドが共有リソース(この場合はmp->mcache)にアクセスし、そのアクセス順序によって結果が非決定的に変わる場合に発生するバグです。競合状態は、プログラムのクラッシュ、データの破損、不正な結果など、予測不能な動作を引き起こす可能性があります。

このコミットで修正された問題は、sigprofhelpgcという2つの異なるコンテキストがmp->mcacheという共有リソースに同時にアクセスし、その操作のタイミングによってmcacheが不正な状態になるという典型的な競合状態でした。

mp->mallocing

mp->mallocingは、M(OSスレッド)が現在メモリ割り当て中であるかどうかを示すフラグです。Goランタイムでは、メモリ割り当ては非常に頻繁に行われる操作であり、特定のランタイム処理(例えば、プロファイリングやGCヘルパー)が実行されている間は、メモリ割り当てを一時的に無効化したり、特別な処理を必要としたりする場合があります。

このフラグは、mcacheのようなメモリ管理に関連するデータ構造へのアクセスを同期したり、特定の操作中にメモリ割り当てをブロックしたりするために使用されます。

技術的詳細

このコミットの技術的詳細を深く掘り下げると、Goランタイムの低レベルな同期メカニズムと、シグナルハンドラ内での安全な操作の重要性が浮き彫りになります。

問題の核心: mcacheの競合

以前の実装では、runtime·sigprof関数(CPUプロファイリングのシグナルハンドラ)は、プロファイリング処理中にmp->mcacheを一時的にnilに設定していました。これは、プロファイリング中にメモリ割り当てが行われることを防ぐため、またはmcacheの状態がプロファイリングのサンプリングに影響を与えないようにするためと考えられます。

// 変更前: runtime·sigprof
mcache = mp->mcache; // 現在のmcacheを保存
mp->mcache = nil;    // mcacheをnilに設定
// ... プロファイリング処理 ...
mp->mcache = mcache; // 保存したmcacheを復元

このアプローチは、単一スレッドのコンテキストでは問題ありませんが、Goランタイムは複数のM(OSスレッド)が並行して動作する環境です。特に、ガベージコレクションのヘルパー関数であるruntime·helpgcは、GCが実行されている間に他のMによって呼び出され、メモリ割り当てを行う可能性があります。runtime·helpgcは、GCのマークフェーズ中に新しいオブジェクトを割り当てたり、既存のオブジェクトを移動したりする際にmcacheを利用します。

競合状態のシナリオは以下の通りです。

  1. M1runtime·sigprofが実行される。
  2. M1mp->mcachenilに設定する。
  3. M1がプロファイリング処理を実行している間に、M2runtime·helpgcが実行される。
  4. runtime·helpgcは、M1mp->mcachenilであることを検出し、新しいmcacheを割り当ててM1mp->mcacheに設定する(または、M1がGCを手伝うためにmcacheを必要とする)。
  5. M1runtime·sigprofがプロファイリング処理を終え、最初に保存しておいたmcacheの値(これはnilであった)をmp->mcacheに復元してしまう。
  6. 結果として、M1mp->mcacheは再びnilになり、その後にM1がメモリ割り当てを行おうとすると、nilポインタ参照が発生し、ランタイムがクラッシュする。特に、GCがmcacheにアクセスしようとした際にクラッシュが発生していました。

修正アプローチ: mp->mallocingの利用

このコミットでは、mcacheを直接nilに設定する代わりに、mp->mallocingというフラグを利用することで競合状態を回避しています。

mp->mallocingは、現在のMがメモリ割り当て操作中であることを示すカウンタです。このカウンタをインクリメントすることで、sigprofが実行されている間は、そのMがメモリ割り当て中であるとランタイムに通知します。

// 変更後: runtime·sigprof
// mcacheを直接nilに設定する代わりに、mallocingカウンタをインクリメント
mp->mallocing++;
// ... プロファイリング処理 ...
// 処理終了後、mallocingカウンタをデクリメント
mp->mallocing--;

この変更のポイントは以下の通りです。

  1. mcacheの直接操作の廃止: runtime·sigprofmp->mcacheを直接nilに設定したり、復元したりしなくなりました。これにより、helpgcとの間のmcacheに対する競合が根本的に解消されます。
  2. mp->mallocingの利用: mp->mallocingカウンタは、Mがメモリ割り当てを行っている最中であることをランタイムの他の部分に伝えます。Goランタイムのメモリ割り当てルーチンは、このmallocingフラグをチェックし、必要に応じて動作を調整します。例えば、mallocingが非ゼロの場合、メモリ割り当てはmcacheを介さずに、より安全な(ただし遅い)グローバルなメモリ割り当てパスを使用するようになります。
  3. シグナルハンドラの安全性: シグナルハンドラは、非同期に発生し、任意の時点で実行中のコードを中断する可能性があるため、非常に慎重に設計する必要があります。シグナルハンドラ内で複雑な操作や、ロックを伴う操作、メモリ割り当てを行うことは、デッドロックや競合状態を引き起こす可能性があるため、一般的に避けるべきです。この修正は、mcacheの直接操作という潜在的に危険な行為を排除し、より安全なmallocingフラグの操作に置き換えることで、シグナルハンドラの堅牢性を向上させています。

この修正により、sigprofが実行されている間でも、helpgcmcacheを安全に操作できるようになり、CPUプロファイリング中のランタイムクラッシュが解消されました。mp->mallocingのインクリメントとデクリメントは、アトミックな操作であり、競合状態を引き起こすことなく、Mのメモリ割り当て状態を正確に反映します。

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

変更は src/pkg/runtime/proc.c ファイルの runtime·sigprof 関数内で行われています。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -2115,7 +2115,6 @@ runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp, M *mp)
 {
  int32 n;
  bool traceback;
- MCache *mcache; // この行が削除された
  // Do not use global m in this function, use mp instead.
  // On windows one m is sending reports about all the g's, so m means a wrong thing.
  byte m;
@@ -2127,8 +2126,7 @@ runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp, M *mp)
  return;
 
  // Profiling runs concurrently with GC, so it must not allocate.
- mcache = mp->mcache; // この行が削除された
- mp->mcache = nil;    // この行が削除された
+ mp->mallocing++;     // この行が追加された
 
  // Define that a "user g" is a user-created goroutine, and a "system g"
  // is one that is m->g0 or m->gsignal. We've only made sure that we
@@ -2216,7 +2214,7 @@ runtime·sigprof(uint8 *pc, uint8 *pc, uint8 *lr, G *gp, M *mp)
  runtime·lock(&prof);
  if(prof.fn == nil) {
  runtime·unlock(&prof);
- mp->mcache = mcache; // この行が削除された
+ mp->mallocing--;     // この行が追加された
  return;
  }
  n = 0;
@@ -2229,7 +2227,7 @@ runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp, M *mp)
  }
  prof.fn(prof.pcbuf, n);
  runtime·unlock(&prof);
- mp->mcache = mcache; // この行が削除された
+ mp->mallocing--;     // この行が追加された
 }
 
 // Arrange to call fn with a traceback hz times a second.

コアとなるコードの解説

このコミットの核心的な変更は、runtime·sigprof関数内でmp->mcacheを直接操作するロジックを削除し、代わりにmp->mallocingカウンタをインクリメント/デクリメントするロジックに置き換えた点です。

  1. MCache *mcache; の削除:

    • 以前は、mp->mcacheの値を一時的に保存するためのローカル変数mcacheが宣言されていました。この変数は、プロファイリング処理後にmp->mcacheを元の状態に戻すために使用されていました。
    • この変数が不要になったのは、mp->mcacheを直接nilに設定し、後で復元するというパターンが廃止されたためです。
  2. mcache = mp->mcache;mp->mcache = nil; の削除:

    • これら2行は、プロファイリング処理の開始時にmp->mcachenilに設定し、その前の値を保存する役割を担っていました。
    • この操作が競合状態の原因でした。sigprofmcachenilに設定した直後に、helpgcのような別のランタイムルーチンが同じMmcacheを非nilの値に設定する可能性があり、sigprofが処理を終えた際にnilを復元してしまうことで問題が発生していました。
  3. mp->mallocing++; の追加:

    • この行は、runtime·sigprofが実行される際に、現在のMがメモリ割り当て操作中であるという状態をランタイムに通知します。
    • mp->mallocingは、Mがメモリ割り当てを行っている回数を追跡するカウンタです。このカウンタがゼロでない場合、ランタイムのメモリ割り当てルーチンは、mcacheを介した高速な割り当てパスではなく、より安全な(ただし、通常は遅い)グローバルな割り当てパスを使用するようにフォールバックします。
    • これにより、sigprofが実行されている間は、mcacheが一時的にnilに設定されることなく、helpgcなどの他のルーチンがmcacheを安全に利用できるようになります。
  4. mp->mcache = mcache; の削除と mp->mallocing--; の追加:

    • プロファイリング処理が終了した後、以前は保存しておいたmcacheの値をmp->mcacheに復元していました。この復元操作も競合状態の一因でした。
    • 新しいコードでは、mp->mallocingカウンタをデクリメントします。これにより、Mがメモリ割り当て操作を終了したことをランタイムに通知し、通常の高速なmcacheを利用した割り当てパスに戻れるようになります。

この変更により、runtime·sigprofmcacheの直接的な状態変更から切り離され、mp->mallocingというより抽象的で安全なメカニズムを通じて、メモリ割り当ての挙動を制御するようになりました。これは、シグナルハンドラのような非同期で実行されるコードにおいて、共有データ構造へのアクセスをより堅牢にするための典型的なパターンです。

関連リンク

  • GoのCPUプロファイリングに関する公式ドキュメント: https://go.dev/doc/diagnose-cpu-mem
  • GoランタイムのM, P, Gスケジューラに関する解説: https://go.dev/blog/go-concurrency-patterns-timing-out-and-cancellation (直接M,P,Gの解説ではないが、Goの並行処理モデルの理解に役立つ)
  • Goのメモリ管理とmcacheに関する議論(より深い理解のため): Goのソースコードや、Goのメモリ管理に関する技術ブログ記事などを参照すると良いでしょう。

参考にした情報源リンク

  • Goの公式ドキュメント
  • Goのソースコード (src/runtime/proc.go, src/runtime/mcache.go など)
  • Goランタイムの内部構造に関する技術ブログや論文 (例: "Go's work-stealing scheduler", "The Go scheduler")
  • 競合状態に関する一般的なプログラミングの概念に関する資料
  • SIGPROFシグナルとCPUプロファイリングの仕組みに関する資料
  • GoのIssueトラッカーやコードレビューシステム (Gerrit) の関連する議論 (例: https://golang.org/cl/58860044 は元のコードレビューへのリンク)
  • Goのpprofツールのドキュメント
  • Goのガベージコレクションに関する技術解説I have generated the detailed technical explanation in Markdown format, following all the instructions, including the specific chapter structure. The output is in Japanese and is as detailed as requested, covering the background, prerequisite knowledge, and technical specifics. I have used the commit data and leveraged web search for comprehensive information.

I will now output the generated Markdown content to standard output.