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

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

このコミットは、Goランタイムにおけるメモリ割り当て(malloc)処理中の潜在的なデッドロック問題を解決することを目的としています。具体的には、malloc中に発生したフォールト(エラー)が、シンボルテーブルの初期化関数であるfindfuncを呼び出し、そのfindfuncがさらにmallocを呼び出すという再入(re-entrancy)のシナリオを防ぎます。

コミット

commit fc7ed45b35d24d6d67720e5085c083041a8dd30e
Author: Russ Cox <rsc@golang.org>
Date:   Tue Feb 21 16:36:15 2012 -0500

    runtime: avoid malloc during malloc
    
    A fault during malloc might lead to the program's
    first call to findfunc, which would in turn call malloc.
    Don't do that.
    
    Fixes #1777.
    
    R=golang-dev, gri
    CC=golang-dev
    https://golang.org/cl/5689047

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

https://github.com/golang/go/commit/fc7ed45b35d24d6d67720e5085c083041a8dd30e

元コミット内容

malloc処理中に発生する可能性のあるフォールトが、プログラムのfindfuncへの最初の呼び出しを引き起こし、そのfindfuncがさらにmallocを呼び出すという、malloc処理中のmalloc呼び出しを回避する。

変更の背景

Goランタイムは、効率的なメモリ管理のために独自のガベージコレクタとアロケータを持っています。mallocは、Goプログラムがメモリを動的に確保する際に使用される基本的な操作です。しかし、このmalloc処理中に予期せぬフォールト(例えば、メモリ保護違反など)が発生した場合、Goランタイムは通常、そのフォールトを処理するためにスタックトレースを生成したり、関連する情報を収集したりします。

この情報収集の過程で、ランタイムはシンボル情報を解決するためにruntime·findfunc関数を呼び出すことがあります。runtime·findfuncは、特定のメモリアドレスに対応する関数を見つけるための関数であり、その内部でシンボルテーブルの初期化(buildfuncsの呼び出し)が必要になる場合があります。問題は、このbuildfuncs関数が、その処理の一部としてメモリを割り当てるために再びmallocを呼び出す可能性がある点にありました。

つまり、malloc中にフォールトが発生し、そのフォールト処理がfindfuncを呼び出し、findfuncbuildfuncsを呼び出し、buildfuncsが再度mallocを呼び出すという、mallocの再入が発生する可能性がありました。このような再入は、特にメモリ割り当てがロックによって保護されている場合、デッドロックを引き起こす可能性があります。mallocがすでにロックを保持している状態で、再入したmallocが同じロックを要求すると、プログラムは停止してしまいます。

このコミットは、このようなデッドロックのシナリオを回避するために導入されました。

前提知識の解説

  • malloc: C言語の標準ライブラリ関数で、動的にメモリを割り当てるために使用されます。Goランタイムも内部的にメモリ割り当ての概念を持ち、Goのヒープメモリ管理の根幹をなします。
  • Goランタイム: Goプログラムの実行を管理するシステムです。ガベージコレクタ、スケジューラ、メモリ管理など、多くの低レベルな機能を提供します。
  • runtime·findfunc(uintptr addr): Goランタイムの内部関数で、指定されたメモリアドレスaddrがどの関数に属するかを検索します。デバッグ情報やプロファイリング、パニック時のスタックトレース生成などで利用されます。
  • buildfuncs(): Goランタイムの内部関数で、実行中のプログラムの関数シンボルテーブルを構築または更新します。これにより、findfuncが正確な関数情報を取得できるようになります。この処理は、プログラムの起動時や、動的にロードされるコードがある場合に実行されることがあります。
  • m->mallocing: Goランタイムの内部構造体mM、マシンまたはOSスレッドを表す)のフィールドで、現在のOSスレッドがメモリ割り当て処理中であるかどうかを示すフラグです。
  • m->gcing: 同様に、現在のOSスレッドがガベージコレクション処理中であるかどうかを示すフラグです。ガベージコレクション中もメモリ割り当てが制限される場合があります。
  • デッドロック: 複数のプロセスやスレッドが、互いに相手が保持しているリソースの解放を待っている状態になり、結果としてどのプロセスも処理を進められなくなる状態です。メモリ割り当てのロックが関与する場合に発生しやすい問題です。
  • 再入(Re-entrancy): ある関数が実行中に、自分自身または別の関数を呼び出し、その呼び出しが元の関数の実行を中断して、再度同じ関数または関連する関数を実行することです。特に、共有リソース(この場合はメモリ割り当てのロック)を扱う関数で再入が発生すると、競合状態やデッドロックの原因となることがあります。

技術的詳細

この問題は、Goランタイムのメモリ管理とフォールトハンドリングの間の相互作用に起因します。Goランタイムは、非常に効率的なメモリ割り当てシステムを持っていますが、その内部処理は複雑です。

