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

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

このコミットは、Goランタイムにおけるメモリ管理の重要なバグ修正に関するものです。具体的には、MSpan(メモリ領域の管理単位)が分割された後に誤ったリストに配置されたり、ガベージコレクションのスイープ処理が完了していないMSpanが操作されることによって発生するメモリ破損の問題を解決します。これにより、Goプログラムの安定性とメモリ管理の正確性が向上します。

コミット

  • コミットハッシュ: 8d321625fdae77d7e4a8c1681fe90bd893b9cdd2
  • 作者: Dmitriy Vyukov dvyukov@google.com
  • コミット日時: 2014年3月14日 金曜日 23:25:48 +0400

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

https://github.com/golang/go/commit/8d321625fdae77d7e4a8c1681fe90bd893b9cdd2

元コミット内容

runtime: fix spans corruption
The problem was that spans end up in wrong lists after split
(e.g. in h->busy instead of h->central->empty).
Also the span can be non-swept before split,
I don't know what it can cause, but it's safer to operate on swept spans.
Fixes #7544.

R=golang-codereviews, rsc
CC=golang-codereviews, khr
https://golang.org/cl/76160043

変更の背景

このコミットは、Goランタイムのメモリ管理における深刻なバグ、具体的にはIssue #7544で報告された「spans corruption(スパンの破損)」を修正するために導入されました。

問題の核心は以下の2点にありました。

  1. MSpan分割後の誤ったリストへの配置: Goのメモリヒープは、MSpanと呼ばれる連続したページ群を管理します。これらのMSpanは、そのサイズや状態に応じて異なるリスト(例: h->busyh->central->empty)に分類されます。MSpanが分割される(例えば、大きなMSpanから小さなMSpanを切り出す)際に、分割後のMSpanが本来属すべきリストとは異なるリストに誤って挿入されることがありました。これにより、メモリの割り当てや解放のロジックが混乱し、メモリ破損やクラッシュにつながる可能性がありました。
  2. 未スイープのMSpanの操作: Goのガベージコレクション(GC)には「スイープ」というフェーズがあります。これは、GCマークフェーズで到達不能と判断されたオブジェクトが占めていたメモリを解放し、MSpanを再利用可能な状態にするプロセスです。このコミット以前は、MSpanが分割される際に、そのMSpanがまだスイープされていない(つまり、古いオブジェクトのデータが残っている可能性がある)状態である可能性がありました。未スイープのMSpanを操作することは、予期せぬデータ破損やGCの不整合を引き起こすリスクがありました。コミットメッセージにある「I don't know what it can cause, but it's safer to operate on swept spans.」という記述は、この潜在的なリスクに対する懸念を示しています。

これらの問題は、Goプログラムの安定性とメモリの健全性を著しく損なう可能性があったため、早急な修正が必要とされました。

前提知識の解説

このコミットを理解するためには、Goランタイムのメモリ管理、特にヒープとMSpanの概念、およびガベージコレクションの基本的な仕組みについて理解しておく必要があります。

Goランタイムのメモリ管理の基本

Goランタイムは、独自のメモリマネージャを持っており、OSからメモリを確保し、それをGoプログラム内のオブジェクトに割り当て、ガベージコレクションによって不要になったメモリを回収します。

MHeap (Memory Heap)

MHeapは、Goランタイムが管理するグローバルなメモリヒープを表す構造体です。OSから取得した大量のメモリを管理し、必要に応じてMSpanに分割して提供します。MHeapは、様々なサイズのMSpanを効率的に管理するために、複数のリスト(例: freebusybusylargecentral)を持っています。

MSpan (Memory Span)

MSpanは、Goランタイムのメモリ管理における基本的な単位です。これは、連続した1つ以上のメモリページ(通常は8KB)の集合を表します。MSpanは、以下のいずれかの状態を持ちます。

  • MSpanFree: どのオブジェクトにも割り当てられていない、完全に空いている状態。
  • MSpanInUse: オブジェクトが割り当てられている状態。
  • MSpanStack: ゴルーチンのスタックとして使用されている状態。

MSpanには、そのstartアドレス、npages(ページ数)、sizeclass(割り当てられるオブジェクトのサイズカテゴリ)、elemsize(割り当てられるオブジェクトのサイズ)、sweepgen(スイープ世代)などの情報が含まれます。

