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

[インデックス 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: 開発者がmalloccallocでメモリを明示的に割り当て、freeで明示的に解放する必要があります。解放を忘れるとメモリリークが発生し、二重解放はクラッシュの原因となります。

この違いが、CGOを介してメモリがやり取りされる際に問題を引き起こす可能性があります。GoのGCはCのメモリ割り当てを認識しないため、Cが割り当てたメモリがGoプログラムに返される前にGCが走ると、Goの視点からはそのメモリが「到達不可能」に見えてしまい、誤って回収されるリスクがありました。

技術的詳細

このコミットは、GoランタイムのM(マシン)構造体に新しいフィールドを追加し、CGOコールバック中に非Goコードによって割り当てられたメモリを一時的に追跡するメカニズムを導入しています。

  1. 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コードによって割り当てられたメモリブロックのリンクリストの先頭を指します。
  2. callbacks.cの変更:

    • _cgo_allocate_internal関数は、CGOコールバック中にCコードがメモリを割り当てる際に使用される内部関数です。この関数に、割り当てられたメモリをCgoMalリンクリストに追加するロジックが追加されました。
      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);
      }
      
      これにより、CGOコールバック中にCコードがメモリを割り当てた場合、そのメモリブロックのポインタがm->cgomalリンクリストに一時的に記録されます。GoのGCは、このリンクリストを辿ることで、CGOコール中に割り当てられたメモリがまだ「参照されている」と判断し、誤って回収するのを防ぐことができます。
  3. 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コードによって割り当てられたメモリを一時的に「認識」できるようにすることです。

  1. 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によって誤って回収されるのを防ぎます。
  2. m->ncgoカウンタ:

    • runtime·cgocall関数が呼び出されるたびにm->ncgoがインクリメントされ、C関数からGoに戻る際にデクリメントされます。
    • このカウンタは、現在のM(OSスレッド)上で実行中のCGO呼び出しのネストレベルを追跡します。
    • m->ncgoが0になったとき、それは現在のMがすべてのCGO呼び出しから完全にGoコードに戻ったことを意味します。この時点で、m->cgomal = nil;と設定されます。
  3. メモリのライフサイクル管理:

    • 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を呼び出すなど)となります。

この変更により、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ランタイムの内部構造(MGPなど)に関する一般的な知識。
  • コミットメッセージとコードの差分。