[インデックス 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
を呼び出し、findfunc
がbuildfuncs
を呼び出し、buildfuncs
が再度malloc
を呼び出すという、malloc
の再入が発生する可能性がありました。このような再入は、特にメモリ割り当てがロックによって保護されている場合、デッドロックを引き起こす可能性があります。malloc
がすでにロックを保持している状態で、再入したmalloc
が同じロックを要求すると、プログラムは停止してしまいます。
このコミットは、このようなデッドロックのシナリオを回避するために導入されました。
前提知識の解説
malloc
: C言語の標準ライブラリ関数で、動的にメモリを割り当てるために使用されます。Goランタイムも内部的にメモリ割り当ての概念を持ち、Goのヒープメモリ管理の根幹をなします。- Goランタイム: Goプログラムの実行を管理するシステムです。ガベージコレクタ、スケジューラ、メモリ管理など、多くの低レベルな機能を提供します。
runtime·findfunc(uintptr addr)
: Goランタイムの内部関数で、指定されたメモリアドレスaddr
がどの関数に属するかを検索します。デバッグ情報やプロファイリング、パニック時のスタックトレース生成などで利用されます。buildfuncs()
: Goランタイムの内部関数で、実行中のプログラムの関数シンボルテーブルを構築または更新します。これにより、findfunc
が正確な関数情報を取得できるようになります。この処理は、プログラムの起動時や、動的にロードされるコードがある場合に実行されることがあります。m->mallocing
: Goランタイムの内部構造体m
(M
、マシンまたは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->mallocing
がtrue
であるため、buildfuncs()
の呼び出しがスキップされます。同様に、ガベージコレクション中に同様のシナリオが発生した場合も、m->gcing
がtrue
であるためスキップされます。
この修正により、malloc
やGCといったクリティカルなセクションでbuildfuncs
が再入的にmalloc
を呼び出すことによるデッドロックが効果的に回避されます。シンボルテーブルの初期化は、これらのクリティカルな状態が解除された後、安全なタイミングで再度findfunc
が呼び出された際に実行されることになります。
関連リンク
- Go CL: https://golang.org/cl/5689047
- Go Issue #1777 (コミットメッセージに記載されているが、公開されているGoリポジトリのIssue #1777は別の内容であるため、このコミットが修正した具体的な問題の詳細はコミットメッセージとコード変更から読み取る必要がある): https://go.dev/issue/1777
参考にした情報源リンク
- Go issue 1777 (2012年の情報): https://go.dev/issue/1777
- Go runtime memory allocation (general concepts):