Size Class

Goのメモリマネージャは、様々なサイズのオブジェクトを効率的に管理するために「サイズクラス」という概念を使用します。これは、特定のサイズのオブジェクトを割り当てるためのMSpanのタイプを定義します。例えば、小さなオブジェクト(数バイトから数KB)は、特定のサイズクラスに属するMSpanから割り当てられます。

MCentral

MCentralは、特定のサイズクラスに属するMSpanを管理する構造体です。各MCentralは、そのサイズクラスのMSpanを保持するemptyリストとpartialリストを持っています。

  • emptyリスト: オブジェクトが一つも割り当てられていない、完全に空いているMSpanのリスト。
  • partialリスト: 一部が割り当てられているが、まだ空きがあるMSpanのリスト。

Sweepgen (Sweep Generation)

sweepgenは、ガベージコレクションのスイープフェーズに関連する概念です。GCは複数のフェーズ(マーク、スイープなど)で構成されます。sweepgenは、MSpanがどのGCサイクルでスイープされたかを示す世代番号のようなものです。MSpanがスイープされると、そのsweepgenが更新されます。これにより、ランタイムはMSpanが最新のGCサイクルで処理されたかどうかを判断できます。

MSpan_EnsureSwept

runtime·MSpan_EnsureSwept(MSpan *s)関数は、指定されたMSpanが確実にスイープされていることを保証するための関数です。もしMSpanがまだスイープされていない場合、この関数はスイープ処理を実行します。これは、MSpanを再利用したり、その内容を安全に操作したりする前に非常に重要です。

MHeap_SplitSpan

runtime·MHeap_SplitSpan(MHeap *h, MSpan *s)関数は、既存のMSpanを2つの小さなMSpanに分割するために使用されます。例えば、大きな連続したメモリ領域が必要なくなった場合や、より小さなオブジェクトを割り当てるためにMSpanを細分化する必要がある場合などに呼び出されます。この関数は、元のMSpanを半分に分割し、新しいMSpanを作成して、それぞれを適切な状態に設定します。

ロック (Locking)

Goランタイムは並行に動作するため、複数のゴルーチンが同時にメモリヒープを操作する可能性があります。このため、MHeapMCentralのような共有データ構造を保護するためにロック(ミューテックス)が使用されます。これにより、データ競合を防ぎ、メモリ管理の整合性を保ちます。

技術的詳細

このコミットの技術的詳細は、主にruntime·MHeap_SplitSpan関数の変更と、runtime·MSpan_EnsureSweptの導入に集約されます。

runtime·MHeap_SplitSpanの変更点

以前のMHeap_SplitSpanの実装では、MSpanを分割する際に、そのMSpanが現在どのリストに属しているかを適切に考慮していませんでした。単にMHeapのロックを取得し、MSpanを分割した後、MHeapのロックを解放していました。このアプローチでは、分割されたMSpanが元のリストに残ったままになったり、誤ったリストに再挿入されたりする可能性がありました。

新しい実装では、以下の重要な変更が加えられました。

  1. 分割前のリストからの削除:

    • MSpanを分割する前に、まずそのMSpanが現在属しているリストから明示的に削除されます。
    • s->sizeclass > 0の場合(つまり、特定のサイズクラスに属するMSpanの場合)、それはh->central[s->sizeclass].emptyリストに存在すると想定されます。この場合、対応するMCentralのロックを取得し、runtime·MSpanList_Remove(s)を呼び出してリストから削除します。その後、MCentralのロックを解放し、MHeapのロックを取得します。
    • s->sizeclass == 0の場合(つまり、大きなMSpanで特定のサイズクラスに属さない場合)、それはh->busyまたはh->busylargeリストに存在すると想定されます。この場合、直接MHeapのロックを取得し、runtime·MSpanList_Remove(s)を呼び出してリストから削除します。
    • この「まず削除する」というステップにより、分割後のMSpanが古いリストに残り、二重管理されるような状態を防ぎます。
  2. ロックの粒度の調整:

    • 以前はMHeap全体をロックしていましたが、新しい実装では、MCentralのリスト操作時にはMCentralのロックを、MHeapのリスト操作時にはMHeapのロックを、というように、より適切な粒度でロックを取得・解放しています。これにより、並行性を高めつつ、データの一貫性を保っています。
  3. 分割後のリストへの再挿入:

    • MSpanが分割され、新しいMSpanが作成された後、元のMSpanと新しく作成されたMSpanは、その新しい状態(サイズクラスなど)に基づいて適切なリストに再挿入されます。
    • s->sizeclass > 0の場合、MSpanh->central[s->sizeclass].emptyリストの末尾に挿入されます。これは、スイープ済みのMSpanがリストの末尾に配置されるという慣習に従っています。
    • s->sizeclass == 0の場合、MSpanh->busyまたはh->busylargeリストの末尾に挿入されます。
    • この再挿入のロジックは、MSpanが常に正しいリストに存在することを保証します。

