[インデックス 18494] ファイルの概要
このコミットは、Goランタイムにおける並行GC (Garbage Collection) のスイープ処理に関するバグ修正と堅牢性向上を目的としています。具体的には、MSpan_Sweep
関数がプリエンプション(横取り)が有効な状態で呼び出される可能性があった問題に対処し、追加のチェックを導入することで、GCスイープの整合性を確保しています。
コミット
commit f8e4a2ef94f852c9f112e118f4d266b97839c3c9
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Feb 13 19:36:45 2014 +0400
runtime: fix concurrent GC sweep
The issue was that one of the MSpan_Sweep callers
was doing sweep with preemption enabled.
Additional checks are added.
LGTM=rsc
R=rsc, dave
CC=golang-codereviews
https://golang.org/cl/62990043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f8e4a2ef94f852c9f112e118f4d266b97839c3c9
元コミット内容
runtime: fix concurrent GC sweep
The issue was that one of the MSpan_Sweep callers
was doing sweep with preemption enabled.
Additional checks are added.
LGTM=rsc
R=rsc, dave
CC=golang-codereviews
https://golang.org/cl/62990043
変更の背景
Goのガベージコレクタ (GC) は、プログラムの実行と並行して動作する「並行GC」を採用しています。この並行性により、GCがアプリケーションの実行を長時間停止させることなくメモリを管理できます。GCのフェーズの一つに「スイープ (Sweep)」があり、これはマークフェーズで到達可能とされなかったオブジェクトが占めていたメモリを解放し、再利用可能にするプロセスです。
このコミットが行われた当時のGoランタイムでは、MSpan_Sweep
という関数がメモリ領域(MSpan)のスイープを担当していました。しかし、この関数を呼び出す一部のコードパスで、プリエンプション(preemption)が有効な状態でスイープが実行されるという問題がありました。
プリエンプションとは、Goランタイムが実行中のゴルーチンを一時停止させ、別のゴルーチンにCPUを切り替えるメカニズムです。これは、長時間実行されるゴルーチンが他のゴルーチンの実行を妨げないようにするために重要です。しかし、GCのスイープのようなクリティカルな操作中にプリエンプションが発生すると、スイープ処理の途中でゴルーチンが切り替わり、GCの状態が不安定になったり、データ競合が発生したりする可能性があります。
この不安定性は、メモリ管理の整合性を損ない、クラッシュや不正なメモリ使用につながる可能性がありました。そのため、MSpan_Sweep
が常に安全なコンテキスト(プリエンプションが無効化された状態、またはGCが実行されないことが保証された状態)で実行されるように修正する必要がありました。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念を理解しておく必要があります。
-
Goのガベージコレクタ (GC):
- GoのGCは、主に「マーク・アンド・スイープ (Mark and Sweep)」アルゴリズムをベースにしています。
- マークフェーズ: プログラムが使用しているオブジェクト(到達可能なオブジェクト)を特定し、マークします。
- スイープフェーズ: マークされなかったオブジェクトが占めていたメモリを解放し、再利用可能な状態にします。
- GoのGCは、ほとんどの時間をアプリケーションの実行と並行して動作する「並行GC」です。これにより、GCによるアプリケーションの一時停止時間(ストップ・ザ・ワールド時間)を最小限に抑えています。
-
MSpan:
- Goのメモリ管理において、ヒープメモリは「MSpan」と呼ばれる連続したメモリブロックに分割されます。
- 各MSpanは、特定のサイズのオブジェクトを格納するために使用されます(例:8バイトオブジェクト用のMSpan、16バイトオブジェクト用のMSpanなど)。
- GCのスイープフェーズでは、これらのMSpanが個別に処理され、不要なオブジェクトが解放されます。
-
プリエンプション (Preemption):
- Goランタイムは、協調的(cooperative)と非協調的(preemptive)なスケジューリングの両方を使用します。
- 協調的スケジューリングでは、ゴルーチンは関数呼び出しやチャネル操作などの特定のポイントで自発的に実行を中断します。
- 非協調的プリエンプションは、Go 1.14以降で導入されたもので、長時間実行されるループなど、協調的ポイントがないゴルーチンでも、ランタイムが強制的に実行を中断させ、他のゴルーチンにCPUを譲ることを可能にします。
- このコミットが行われた2014年時点では、Goのプリエンプションは主に協調的なものでしたが、GCのようなクリティカルなセクションでは、ランタイムがプリエンプションを明示的に無効化することが重要でした。
-
m->locks
:- Goランタイムの内部では、
m
は「M」(Machine)を表し、OSのスレッドに対応します。 m->locks
は、現在のM(OSスレッド)が保持しているロックの数を追跡するカウンタです。- このカウンタがゼロでない場合、そのMはクリティカルセクションにあり、プリエンプションやGCのストップ・ザ・ワールドイベントがブロックされるべきであることを示します。
m->locks
をインクリメントすることでクリティカルセクションに入り、デクリメントすることでクリティカルセクションを抜けます。これにより、GCやスケジューラがそのMを中断しないようにします。
- Goランタイムの内部では、
-
runtime·atomicload
とruntime·cas
:- これらはGoランタイム内部で使用されるアトミック操作です。
runtime·atomicload
: 指定されたアドレスから値をアトミックに読み込みます。runtime·cas
(Compare And Swap): 指定されたアドレスの現在の値が期待値と一致する場合にのみ、新しい値に更新します。これはロックフリーなデータ構造を実装する際に使用され、複数のゴルーチンが同時に同じメモリ位置にアクセスしても安全性を保ちます。
技術的詳細
このコミットの核心は、MSpan_Sweep
関数が呼び出される際のコンテキストの安全性を確保することにあります。
元の問題は、MSpan_Sweep
を呼び出す一部のコードパスで、プリエンプションが有効な状態(つまり、m->locks
が0の状態)でスイープが実行される可能性があったことです。スイープ処理はメモリの解放と再利用という非常にデリケートな操作であり、その途中でゴルーチンがプリエンプトされると、GCの状態が不整合になり、クラッシュやメモリ破損を引き起こす可能性があります。
この修正では、以下の変更が導入されています。
-
ConcurrentSweep
の有効化:src/pkg/runtime/mgc0.c
のConcurrentSweep
定数が0
から1
に変更されています。これは、並行スイープ機能がこの時点で有効化されたことを示唆しています。並行スイープが有効になることで、スイープ処理がより頻繁に、かつ並行して実行されるようになるため、スイープ処理の堅牢性がより重要になります。 -
MSpan_EnsureSwept
におけるm->locks
の使用:runtime·MSpan_EnsureSwept
関数は、特定のMSpanがスイープ済みであることを保証する役割を担います。この関数内で、MSpan_Sweep
が呼び出される可能性があるパスにm->locks++
とm->locks--
が追加されました。m->locks++
:MSpan_Sweep
を呼び出す前にロックカウンタをインクリメントします。これにより、MSpan_Sweep
の実行中は現在のM(OSスレッド)がクリティカルセクションに入り、プリエンプションが抑制されます。m->locks--
:MSpan_Sweep
の呼び出し後にロックカウンタをデクリメントし、クリティカルセクションを終了します。- この変更により、
MSpan_Sweep
が実行されている間は、ランタイムがそのゴルーチンをプリエンプトしないことが保証され、スイープ処理の原子性と整合性が保たれます。
-
MSpan_Sweep
内でのプリエンプションチェック:runtime·MSpan_Sweep
関数の冒頭に、プリエンプションが無効化されていることを確認するための厳格なチェックが追加されました。if(m->locks == 0 && m->mallocing == 0 && g != m->g0) runtime·throw("MSpan_Sweep: m is not locked");
m->locks == 0
: これは、現在のMがロックを保持していない(つまり、クリティカルセクションにいない)ことを意味します。m->mallocing == 0
: これは、現在のMがメモリ割り当て中ではないことを意味します。メモリ割り当て中もGCの特定の操作がブロックされるべきクリティカルな状態です。g != m->g0
: これは、現在のゴルーチンがシステムゴルーチン(m->g0
)ではないことを意味します。m->g0
はランタイムの内部処理を行う特別なゴルーチンであり、通常のゴルーチンとは異なるプリエンプションルールを持つ場合があります。- これらの条件がすべて真である場合(つまり、ロックが保持されておらず、メモリ割り当て中でもなく、システムゴルーチンでもないのに
MSpan_Sweep
が呼び出された場合)、それは不正な状態であるため、runtime·throw
を呼び出してパニックを発生させます。これにより、開発段階でこのような誤用が早期に発見されるようになります。
-
sweepgen
のローカル変数化と追加チェック:MSpan_Sweep
関数内で、runtime·mheap.sweepgen
の値をローカル変数sweepgen
にキャッシュするようになりました。これにより、関数実行中にグローバルなruntime·mheap.sweepgen
が変更されても、スイープ処理の整合性が保たれます。 また、スイープ完了後のsweepgen
の更新時にも、s->state
とs->sweepgen
の状態を再確認するチェックが追加されました。if(s->state != MSpanInUse || s->sweepgen != sweepgen-1) { runtime·printf("MSpan_Sweep: state=%d sweepgen=%d mheap.sweepgen=%d\n", s->state, s->sweepgen, sweepgen); runtime·throw("MSpan_Sweep: bad span state after sweep"); }
これは、スイープ処理中にMSpanの状態が予期せず変更されていないことを保証するためのものです。
これらの変更により、MSpan_Sweep
はより堅牢になり、並行GC環境下でのメモリ管理の安定性が向上しました。
コアとなるコードの変更箇所
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -66,7 +66,7 @@ enum {
CollectStats = 0,
ScanStackByFrames = 1,
IgnorePreciseGC = 0,
- ConcurrentSweep = 0,
+ ConcurrentSweep = 1,
// Four bits per word (see #defines below).
wordsPerBitmapWord = sizeof(void*)*8/4,
@@ -1694,10 +1694,13 @@ runtime·MSpan_EnsureSwept(MSpan *s)
sg = runtime·mheap.sweepgen;
if(runtime·atomicload(&s->sweepgen) == sg)
return;
+ m->locks++;
if(runtime·cas(&s->sweepgen, sg-2, sg-1)) {
runtime·MSpan_Sweep(s);
+ m->locks--;
return;
}
+ m->locks--;
// unfortunate condition, and we don't have efficient means to wait
while(runtime·atomicload(&s->sweepgen) != sg)
runtime·osyield();
@@ -1709,13 +1712,13 @@ runtime·MSpan_EnsureSwept(MSpan *s)
bool
runtime·MSpan_Sweep(MSpan *s)
{
- int32 cl, n, npages;
+ int32 cl, n, npages, nfree;
uintptr size, off, *bitp, shift, bits;
+ uint32 sweepgen;
byte *p;
MCache *c;
byte *arena_start;
MLink head, *end;
- int32 nfree;
byte *type_data;
byte compression;
uintptr type_data_inc;
@@ -1723,9 +1726,14 @@ runtime·MSpan_Sweep(MSpan *s)
Special *special, **specialp, *y;\n bool res, sweepgenset;\n \n- if(s->state != MSpanInUse || s->sweepgen != runtime·mheap.sweepgen-1) {\n+ // It's critical that we enter this function with preemption disabled,\n+ // GC must not start while we are in the middle of this function.\n+ if(m->locks == 0 && m->mallocing == 0 && g != m->g0)\n+ runtime·throw("MSpan_Sweep: m is not locked");\n+ sweepgen = runtime·mheap.sweepgen;\n+ if(s->state != MSpanInUse || s->sweepgen != sweepgen-1) {\n runtime·printf("MSpan_Sweep: state=%d sweepgen=%d mheap.sweepgen=%d\\n",\n- s->state, s->sweepgen, runtime·mheap.sweepgen);\n+ s->state, s->sweepgen, sweepgen);\n runtime·throw("MSpan_Sweep: bad span state");\n }\n arena_start = runtime·mheap.arena_start;\
@@ -1820,7 +1828,7 @@ runtime·MSpan_Sweep(MSpan *s)\
\truntime·unmarkspan(p, 1<<PageShift);\n \t*(uintptr*)p = (uintptr)0xdeaddeaddeaddeadll;\t// needs zeroing\n \t// important to set sweepgen before returning it to heap\n- \truntime·atomicstore(&s->sweepgen, runtime·mheap.sweepgen);\n+ \truntime·atomicstore(&s->sweepgen, sweepgen);\n \tsweepgenset = true;\n \tif(runtime·debug.efence)\n \t\truntime·SysFree(p, size, &mstats.gc_sys);\
@@ -1851,8 +1859,16 @@ runtime·MSpan_Sweep(MSpan *s)\
}\n }\n \n- if(!sweepgenset)\n- runtime·atomicstore(&s->sweepgen, runtime·mheap.sweepgen);\n+ if(!sweepgenset) {\n+ // The span must be in our exclusive ownership until we update sweepgen,\n+ // check for potential races.\n+ if(s->state != MSpanInUse || s->sweepgen != sweepgen-1) {\n+ runtime·printf("MSpan_Sweep: state=%d sweepgen=%d mheap.sweepgen=%d\\n",\n+ s->state, s->sweepgen, sweepgen);\n+ runtime·throw("MSpan_Sweep: bad span state after sweep");\n+ }\n+ runtime·atomicstore(&s->sweepgen, sweepgen);\n+ }\n if(nfree) {\n c->local_nsmallfree[cl] += nfree;\n c->local_cachealloc -= nfree * size;\
コアとなるコードの解説
このコミットは src/pkg/runtime/mgc0.c
ファイルに対して行われています。
-
ConcurrentSweep
定数の変更:- ConcurrentSweep = 0, + ConcurrentSweep = 1,
ConcurrentSweep
の値を0
から1
に変更しています。これは、Goランタイムが並行スイープ機能を有効にしたことを示します。これにより、GCのスイープフェーズがアプリケーションの実行と並行して行われるようになり、GCの一時停止時間が短縮されます。この変更は、スイープ処理の堅牢性を高める必要性を強調しています。 -
runtime·MSpan_EnsureSwept
関数内のm->locks
操作:@@ -1694,10 +1694,13 @@ runtime·MSpan_EnsureSwept(MSpan *s) sg = runtime·mheap.sweepgen; if(runtime·atomicload(&s->sweepgen) == sg) return; + m->locks++; if(runtime·cas(&s->sweepgen, sg-2, sg-1)) { runtime·MSpan_Sweep(s); + m->locks--; return; } + m->locks--; // unfortunate condition, and we don't have efficient means to wait while(runtime·atomicload(&s->sweepgen) != sg) runtime·osyield();
runtime·MSpan_EnsureSwept
関数は、特定のMSpanが確実にスイープされていることを保証します。この関数内でruntime·MSpan_Sweep(s)
が呼び出される直前と直後にm->locks++
とm->locks--
が追加されました。m->locks++
: 現在のM(OSスレッド)がクリティカルセクションに入ったことをランタイムに通知します。これにより、MSpan_Sweep
の実行中にプリエンプションが発生するのを防ぎます。m->locks--
: クリティカルセクションを終了し、プリエンプションが再び可能になることをランタイムに通知します。 この変更により、MSpan_Sweep
の実行がアトミックな操作として扱われ、その途中で他のゴルーチンに切り替わることによる不整合が防止されます。
-
runtime·MSpan_Sweep
関数内のプリエンプションチェックとsweepgen
のローカル変数化:@@ -1709,13 +1712,13 @@ runtime·MSpan_EnsureSwept(MSpan *s) bool runtime·MSpan_Sweep(MSpan *s) { - int32 cl, n, npages; + int32 cl, n, npages, nfree; uintptr size, off, *bitp, shift, bits; + uint32 sweepgen; byte *p; MCache *c; byte *arena_start; MLink head, *end; - int32 nfree; byte *type_data; byte compression; uintptr type_data_inc; @@ -1723,9 +1726,14 @@ runtime·MSpan_Sweep(MSpan *s) Special *special, **specialp, *y;\n bool res, sweepgenset;\n \n - if(s->state != MSpanInUse || s->sweepgen != runtime·mheap.sweepgen-1) {\n+ // It's critical that we enter this function with preemption disabled,\n+ // GC must not start while we are in the middle of this function.\n+ if(m->locks == 0 && m->mallocing == 0 && g != m->g0)\n+ runtime·throw("MSpan_Sweep: m is not locked");\n+ sweepgen = runtime·mheap.sweepgen;\n+ if(s->state != MSpanInUse || s->sweepgen != sweepgen-1) {\n runtime·printf("MSpan_Sweep: state=%d sweepgen=%d mheap.sweepgen=%d\\n",\n - s->state, s->sweepgen, runtime·mheap.sweepgen);\n+ s->state, s->sweepgen, sweepgen);\n runtime·throw("MSpan_Sweep: bad span state");\n }\n arena_start = runtime·mheap.arena_start;\
nfree
変数の宣言位置が変更され、sweepgen
という新しいローカル変数が追加されました。- プリエンプションチェックの追加:
MSpan_Sweep
の冒頭に、現在のMがロックされている(クリティカルセクションにいる)ことを確認するif
文が追加されました。m->locks == 0
、m->mallocing == 0
、g != m->g0
のいずれかの条件が満たされない場合(つまり、プリエンプションが有効な状態や、メモリ割り当て中、またはシステムゴルーチン以外でこの関数が呼び出された場合)、runtime·throw
が呼び出され、ランタイムパニックが発生します。これは、MSpan_Sweep
が常に安全なコンテキストで実行されることを強制するためのガードレールです。 sweepgen
のローカルキャッシュ: グローバルなruntime·mheap.sweepgen
の値がsweepgen
というローカル変数にコピーされます。これにより、スイープ処理中にグローバルなsweepgen
が変更されても、このスイープ操作のコンテキストが安定します。
-
sweepgen
の更新と追加チェック:@@ -1820,7 +1828,7 @@ runtime·MSpan_Sweep(MSpan *s)\ \truntime·unmarkspan(p, 1<<PageShift);\n \t*(uintptr*)p = (uintptr)0xdeaddeaddeaddeadll;\t// needs zeroing\n \t// important to set sweepgen before returning it to heap\n - \truntime·atomicstore(&s->sweepgen, runtime·mheap.sweepgen);\n+ \truntime·atomicstore(&s->sweepgen, sweepgen);\n \tsweepgenset = true;\n \tif(runtime·debug.efence)\n \t\truntime·SysFree(p, size, &mstats.gc_sys);\ @@ -1851,8 +1859,16 @@ runtime·MSpan_Sweep(MSpan *s)\ }\n }\n \n - if(!sweepgenset)\n - runtime·atomicstore(&s->sweepgen, runtime·mheap.sweepgen);\n+ if(!sweepgenset) {\n+ // The span must be in our exclusive ownership until we update sweepgen,\n+ // check for potential races.\n+ if(s->state != MSpanInUse || s->sweepgen != sweepgen-1) {\n+ runtime·printf("MSpan_Sweep: state=%d sweepgen=%d mheap.sweepgen=%d\\n",\n+ s->state, s->sweepgen, sweepgen);\n+ runtime·throw("MSpan_Sweep: bad span state after sweep");\n+ }\n+ runtime·atomicstore(&s->sweepgen, sweepgen);\n+ }\n if(nfree) {\n c->local_nsmallfree[cl] += nfree;\n c->local_cachealloc -= nfree * size;\
s->sweepgen
を更新する際に、グローバルなruntime·mheap.sweepgen
ではなく、ローカル変数sweepgen
の値を使用するように変更されました。- スイープが完了し、
sweepgen
が更新される直前に、MSpanの状態 (s->state
とs->sweepgen
) が期待通りであることを再確認する追加のチェックが導入されました。これにより、スイープ処理中にMSpanの状態が不正に変更されていないことを保証し、潜在的なデータ競合や不整合を防ぎます。
これらの変更は、Goの並行GCにおけるスイープ処理の堅牢性と正確性を大幅に向上させるものです。特に、クリティカルなメモリ管理操作中にプリエンプションが発生しないようにするためのガードレールが強化されています。
関連リンク
- Goのガベージコレクションに関する公式ドキュメントやブログ記事:
- Go's Garbage Collector: A Look Back and a Look Forward (Go 1.5 GCに関する記事ですが、GCの基本的な概念と進化を理解するのに役立ちます)
- The Go Memory Model (Goのメモリモデルに関する公式ドキュメント)
参考にした情報源リンク
- Go言語のソースコード (特に
src/runtime/mgc.go
やsrc/runtime/mheap.go
など、GCとメモリ管理に関連するファイル) - GoのIssueトラッカーやコードレビューシステム (このコミットのCL:
https://golang.org/cl/62990043
) - Goのランタイムに関する技術ブログや解説記事 (例: "Go scheduler: MSpan_Sweep" などのキーワードで検索)
- GoのGCに関する論文や発表資料
- Goのメモリ管理に関する書籍やオンラインリソース