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

[インデックス 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バグの原因)。逆に、デッドなオブジェクトをライブと判断し、メモリリークを引き起こす可能性もあります。

deferdeferreturn

deferステートメントは、Goの関数がリターンする直前に実行される関数呼び出しをスケジュールします。これは、リソースの解放(ファイルのクローズ、ロックの解除など)やエラーハンドリングによく使用されます。

deferがスケジュールされた関数は、Goランタイムの内部的なdeferスタックに積まれます。関数がリターンする際、このdeferスタックから関数が取り出され、runtime.deferreturnという内部関数によって実行されます。deferreturnは、呼び出し元の関数のコンテキストから、deferされた関数を実行するための新しいコールフレームを動的に構築する役割を担います。

技術的詳細

このコミットが対処している問題は、deferreturnが新しいコールフレームを合成する際の原子性(不可分性)の欠如です。具体的には、以下のステップが連続して行われる必要があります。

  1. deferされた関数の引数を、新しいコールフレームのスタック領域にコピーする。
  2. プログラムカウンタ(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->stackguard0StackPreemptに設定します。これは、次にゴルーチンが関数呼び出しなどを行う際に、プリエンプションチェックがトリガーされるようにするシグナルです。
  • 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);
 }

コアとなるコードの解説

追加されたコードは以下の通りです。

  1. m->locks++;

    • runtime·memmoveによる引数コピーの直前に、現在のM(OSスレッド)のロックカウンタm->locksをインクリメントしています。
    • このカウンタが0より大きい間は、Goスケジューラは現在のゴルーチンをプリエンプトしません。これにより、引数コピーからjmpdeferによるPC修正までのクリティカルセクションが保護されます。
  2. m->locks--;

    • freedefer(d)の直後、つまりクリティカルセクションの終了時にロックカウンタをデクリメントしています。
  3. if(m->locks == 0 && g->preempt)

    • ロックカウンタが0に戻った(クリティカルセクションが終了した)後、かつ現在のゴルーチンがプリエンプションを要求されている場合(g->preemptが真の場合)に、プリエンプションをトリガーするための処理を行います。
    • g->stackguard0 = StackPreempt;
      • これは、次にゴルーチンが関数呼び出しなどを行う際に、スタックガードチェックが失敗し、プリエンプションが実行されるようにするためのフラグ設定です。これにより、クリティカルセクションを抜けた直後に、保留されていたプリエンプションが安全に実行されるようになります。

これらの変更により、deferreturnがスタックフレームを構築する際の引数コピーとPC修正の間に、ガベージコレクタが不整合なスタックをスキャンする可能性が排除され、ランタイムの堅牢性が向上しました。

関連リンク

  • Go言語のdeferステートメントに関する公式ドキュメントやチュートリアル
  • Goランタイムのスケジューラやガベージコレクタに関する技術記事や論文
  • Goのソースコードリポジトリ(特にsrc/runtimeディレクトリ)

参考にした情報源リンク