runtime·MSpan_EnsureSweptの導入

src/pkg/runtime/stack.cruntime·shrinkstack関数(ゴルーチンのスタックを縮小する際に呼び出される)において、runtime·MHeap_SplitSpanを呼び出す前にruntime·MSpan_EnsureSwept(span)が追加されました。

  • 以前のshrinkstackでは、スタックのMSpanを分割する際に、そのMSpanがスイープ済みであるかどうかの確認がありませんでした。
  • runtime·MSpan_EnsureSweptを呼び出すことで、MSpanが分割される前に必ずスイープ処理が完了していることが保証されます。これにより、未スイープのMSpanを操作することによる潜在的な問題(例えば、古いスタックフレームのデータが残っていることによるセキュリティリスクや、GCの不整合)が回避されます。コミットメッセージにある「it's safer to operate on swept spans」という意図がここで実現されています。

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

runtime·MSpan_EnsureSwept関数内のロックチェックの条件が変更されました。 if(m->locks == 0 && m->mallocing == 0) から if(m->locks == 0 && m->mallocing == 0 && g != m->g0) へと変更されています。 これは、MSpan_EnsureSweptが呼び出される際に、現在のゴルーチンがスケジューラゴルーチン(m->g0)ではない場合に、ロックが適切に保持されていることを確認するためのものです。m->g0は特別なゴルーチンであり、特定の状況下ではロックを持たずにメモリ操作を行うことがあるため、この条件が追加されたと考えられます。これにより、MSpan_EnsureSweptの呼び出し元が適切なコンテキストで実行されていることをより厳密にチェックし、ランタイムの堅牢性を高めています。

これらの変更により、MSpanのライフサイクル管理がより厳密になり、メモリ破損のリスクが大幅に低減されました。

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

