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

[インデックス 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ランタイムの概念を理解しておく必要があります。

  1. Goのガベージコレクタ (GC):

    • GoのGCは、主に「マーク・アンド・スイープ (Mark and Sweep)」アルゴリズムをベースにしています。
    • マークフェーズ: プログラムが使用しているオブジェクト(到達可能なオブジェクト)を特定し、マークします。
    • スイープフェーズ: マークされなかったオブジェクトが占めていたメモリを解放し、再利用可能な状態にします。
    • GoのGCは、ほとんどの時間をアプリケーションの実行と並行して動作する「並行GC」です。これにより、GCによるアプリケーションの一時停止時間(ストップ・ザ・ワールド時間)を最小限に抑えています。
  2. MSpan:

    • Goのメモリ管理において、ヒープメモリは「MSpan」と呼ばれる連続したメモリブロックに分割されます。
    • 各MSpanは、特定のサイズのオブジェクトを格納するために使用されます(例:8バイトオブジェクト用のMSpan、16バイトオブジェクト用のMSpanなど)。
    • GCのスイープフェーズでは、これらのMSpanが個別に処理され、不要なオブジェクトが解放されます。
  3. プリエンプション (Preemption):

    • Goランタイムは、協調的(cooperative)と非協調的(preemptive)なスケジューリングの両方を使用します。
    • 協調的スケジューリングでは、ゴルーチンは関数呼び出しやチャネル操作などの特定のポイントで自発的に実行を中断します。
    • 非協調的プリエンプションは、Go 1.14以降で導入されたもので、長時間実行されるループなど、協調的ポイントがないゴルーチンでも、ランタイムが強制的に実行を中断させ、他のゴルーチンにCPUを譲ることを可能にします。
    • このコミットが行われた2014年時点では、Goのプリエンプションは主に協調的なものでしたが、GCのようなクリティカルなセクションでは、ランタイムがプリエンプションを明示的に無効化することが重要でした。
  4. m->locks:

    • Goランタイムの内部では、m は「M」(Machine)を表し、OSのスレッドに対応します。
    • m->locks は、現在のM(OSスレッド)が保持しているロックの数を追跡するカウンタです。
    • このカウンタがゼロでない場合、そのMはクリティカルセクションにあり、プリエンプションやGCのストップ・ザ・ワールドイベントがブロックされるべきであることを示します。
    • m->locks をインクリメントすることでクリティカルセクションに入り、デクリメントすることでクリティカルセクションを抜けます。これにより、GCやスケジューラがそのMを中断しないようにします。
  5. runtime·atomicloadruntime·cas:

    • これらはGoランタイム内部で使用されるアトミック操作です。
    • runtime·atomicload: 指定されたアドレスから値をアトミックに読み込みます。
    • runtime·cas (Compare And Swap): 指定されたアドレスの現在の値が期待値と一致する場合にのみ、新しい値に更新します。これはロックフリーなデータ構造を実装する際に使用され、複数のゴルーチンが同時に同じメモリ位置にアクセスしても安全性を保ちます。

技術的詳細

このコミットの核心は、MSpan_Sweep 関数が呼び出される際のコンテキストの安全性を確保することにあります。

元の問題は、MSpan_Sweep を呼び出す一部のコードパスで、プリエンプションが有効な状態(つまり、m->locks が0の状態)でスイープが実行される可能性があったことです。スイープ処理はメモリの解放と再利用という非常にデリケートな操作であり、その途中でゴルーチンがプリエンプトされると、GCの状態が不整合になり、クラッシュやメモリ破損を引き起こす可能性があります。

この修正では、以下の変更が導入されています。

  1. ConcurrentSweep の有効化: src/pkg/runtime/mgc0.cConcurrentSweep 定数が 0 から 1 に変更されています。これは、並行スイープ機能がこの時点で有効化されたことを示唆しています。並行スイープが有効になることで、スイープ処理がより頻繁に、かつ並行して実行されるようになるため、スイープ処理の堅牢性がより重要になります。

  2. 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 が実行されている間は、ランタイムがそのゴルーチンをプリエンプトしないことが保証され、スイープ処理の原子性と整合性が保たれます。
  3. 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 を呼び出してパニックを発生させます。これにより、開発段階でこのような誤用が早期に発見されるようになります。
  4. sweepgen のローカル変数化と追加チェック: MSpan_Sweep 関数内で、runtime·mheap.sweepgen の値をローカル変数 sweepgen にキャッシュするようになりました。これにより、関数実行中にグローバルな runtime·mheap.sweepgen が変更されても、スイープ処理の整合性が保たれます。 また、スイープ完了後の sweepgen の更新時にも、s->states->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 ファイルに対して行われています。

  1. ConcurrentSweep 定数の変更:

    - 	ConcurrentSweep = 0,
    + 	ConcurrentSweep = 1,
    

    ConcurrentSweep の値を 0 から 1 に変更しています。これは、Goランタイムが並行スイープ機能を有効にしたことを示します。これにより、GCのスイープフェーズがアプリケーションの実行と並行して行われるようになり、GCの一時停止時間が短縮されます。この変更は、スイープ処理の堅牢性を高める必要性を強調しています。

  2. 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 の実行がアトミックな操作として扱われ、その途中で他のゴルーチンに切り替わることによる不整合が防止されます。
  3. 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 == 0m->mallocing == 0g != m->g0 のいずれかの条件が満たされない場合(つまり、プリエンプションが有効な状態や、メモリ割り当て中、またはシステムゴルーチン以外でこの関数が呼び出された場合)、runtime·throw が呼び出され、ランタイムパニックが発生します。これは、MSpan_Sweep が常に安全なコンテキストで実行されることを強制するためのガードレールです。
    • sweepgen のローカルキャッシュ: グローバルな runtime·mheap.sweepgen の値が sweepgen というローカル変数にコピーされます。これにより、スイープ処理中にグローバルな sweepgen が変更されても、このスイープ操作のコンテキストが安定します。
  4. 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->states->sweepgen) が期待通りであることを再確認する追加のチェックが導入されました。これにより、スイープ処理中にMSpanの状態が不正に変更されていないことを保証し、潜在的なデータ競合や不整合を防ぎます。

これらの変更は、Goの並行GCにおけるスイープ処理の堅牢性と正確性を大幅に向上させるものです。特に、クリティカルなメモリ管理操作中にプリエンプションが発生しないようにするためのガードレールが強化されています。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (特に src/runtime/mgc.gosrc/runtime/mheap.go など、GCとメモリ管理に関連するファイル)
  • GoのIssueトラッカーやコードレビューシステム (このコミットのCL: https://golang.org/cl/62990043)
  • Goのランタイムに関する技術ブログや解説記事 (例: "Go scheduler: MSpan_Sweep" などのキーワードで検索)
  • GoのGCに関する論文や発表資料
  • Goのメモリ管理に関する書籍やオンラインリソース