[インデックス 16812] ファイルの概要
このコミットは、Goランタイムにおけるdeferreturn
処理中のプリエンプション(横取り)を無効化することで、スタックの一貫性問題を解決することを目的としています。具体的には、defer
関数の引数コピーとプログラムカウンタの修正の間にプリエンプションが発生すると、ガベージコレクタがスタックを正しく解釈できなくなる可能性があったため、その期間のプリエンプションを禁止します。
コミット
commit ef12bbfc9ddbb168fcd2ab0ad0bd364e40a1ab7f
Author: Russ Cox <rsc@golang.org>
Date: Thu Jul 18 12:26:47 2013 -0400
runtime: disable preemption during deferreturn
Deferreturn is synthesizing a new call frame.
It must not be interrupted between copying the args there
and fixing up the program counter, or else the stack will
be in an inconsistent state, one that will confuse the
garbage collector.
R=golang-dev, dvyukov
CC=golang-dev
https://golang.org/cl/11522043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ef12bbfc9ddbb168fcd2ab0ad0bd364e40a1ab7f
元コミット内容
runtime: disable preemption during deferreturn
Deferreturn is synthesizing a new call frame.
It must not be interrupted between copying the args there
and fixing up the program counter, or else the stack will
be in an inconsistent state, one that will confuse the
garbage collector.
R=golang-dev, dvyukov
CC=golang-dev
https://golang.org/cl/11522043
変更の背景
Go言語のdefer
ステートメントは、関数がリターンする直前に実行されるコードをスケジュールする強力な機能です。このdefer
が実行される際には、ランタイム内部でdeferreturn
という処理が走ります。deferreturn
は、新しいコールフレームを合成し、引数をコピーし、プログラムカウンタ(PC)を修正するという一連の操作を行います。
この一連の操作中に、Goランタイムのスケジューラによるプリエンプション(実行中のゴルーチンを中断し、別のゴルーチンに切り替える処理)が発生すると、問題が生じる可能性がありました。特に、引数のコピーが完了し、PCの修正が行われるまでの間にプリエンプションされると、スタックが一時的に不整合な状態になります。この不整合な状態のスタックは、Goのガベージコレクタ(GC)がスタックをスキャンする際に誤った情報を与え、GCの正確な動作を妨げる可能性がありました。
ガベージコレクタは、プログラムが使用しているメモリを正確に識別するために、すべてのゴルーチンのスタックをスキャンして、どのメモリがまだ参照されているか(ライブであるか)を判断する必要があります。スタックが不整合な状態にあると、GCはポインタを正しく識別できず、誤ってライブなオブジェクトを解放したり、逆に不要なオブジェクトを保持し続けたりする可能性があります。これは、プログラムのクラッシュやメモリリーク、あるいはより深刻なデータ破損につながる可能性があります。
このコミットは、このようなスタックの不整合によるGCの混乱を防ぐために、deferreturn
処理のクリティカルセクションにおいてプリエンプションを一時的に無効化することを目的としています。
前提知識の解説
Goランタイム
Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ゴルーチン(軽量スレッド)のスケジューリング、メモリ管理(ガベージコレクションを含む)、チャネル通信、システムコールなどが含まれます。Goプログラムは、オペレーティングシステム上で直接実行されるのではなく、このランタイム上で動作します。
ゴルーチンとプリエンプション
Goのゴルーチンは、OSのスレッドよりもはるかに軽量な並行実行単位です。数百万のゴルーチンを同時に実行することも可能です。Goランタイムのスケジューラは、これらのゴルーチンをOSスレッドにマッピングし、効率的に実行を切り替えます。
プリエンプションとは、スケジューラが現在実行中のゴルーチンを中断し、別のゴルーチンにCPUの実行権を渡すことです。これにより、すべてのゴルーチンが公平にCPU時間を受け取り、応答性の高いアプリケーションを実現できます。Goのプリエンプションは、協調的(ゴルーチンが自ら中断する)と非同期(スケジューラが強制的に中断する)の両方があります。このコミットで問題となっているのは、非同期プリエンプションがクリティカルな操作中に発生することです。
ガベージコレクション(GC)とスタックスキャン
Goは自動メモリ管理を採用しており、ガベージコレクタが不要になったメモリを自動的に回収します。GoのGCは、並行マーク&スイープ方式を採用しており、ユーザープログラムの実行と並行して動作します。
GCがメモリを回収するためには、どのメモリがまだ使用されているかを正確に知る必要があります。これを「ライブオブジェクトの特定」と呼びます。ライブオブジェクトを特定するために、GCはプログラムのルート(グローバル変数、レジスタ、そしてゴルーチンのスタック)から到達可能なすべてのオブジェクトをマークします。
スタックスキャンは、GCがゴルーチンのスタックを調べて、スタック上に存在するポインタ(他のメモリ領域を指すアドレス)を識別するプロセスです。スタック上のポインタを正確に識別できなければ、GCはライブなオブジェクトを誤ってデッドと判断し、回収してしまう可能性があります(Use-After-Freeバグの原因)。逆に、デッドなオブジェクトをライブと判断し、メモリリークを引き起こす可能性もあります。
defer
とdeferreturn
defer
ステートメントは、Goの関数がリターンする直前に実行される関数呼び出しをスケジュールします。これは、リソースの解放(ファイルのクローズ、ロックの解除など)やエラーハンドリングによく使用されます。
defer
がスケジュールされた関数は、Goランタイムの内部的なdefer
スタックに積まれます。関数がリターンする際、このdefer
スタックから関数が取り出され、runtime.deferreturn
という内部関数によって実行されます。deferreturn
は、呼び出し元の関数のコンテキストから、defer
された関数を実行するための新しいコールフレームを動的に構築する役割を担います。
技術的詳細
このコミットが対処している問題は、deferreturn
が新しいコールフレームを合成する際の原子性(不可分性)の欠如です。具体的には、以下のステップが連続して行われる必要があります。
defer
された関数の引数を、新しいコールフレームのスタック領域にコピーする。- プログラムカウンタ(PC)を、
defer
された関数のエントリポイントに修正する。
もし、ステップ1とステップ2の間にプリエンプションが発生し、ガベージコレクタがスタックスキャンを実行した場合、GCは不完全な状態のスタックフレームを観測することになります。例えば、引数はコピーされたがPCはまだ修正されていない場合、GCはスタック上のポインタを正しく解釈できず、誤ったメモリ参照を追跡したり、重要なポインタを見落としたりする可能性があります。
この問題を解決するために、コミットではdeferreturn
処理のクリティカルセクションにおいて、Goランタイムのプリエンプションメカニズムを一時的に無効化しています。これは、m->locks
カウンタをインクリメントすることで実現されます。
m->locks++
: 現在のM(OSスレッド)に関連付けられたロックカウンタをインクリメントします。このカウンタが0より大きい間は、スケジューラは現在のゴルーチンをプリエンプトしません。これは、ランタイム内部のクリティカルセクションでよく使われる手法です。runtime·memmove(argp, d->args, d->siz)
:defer
された関数の引数を、新しいスタックフレームにコピーします。fn = d->fn
:defer
された関数のポインタを取得します。popdefer()
:defer
スタックから現在のdefer
エントリをポップします。freedefer(d)
:defer
エントリが使用していたメモリを解放します。m->locks--
: ロックカウンタをデクリメントします。if(m->locks == 0 && g->preempt)
: ロックカウンタが0に戻り、かつ現在のゴルーチンがプリエンプションを要求されている場合(g->preempt
が真の場合)、g->stackguard0
をStackPreempt
に設定します。これは、次にゴルーチンが関数呼び出しなどを行う際に、プリエンプションチェックがトリガーされるようにするシグナルです。runtime·jmpdefer(fn, argp)
:defer
された関数にジャンプし、実行を開始します。このジャンプによってPCが修正されます。
この変更により、引数のコピーからPCの修正(jmpdefer
によるジャンプ)までの間、プリエンプションが確実に無効化され、スタックが常に一貫した状態に保たれることが保証されます。これにより、ガベージコレクタがスタックをスキャンする際に、不整合な状態に遭遇するリスクがなくなります。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/panic.c
ファイルのruntime·deferreturn
関数内で行われています。
--- a/src/pkg/runtime/panic.c
+++ b/src/pkg/runtime/panic.c
@@ -175,10 +175,19 @@ runtime·deferreturn(uintptr arg0, ...)
argp = (byte*)&arg0;
if(d->argp != argp)
return;
+
+ // Moving arguments around.
+ // Do not allow preemption here, because the garbage collector
+ // won't know the form of the arguments until the jmpdefer can
+ // flip the PC over to fn.
+ m->locks++;
runtime·memmove(argp, d->args, d->siz);
fn = d->fn;
popdefer();
freedefer(d);
+ m->locks--;
+ if(m->locks == 0 && g->preempt)
+ g->stackguard0 = StackPreempt;
runtime·jmpdefer(fn, argp);
}
コアとなるコードの解説
追加されたコードは以下の通りです。
-
m->locks++;
runtime·memmove
による引数コピーの直前に、現在のM(OSスレッド)のロックカウンタm->locks
をインクリメントしています。- このカウンタが0より大きい間は、Goスケジューラは現在のゴルーチンをプリエンプトしません。これにより、引数コピーから
jmpdefer
によるPC修正までのクリティカルセクションが保護されます。
-
m->locks--;
freedefer(d)
の直後、つまりクリティカルセクションの終了時にロックカウンタをデクリメントしています。
-
if(m->locks == 0 && g->preempt)
- ロックカウンタが0に戻った(クリティカルセクションが終了した)後、かつ現在のゴルーチンがプリエンプションを要求されている場合(
g->preempt
が真の場合)に、プリエンプションをトリガーするための処理を行います。 g->stackguard0 = StackPreempt;
- これは、次にゴルーチンが関数呼び出しなどを行う際に、スタックガードチェックが失敗し、プリエンプションが実行されるようにするためのフラグ設定です。これにより、クリティカルセクションを抜けた直後に、保留されていたプリエンプションが安全に実行されるようになります。
- ロックカウンタが0に戻った(クリティカルセクションが終了した)後、かつ現在のゴルーチンがプリエンプションを要求されている場合(
これらの変更により、deferreturn
がスタックフレームを構築する際の引数コピーとPC修正の間に、ガベージコレクタが不整合なスタックをスキャンする可能性が排除され、ランタイムの堅牢性が向上しました。
関連リンク
- Go言語の
defer
ステートメントに関する公式ドキュメントやチュートリアル - Goランタイムのスケジューラやガベージコレクタに関する技術記事や論文
- Goのソースコードリポジトリ(特に
src/runtime
ディレクトリ)
参考にした情報源リンク
- https://github.com/golang/go/commit/ef12bbfc9ddbb168fcd2ab0ad0bd364e40a1ab7f
- Web検索結果: "Go runtime deferreturn preemption garbage collector stack inconsistency"
- 特に、Goのプリエンプション、GCのスタックスキャン、
defer
の内部動作に関する情報源。 - Goの内部実装に関するブログ記事やGoのソースコード解説。
- 特に、Goのプリエンプション、GCのスタックスキャン、