このコミットでは、主に以下の3つのファイルが変更されています。

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

    • runtime·MSpan_EnsureSwept関数のロックチェック条件が変更されました。
      --- a/src/pkg/runtime/mgc0.c
      +++ b/src/pkg/runtime/mgc0.c
      @@ -1679,7 +1679,7 @@ runtime·MSpan_EnsureSwept(MSpan *s)
       	// Caller must disable preemption.
       	// Otherwise when this function returns the span can become unswept again
       	// (if GC is triggered on another goroutine).
      -	if(m->locks == 0 && m->mallocing == 0)
      +	if(m->locks == 0 && m->mallocing == 0 && g != m->g0)
       		runtime·throw("MSpan_EnsureSwept: m is not locked");
      
       	sg = runtime·mheap.sweepgen;
      
  2. src/pkg/runtime/mheap.c:

    • runtime·MHeap_SplitSpan関数が大幅に修正されました。
      • MSpanを分割する前に、現在属しているリストから削除するロジックが追加されました。
      • ロックの取得・解放のタイミングと粒度が変更されました。
      • 分割後のMSpanを適切なリストに再挿入するロジックが追加されました。
      • npages == 1の場合の特殊な処理が追加されました。
      --- a/src/pkg/runtime/mheap.c
      +++ b/src/pkg/runtime/mheap.c
      @@ -846,48 +846,86 @@ void
       runtime·MHeap_SplitSpan(MHeap *h, MSpan *s)
       {
       	MSpan *t;
      +	MCentral *c;
       	uintptr i;
       	uintptr npages;
       	PageID p;
      
      -	if((s->npages & 1) != 0)
      -		runtime·throw("MHeap_SplitSpan on an odd size span");
       	if(s->state != MSpanInUse)
       		runtime·throw("MHeap_SplitSpan on a free span");
       	if(s->sizeclass != 0 && s->ref != 1)
       		runtime·throw("MHeap_SplitSpan doesn't have an allocated object");
       	npages = s->npages;
      
      -	runtime·lock(h);
      +	// remove the span from whatever list it is in now
      +	if(s->sizeclass > 0) {
      +		// must be in h->central[x].empty
      +		c = &h->central[s->sizeclass];
      +		runtime·lock(c);
      +		runtime·MSpanList_Remove(s);
      +		runtime·unlock(c);
      +		runtime·lock(h);
      +	} else {
      +		// must be in h->busy/busylarge
      +		runtime·lock(h);
      +		runtime·MSpanList_Remove(s);
      +	}
      +	// heap is locked now
      +
      +	if(npages == 1) {
      +		// convert span of 1 PageSize object to a span of 2 PageSize/2 objects.
      +		s->ref = 2;
      +		s->sizeclass = runtime·SizeToClass(PageSize/2);
      +		s->elemsize = PageSize/2;
      +	} else {
      +		// convert span of n>1 pages into two spans of n/2 pages each.
      +		if((s->npages & 1) != 0)
      +			runtime·throw("MHeap_SplitSpan on an odd size span");
      +
      +		// compute position in h->spans
      +		p = s->start;
      +		p -= (uintptr)h->arena_start >> PageShift;
      +
      +		// Allocate a new span for the first half.
      +		t = runtime·FixAlloc_Alloc(&h->spanalloc);
      +		runtime·MSpan_Init(t, s->start, npages/2);
      +		t->limit = (byte*)((t->start + npages/2) << PageShift);
      +		t->state = MSpanInUse;
      +		t->elemsize = npages << (PageShift - 1);
      +		t->sweepgen = s->sweepgen;
      +		if(t->elemsize <= MaxSmallSize) {
      +			t->sizeclass = runtime·SizeToClass(t->elemsize);
      +			t->ref = 1;
      +		}
      +
      +		// the old span holds the second half.
      +		s->start += npages/2;
      +		s->npages = npages/2;
      +		s->elemsize = npages << (PageShift - 1);
      +		if(s->elemsize <= MaxSmallSize) {
      +			s->sizeclass = runtime·SizeToClass(s->elemsize);
      +			s->ref = 1;
      +		}
      +
      +		// update span lookup table
      +		for(i = p; i < p + npages/2; i++)
      +			h->spans[i] = t;
      +	}
      +
      +	// place the span into a new list
      +	if(s->sizeclass > 0) {
      +		runtime·unlock(h);
      +		c = &h->central[s->sizeclass];
      +		runtime·lock(c);
      +		// swept spans are at the end of the list
      +		runtime·MSpanList_InsertBack(&c->empty, s);
      +		runtime·unlock(c);
      +	} else {
      +		// Swept spans are at the end of lists.
      +		if(s->npages < nelem(h->free))
      +			runtime·MSpanList_InsertBack(&h->busy[s->npages], s);
      +		else
      +			runtime·MSpanList_InsertBack(&h->busylarge, s);
      +		runtime·unlock(h);
      +	}
      
  3. src/pkg/runtime/stack.c:

    • runtime·shrinkstack関数内で、runtime·MHeap_SplitSpanを呼び出す前にruntime·MSpan_EnsureSwept(span)が追加されました。
      --- a/src/pkg/runtime/stack.c
      +++ b/src/pkg/runtime/stack.c
      @@ -838,15 +838,7 @@ runtime·shrinkstack(G *gp)
       	// First, we trick malloc into thinking
       	// we allocated the stack as two separate half-size allocs.  Then the
       	// free() call does the rest of the work for us.
      -	if(oldsize == PageSize) {
      -		// convert span of 1 PageSize object to a span of 2
      -		// PageSize/2 objects.
      -		span->ref = 2;
      -		span->sizeclass = runtime·SizeToClass(PageSize/2);
      -		span->elemsize = PageSize/2;
      -	} else {
      -		// convert span of n>1 pages into two spans of n/2 pages each.
      -		runtime·MHeap_SplitSpan(&runtime·mheap, span);
      -	}
      +	runtime·MSpan_EnsureSwept(span);
      +	runtime·MHeap_SplitSpan(&runtime·mheap, span);
       	runtime·free(oldstk);
       }
      

コアとなるコードの解説

