[インデックス 14372] ファイルの概要
このコミットでは、Goランタイムとruntime/cgo
パッケージが変更されています。具体的には、以下の3つのファイルが修正されました。
src/pkg/runtime/cgo/callbacks.c
: CGOコールバックにおけるメモリ割り当ての追跡ロジックが追加されました。src/pkg/runtime/cgocall.c
: CGO呼び出しの開始と終了時に、非Goコードによるメモリ割り当ての追跡を制御するロジックが追加されました。src/pkg/runtime/runtime.h
: ランタイムの内部構造体であるM
(マシン)構造体に、CGO関連のメモリ追跡のためのフィールドが追加されました。
コミット
commit e9a3087e290b52212af1ca2001ea9b24d8797fd0
Author: Ian Lance Taylor <iant@golang.org>
Date: Sat Nov 10 11:19:06 2012 -0800
runtime, runtime/cgo: track memory allocated by non-Go code
Otherwise a poorly timed GC can collect the memory before it
is returned to the Go program.
R=golang-dev, dave, dvyukov, minux.ma
CC=golang-dev
https://golang.org/cl/6819119
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e9a3087e290b52212af1ca2001ea9b24d8797fd0
元コミット内容
runtime, runtime/cgo: track memory allocated by non-Go code
Otherwise a poorly timed GC can collect the memory before it
is returned to the Go program.
変更の背景
このコミットの主な背景は、Goのガベージコレクタ(GC)が、CGO(C言語との相互運用)を通じてCコードによって割り当てられたメモリを認識できないという問題に対処することです。
GoプログラムがCGOを介してC関数を呼び出し、そのC関数がメモリを割り当て、そのポインタをGoに返す場合を考えます。GoのGCはGoヒープ上のオブジェクトのみを追跡し、Cヒープ上のメモリは直接管理しません。もしC関数がメモリを割り当てた直後、しかしそのポインタがGoプログラムに返される前にGCが実行された場合、GoのGCはそのC言語で割り当てられたメモリがどこからも参照されていないと誤って判断し、回収してしまう可能性がありました。これにより、Goプログラムが後でそのポインタを使用しようとした際に、不正なメモリアクセスやクラッシュが発生する可能性がありました。
このコミットは、このような「タイミングの悪いGC」によってCGO経由で割り当てられたメモリが誤って回収されるのを防ぐために、Goランタイムが非Goコードによって割り当てられたメモリを一時的に追跡するメカニズムを導入することを目的としています。
前提知識の解説
Goのガベージコレクタ (GC)
Goは自動メモリ管理を採用しており、ガベージコレクタが不要になったメモリを自動的に解放します。GoのGCは、Goヒープ上に割り当てられたGoオブジェクトを追跡し、到達可能なオブジェクト(プログラムから参照されているオブジェクト)をマークし、到達不可能なオブジェクトを解放する「マーク&スイープ」方式を基本としています。GCはバックグラウンドで並行して動作し、プログラムの実行を一時停止する時間を最小限に抑えるように設計されています。
CGO (C言語との相互運用)
CGOは、GoプログラムからC言語の関数を呼び出したり、C言語のコードからGoの関数を呼び出したりするためのメカニズムです。これにより、既存のCライブラリを利用したり、パフォーマンスが重要な部分をCで記述したりすることが可能になります。
CGOを使用する際、GoとCの間でデータのやり取りが行われます。特にメモリの受け渡しにおいては、GoのメモリモデルとCのメモリモデルの違いを理解することが重要です。
- GoからCへのポインタ渡し: GoのポインタをCに渡す場合、GoのGCはCコードがそのポインタを参照している間は、そのポインタが指すGoメモリを回収しないように保証する必要があります。Go 1.6以降では、
go tool cgo -godefs
で生成されるコードや、C.malloc
などのCGO関数がこの保証を適切に行います。 - CからGoへのポインタ渡し: Cコードが
malloc
などでメモリを割り当て、そのポインタをGoに返す場合、GoのGCはそのC言語で割り当てられたメモリを直接管理しません。Go側でそのポインタを保持している間は、C側でそのメモリを解放しないように注意する必要があります。このコミットが対処しようとしているのは、まさにこのシナリオにおけるGCの誤動作です。
GoとCのメモリ管理の違い
- Go: Goランタイムがヒープメモリを管理し、GCが不要なメモリを自動的に解放します。開発者は通常、メモリの解放について明示的にコードを書く必要がありません。
- C: 開発者が
malloc
やcalloc
でメモリを明示的に割り当て、free
で明示的に解放する必要があります。解放を忘れるとメモリリークが発生し、二重解放はクラッシュの原因となります。
この違いが、CGOを介してメモリがやり取りされる際に問題を引き起こす可能性があります。GoのGCはCのメモリ割り当てを認識しないため、Cが割り当てたメモリがGoプログラムに返される前にGCが走ると、Goの視点からはそのメモリが「到達不可能」に見えてしまい、誤って回収されるリスクがありました。
技術的詳細
このコミットは、GoランタイムのM
(マシン)構造体に新しいフィールドを追加し、CGOコールバック中に非Goコードによって割り当てられたメモリを一時的に追跡するメカニズムを導入しています。
-
runtime.h
の変更:struct CgoMal
という新しい構造体が定義されました。これは、CGOコール中に非Goコードによって割り当てられたメモリを追跡するためのリンクリストのノードとして機能します。// Track memory allocated by code not written in Go during a cgo call, // so that the garbage collector can see them. struct CgoMal { CgoMal *next; byte *alloc; };
next
:CgoMal
構造体のリンクリストを形成するための次の要素へのポインタ。alloc
: 非Goコードによって割り当てられたメモリブロックへのポインタ。
struct M
(マシン構造体)に以下のフィールドが追加されました。int32 ncgo; // number of cgo calls currently in progress CgoMal* cgomal;
ncgo
: 現在進行中のCGO呼び出しの数を追跡します。これにより、GoランタイムはCGO呼び出しのネストレベルを把握できます。cgomal
: 現在のM
(OSスレッドに紐づくGoの論理プロセッサ)に関連付けられた、非Goコードによって割り当てられたメモリブロックのリンクリストの先頭を指します。
-
callbacks.c
の変更:_cgo_allocate_internal
関数は、CGOコールバック中にCコードがメモリを割り当てる際に使用される内部関数です。この関数に、割り当てられたメモリをCgoMal
リンクリストに追加するロジックが追加されました。
これにより、CGOコールバック中にCコードがメモリを割り当てた場合、そのメモリブロックのポインタがstatic void _cgo_allocate_internal(uintptr len, byte *ret) { CgoMal *c; ret = runtime·mal(len); // Goランタイムのメモリ割り当て関数 c = runtime·mal(sizeof(*c)); // CgoMal構造体自体もGoヒープに割り当てる c->next = m->cgomal; // 既存のリストの先頭に新しい要素を追加 c->alloc = ret; // 割り当てられたメモリブロックのポインタを保存 m->cgomal = c; // リストの先頭を更新 FLUSH(&ret); }
m->cgomal
リンクリストに一時的に記録されます。GoのGCは、このリンクリストを辿ることで、CGOコール中に割り当てられたメモリがまだ「参照されている」と判断し、誤って回収するのを防ぐことができます。
-
cgocall.c
の変更:runtime·cgocall
関数は、GoからC関数を呼び出す際の主要なエントリポイントです。- CGO呼び出しに入る際に
m->ncgo++
でncgo
カウンタをインクリメントします。 - CGO呼び出しから戻る際に
m->ncgo--
でncgo
カウンタをデクリメントします。 m->ncgo
が0になった場合(つまり、すべてのネストされたCGO呼び出しが完了し、Goコードに戻る直前)、m->cgomal = nil;
と設定されます。// ... m->ncgo++; // CGO呼び出し開始 // ... CGO呼び出しの実行 ... m->ncgo--; // CGO呼び出し終了 if(m->ncgo == 0) { // We are going back to Go and are not in a recursive // call. Let the GC collect any memory allocated via // _cgo_allocate that is no longer referenced. m->cgomal = nil; } // ...
m->cgomal = nil;
とすることで、CGO呼び出し中に追跡されていたメモリブロックのリンクリストがクリアされます。これにより、GoプログラムがCGO呼び出しから返された後、Go側でそのメモリへの参照がなくなった場合に、GoのGCがそのメモリを適切に回収できるようになります。これは、CGO呼び出し中に一時的に追跡するだけで、Goプログラムがそのメモリを使い続ける場合は、Go側でそのポインタを保持し続ける必要があることを意味します。
コアとなるコードの変更箇所
src/pkg/runtime/cgo/callbacks.c
--- a/src/pkg/runtime/cgo/callbacks.c
+++ b/src/pkg/runtime/cgo/callbacks.c
@@ -33,7 +33,13 @@
static void
_cgo_allocate_internal(uintptr len, byte *ret)
{
+ CgoMal *c;
+
ret = runtime·mal(len);
+ c = runtime·mal(sizeof(*c));
+ c->next = m->cgomal;
+ c->alloc = ret;
+ m->cgomal = c;
FLUSH(&ret);
}
src/pkg/runtime/cgocall.c
--- a/src/pkg/runtime/cgocall.c
+++ b/src/pkg/runtime/cgocall.c
@@ -135,6 +135,8 @@ runtime·cgocall(void (*fn)(void*), void *arg)
g->defer = &d;
}
+ m->ncgo++;
+
/*
* Announce we are entering a system call
* so that the scheduler knows to create another
@@ -150,6 +152,14 @@ runtime·cgocall(void (*fn)(void*), void *arg)
runtime·asmcgocall(fn, arg);
runtime·exitsyscall();
+ m->ncgo--;
+ if(m->ncgo == 0) {
+ // We are going back to Go and are not in a recursive
+ // call. Let the GC collect any memory allocated via
+ // _cgo_allocate that is no longer referenced.
+ m->cgomal = nil;
+ }
+
if(d.nofree) {
if(g->defer != &d || d.fn != (byte*)unlockm)
runtime·throw("runtime: bad defer entry in cgocallback");
src/pkg/runtime/runtime.h
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -81,6 +81,7 @@ typedef struct GCStats GCStats;
typedef struct LFNode LFNode;
typedef struct ParFor ParFor;
typedef struct ParForThread ParForThread;
+typedef struct CgoMal CgoMal;
/*
* Per-CPU declaration.
@@ -249,7 +250,9 @@ struct M
int32 profilehz;
int32 helpgc;
uint32 fastrand;
- uint64 ncgocall;
+ uint64 ncgocall; // number of cgo calls in total
+ int32 ncgo; // number of cgo calls currently in progress
+ CgoMal* cgomal;
Note havenextg;
G* nextg;
M* alllink; // on allm
@@ -414,6 +417,14 @@ struct ParFor
uint64 nsleep;
};
+// Track memory allocated by code not written in Go during a cgo call,
+// so that the garbage collector can see them.
+struct CgoMal
+{
+ CgoMal *next;
+ byte *alloc;
+};
+
/*
* defined macros
* you need super-gopher-guru privilege
コアとなるコードの解説
このコミットの核心は、GoのガベージコレクタがCGO呼び出し中にCコードによって割り当てられたメモリを一時的に「認識」できるようにすることです。
-
CgoMal
構造体とm->cgomal
リンクリスト:CgoMal
構造体は、CGOコールバック中に_cgo_allocate_internal
関数(CGO経由でGoランタイムのメモリ割り当て機能を利用するCコードが使用)によって割り当てられた各メモリブロックのポインタ(alloc
フィールド)を保持します。- これらの
CgoMal
インスタンスは、m->cgomal
をヘッドとするリンクリストとして連結されます。m
は現在のOSスレッドに紐づくGoの論理プロセッサ(M: Machine)を表す構造体です。 - GoのGCは、GCサイクル中に
m->cgomal
リンクリストを辿ることで、リスト内のalloc
フィールドが指すメモリブロックが「到達可能」であると判断します。これにより、CGO呼び出し中にCコードが割り当てたメモリが、Goプログラムにポインタが返される前にGCによって誤って回収されるのを防ぎます。
-
m->ncgo
カウンタ:runtime·cgocall
関数が呼び出されるたびにm->ncgo
がインクリメントされ、C関数からGoに戻る際にデクリメントされます。- このカウンタは、現在のM(OSスレッド)上で実行中のCGO呼び出しのネストレベルを追跡します。
m->ncgo
が0になったとき、それは現在のMがすべてのCGO呼び出しから完全にGoコードに戻ったことを意味します。この時点で、m->cgomal = nil;
と設定されます。
-
メモリのライフサイクル管理:
- CGO呼び出し中にCコードが
_cgo_allocate_internal
を使ってメモリを割り当てると、そのメモリはm->cgomal
リストに追加され、GCから保護されます。 - CGO呼び出しが完了し、
m->ncgo
が0に戻ると、m->cgomal
リストがクリアされます。これにより、Goランタイムはもはやこれらのメモリブロックを「参照している」とは見なさなくなります。 - この時点で、もしGoプログラムがCGO呼び出しから返されたポインタを保持していなければ、そのメモリブロックはGoのGCによって回収される対象となります。もしGoプログラムがそのポインタを保持し続けている場合は、GoのGCはそのポインタを介してメモリブロックが到達可能であると判断し、回収しません。
- このメカニズムは、CGO呼び出し中に一時的に割り当てられたメモリが、Goプログラムがそのポインタを受け取る前にGCによって誤って回収されるという特定の競合状態を解決します。GoプログラムがCから受け取ったメモリを使い続ける場合、そのメモリの解放は依然としてC側の責任(またはGo側で
C.free
を呼び出すなど)となります。
- CGO呼び出し中にCコードが
この変更により、GoとCのメモリ管理の境界における潜在的なバグが修正され、CGOの堅牢性が向上しました。
関連リンク
- Go Change-list 6819119 - このコミットに対応するGoのコードレビューシステム(Gerrit)の変更リスト。詳細な議論やレビューコメントが含まれている可能性があります。
- Go Programming Language - Cgo - Go公式ブログのCGOに関する記事。CGOの基本的な使い方や注意点について解説されています。
- Go: The complete guide to Cgo - CGOに関する詳細なガイド記事。
参考にした情報源リンク
- 上記の「関連リンク」セクションに記載されたGoの公式ドキュメントやブログ記事、およびCGOに関する一般的な技術記事。
- Goのガベージコレクタの動作に関する一般的な知識。
- C言語のメモリ管理(
malloc
/free
)に関する一般的な知識。 - Goランタイムの内部構造(
M
、G
、P
など)に関する一般的な知識。 - コミットメッセージとコードの差分。