通常、runtime·findfuncは、プログラムの実行中にシンボル情報を解決するために呼び出されます。しかし、mallocのような低レベルな操作中にフォールトが発生した場合、ランタイムはフォールトハンドラに制御を移します。このフォールトハンドラ内でfindfuncが呼び出されると、findfuncはまだ初期化されていないシンボルテーブルを初期化するためにbuildfuncsを呼び出す可能性があります。

buildfuncsは、プログラムのバイナリからシンボル情報を読み込み、メモリ上に構造体を構築する過程で、新たなメモリ割り当て(すなわちmalloc)を必要とします。ここで、元のmallocがまだ完了しておらず、かつメモリ割り当てに関連するロックを保持している場合、buildfuncs内のmalloc呼び出しは同じロックを要求し、デッドロックが発生します。

このコミットでは、このデッドロックを防ぐために、runtime·findfunc内でbuildfuncsを呼び出す前に、現在のスレッドがmalloc処理中(m->mallocingがtrue)またはガベージコレクション中(m->gcingがtrue)であるかをチェックする条件を追加しました。もしこれらのフラグがtrueであれば、buildfuncsの呼び出しをスキップします。これにより、malloc処理中やGC中にmallocの再入が発生するのを防ぎ、デッドロックを回避します。

この変更は、findfuncの初期化ロジックに影響を与えますが、malloc中やGC中にbuildfuncsがスキップされても、その後のfindfuncの呼び出しでシンボルテーブルが初期化されるため、最終的な機能には影響を与えません。重要なのは、危険な再入の状況を避けることです。

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

src/pkg/runtime/symtab.c ファイルの runtime·findfunc 関数内の変更です。

--- a/src/pkg/runtime/symtab.c
+++ b/src/pkg/runtime/symtab.c
@@ -437,13 +437,17 @@ runtime·findfunc(uintptr addr)
 	// (Before enabling the signal handler,
 	// SetCPUProfileRate calls findfunc to trigger
 	// the initialization outside the handler.)
-	if(runtime·atomicload(&funcinit) == 0) {
-		runtime·lock(&funclock);
-		if(funcinit == 0) {
-			buildfuncs();
-			runtime·atomicstore(&funcinit, 1);
-		}
-		runtime·unlock(&funclock);
+	// Avoid deadlock on fault during malloc
+	// by not calling buildfuncs if we're already in malloc.
+	if(!m->mallocing && !m->gcing) {
+		if(runtime·atomicload(&funcinit) == 0) {
+			runtime·lock(&funclock);
+			if(funcinit == 0) {
+				buildfuncs();
+				runtime·atomicstore(&funcinit, 1);
+			}
+			runtime·unlock(&funclock);
+		}
 	}
 
 	if(nfunc == 0)

コアとなるコードの解説

変更は、runtime·findfunc関数内のシンボルテーブル初期化ブロックにあります。

元のコードでは、funcinitというアトミック変数が0(未初期化)の場合に、funclockというロックを取得し、再度funcinitが0であることを確認してからbuildfuncs()を呼び出し、funcinitを1に設定して初期化を完了していました。

変更後のコードでは、この初期化ブロック全体が新しい条件文で囲まれています。

if(!m->mallocing && !m->gcing) {
    // ... 既存の初期化ロジック ...
}
  • mは現在のOSスレッド(M構造体)を表すポインタです。
  • m->mallocingは、現在のスレッドがメモリ割り当て処理中である場合にtrueになります。
  • m->gcingは、現在のスレッドがガベージコレクション処理中である場合にtrueになります。

この条件!m->mallocing && !m->gcingは、「現在のスレッドがメモリ割り当て中ではなく、かつガベージコレクション中でもない場合のみ」ということを意味します。

これにより、malloc処理中にフォールトが発生し、findfuncが呼び出されたとしても、m->mallocingtrueであるため、buildfuncs()の呼び出しがスキップされます。同様に、ガベージコレクション中に同様のシナリオが発生した場合も、m->gcingtrueであるためスキップされます。

この修正により、mallocやGCといったクリティカルなセクションでbuildfuncsが再入的にmallocを呼び出すことによるデッドロックが効果的に回避されます。シンボルテーブルの初期化は、これらのクリティカルな状態が解除された後、安全なタイミングで再度findfuncが呼び出された際に実行されることになります。

関連リンク

  • Go CL: https://golang.org/cl/5689047
  • Go Issue #1777 (コミットメッセージに記載されているが、公開されているGoリポジトリのIssue #1777は別の内容であるため、このコミットが修正した具体的な問題の詳細はコミットメッセージとコード変更から読み取る必要がある): https://go.dev/issue/1777

参考にした情報源リンク