src/pkg/runtime/mheap.cruntime·MHeap_SplitSpan

この関数は、Goランタイムのメモリ管理において、既存のMSpanを2つの小さなMSpanに分割する役割を担います。変更の核心は、MSpanが分割される前後のリスト管理とロックの取り扱いにあります。

  1. 分割前のリストからの削除:

    • if(s->sizeclass > 0): もしMSpan sが特定のサイズクラスに属している場合(つまり、h->centralリストのいずれかに属しているはず)、対応するMCentral構造体cを取得します。
    • runtime·lock(c);runtime·unlock(c);: MCentralのリスト操作は、MCentral固有のロックで保護されます。これにより、他のゴルーチンが同じMCentralのリストを同時に変更するのを防ぎます。
    • runtime·MSpanList_Remove(s);: MSpan sを現在属しているリストから削除します。これは、分割後のMSpanが古いリストに残り続けることを防ぐために非常に重要です。
    • else: s->sizeclass == 0の場合(大きなMSpanh->busyまたはh->busylargeに属しているはず)、直接runtime·lock(h);MHeap全体をロックし、runtime·MSpanList_Remove(s);でリストから削除します。
    • この段階で、MSpan sはどのリストにも属していない状態になり、MHeapのロックが保持されていることが保証されます。
  2. MSpanの分割ロジック:

    • if(npages == 1): もし元のMSpanが1ページのみで構成されている場合、これを2つの半ページサイズのオブジェクトを保持するMSpanに変換します。これは、s->ref = 2;(参照カウントを2に設定)、s->sizeclass = runtime·SizeToClass(PageSize/2);s->elemsize = PageSize/2;によって行われます。
    • else: 元のMSpanが複数ページで構成されている場合、これを2つの半分のページ数のMSpanに分割します。
      • 新しいMSpan th->spanallocから割り当て、元のsの最初の半分を表現するように初期化します(runtime·MSpan_Init(t, s->start, npages/2);)。
      • 元のMSpan sは、残りの半分を表現するように更新されます(s->start += npages/2; s->npages = npages/2;)。
      • h->spansテーブル(ページIDからMSpanへのマッピング)も、新しいMSpan tを指すように更新されます。
  3. 分割後のリストへの再挿入:

    • if(s->sizeclass > 0): 分割後のMSpan sが特定のサイズクラスに属する場合、MHeapのロックを解放し、対応するMCentralのロックを取得します。
    • runtime·MSpanList_InsertBack(&c->empty, s);: MSpan sMCentralemptyリストの末尾に挿入します。これは、スイープ済みのMSpanがリストの末尾に配置されるという慣習に従っています。
    • else: s->sizeclass == 0の場合、MSpan sh->busyまたはh->busylargeリストの末尾に挿入します。
    • 最後に、MHeapのロックを解放します。

この一連の処理により、MSpanが分割された後も、常に正しいリストに配置され、メモリ管理の整合性が保たれるようになります。

src/pkg/runtime/stack.cruntime·shrinkstack

この関数は、ゴルーチンのスタックが大きくなりすぎた場合に、そのスタックを縮小する役割を担います。

  • runtime·MSpan_EnsureSwept(span);: runtime·MHeap_SplitSpanを呼び出す前に、この行が追加されました。これは、スタックのMSpanが分割される前に、必ずガベージコレクションのスイープ処理が完了していることを保証します。これにより、未スイープのメモリ領域を操作することによる潜在的なデータ破損やセキュリティ上の問題が回避されます。

これらの変更は、Goランタイムのメモリ管理の堅牢性と正確性を大幅に向上させるものです。

関連リンク

参考にした情報源リンク

  • Goのソースコード (特に src/runtime/mheap.go, src/runtime/mgc.go, src/runtime/stack.go の関連部分)
  • Goのガベージコレクションに関する公式ドキュメントやブログ記事 (例: "Go's new GC: less latency and more throughput")
  • Goのメモリ管理に関する技術解説記事 (例: "Go Memory Management")
  • Issue #7544 の議論スレッド (GitHub)

(注: 上記の参考情報源リンクは一般的なものであり、この解説の生成時に直接参照した特定のURLを指すものではありません。Goのランタイムに関する知識は、公式ドキュメント、ソースコード、コミュニティの議論から得られることが多いです。)