[インデックス 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点にありました。
MSpan
分割後の誤ったリストへの配置: Goのメモリヒープは、MSpan
と呼ばれる連続したページ群を管理します。これらのMSpan
は、そのサイズや状態に応じて異なるリスト(例:h->busy
、h->central->empty
)に分類されます。MSpan
が分割される(例えば、大きなMSpan
から小さなMSpan
を切り出す)際に、分割後のMSpan
が本来属すべきリストとは異なるリストに誤って挿入されることがありました。これにより、メモリの割り当てや解放のロジックが混乱し、メモリ破損やクラッシュにつながる可能性がありました。- 未スイープの
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
を効率的に管理するために、複数のリスト(例: free
、busy
、busylarge
、central
)を持っています。
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ランタイムは並行に動作するため、複数のゴルーチンが同時にメモリヒープを操作する可能性があります。このため、MHeap
やMCentral
のような共有データ構造を保護するためにロック(ミューテックス)が使用されます。これにより、データ競合を防ぎ、メモリ管理の整合性を保ちます。
技術的詳細
このコミットの技術的詳細は、主にruntime·MHeap_SplitSpan
関数の変更と、runtime·MSpan_EnsureSwept
の導入に集約されます。
runtime·MHeap_SplitSpan
の変更点
以前のMHeap_SplitSpan
の実装では、MSpan
を分割する際に、そのMSpan
が現在どのリストに属しているかを適切に考慮していませんでした。単にMHeap
のロックを取得し、MSpan
を分割した後、MHeap
のロックを解放していました。このアプローチでは、分割されたMSpan
が元のリストに残ったままになったり、誤ったリストに再挿入されたりする可能性がありました。
新しい実装では、以下の重要な変更が加えられました。
-
分割前のリストからの削除:
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
が古いリストに残り、二重管理されるような状態を防ぎます。
-
ロックの粒度の調整:
- 以前は
MHeap
全体をロックしていましたが、新しい実装では、MCentral
のリスト操作時にはMCentral
のロックを、MHeap
のリスト操作時にはMHeap
のロックを、というように、より適切な粒度でロックを取得・解放しています。これにより、並行性を高めつつ、データの一貫性を保っています。
- 以前は
-
分割後のリストへの再挿入:
MSpan
が分割され、新しいMSpan
が作成された後、元のMSpan
と新しく作成されたMSpan
は、その新しい状態(サイズクラスなど)に基づいて適切なリストに再挿入されます。s->sizeclass > 0
の場合、MSpan
はh->central[s->sizeclass].empty
リストの末尾に挿入されます。これは、スイープ済みのMSpan
がリストの末尾に配置されるという慣習に従っています。s->sizeclass == 0
の場合、MSpan
はh->busy
またはh->busylarge
リストの末尾に挿入されます。- この再挿入のロジックは、
MSpan
が常に正しいリストに存在することを保証します。
runtime·MSpan_EnsureSwept
の導入
src/pkg/runtime/stack.c
のruntime·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つのファイルが変更されています。
-
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;
-
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); + }
-
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.c
の runtime·MHeap_SplitSpan
この関数は、Goランタイムのメモリ管理において、既存のMSpan
を2つの小さなMSpan
に分割する役割を担います。変更の核心は、MSpan
が分割される前後のリスト管理とロックの取り扱いにあります。
-
分割前のリストからの削除:
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
の場合(大きなMSpan
でh->busy
またはh->busylarge
に属しているはず)、直接runtime·lock(h);
でMHeap
全体をロックし、runtime·MSpanList_Remove(s);
でリストから削除します。- この段階で、
MSpan s
はどのリストにも属していない状態になり、MHeap
のロックが保持されていることが保証されます。
-
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 t
をh->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
を指すように更新されます。
- 新しい
-
分割後のリストへの再挿入:
if(s->sizeclass > 0)
: 分割後のMSpan s
が特定のサイズクラスに属する場合、MHeap
のロックを解放し、対応するMCentral
のロックを取得します。runtime·MSpanList_InsertBack(&c->empty, s);
:MSpan s
をMCentral
のempty
リストの末尾に挿入します。これは、スイープ済みのMSpan
がリストの末尾に配置されるという慣習に従っています。else
:s->sizeclass == 0
の場合、MSpan s
をh->busy
またはh->busylarge
リストの末尾に挿入します。- 最後に、
MHeap
のロックを解放します。
この一連の処理により、MSpan
が分割された後も、常に正しいリストに配置され、メモリ管理の整合性が保たれるようになります。
src/pkg/runtime/stack.c
の runtime·shrinkstack
この関数は、ゴルーチンのスタックが大きくなりすぎた場合に、そのスタックを縮小する役割を担います。
runtime·MSpan_EnsureSwept(span);
:runtime·MHeap_SplitSpan
を呼び出す前に、この行が追加されました。これは、スタックのMSpan
が分割される前に、必ずガベージコレクションのスイープ処理が完了していることを保証します。これにより、未スイープのメモリ領域を操作することによる潜在的なデータ破損やセキュリティ上の問題が回避されます。
これらの変更は、Goランタイムのメモリ管理の堅牢性と正確性を大幅に向上させるものです。
関連リンク
- Go CL (Change List): https://golang.org/cl/76160043
- Go Issue #7544: https://github.com/golang/go/issues/7544
参考にした情報源リンク
- 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のランタイムに関する知識は、公式ドキュメント、ソースコード、コミュニティの議論から得られることが多いです。)