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

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

このコミットは、Goランタイムのスタック管理メカニズムであるnewstack関数に、呼び出し元のゴルーチンが正しいことを二重に確認するチェックを追加するものです。これにより、ランタイムの整合性が向上し、特にデバッグが困難な問題の特定に役立ちます。

コミット

  • コミットハッシュ: ebc5513be607165c4d676283d76b3284f103dbd8
  • Author: Russ Cox rsc@golang.org
  • Date: Fri Aug 2 13:51:28 2013 -0400
  • Subject: runtime: in newstack, double-check calling goroutine

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

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

元コミット内容

runtime: in newstack, double-check calling goroutine

Checking this condition helped me find the arm problem last night.

R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/12267043

変更の背景

この変更は、Goランタイムのnewstack関数が呼び出された際に、現在実行中のゴルーチン(m->curg)と、スタック拡張を要求したゴルーチン(m->morebuf.g)が一致していることを確認するためのものです。コミットメッセージには「Checking this condition helped me find the arm problem last night.」とあり、これはARMアーキテクチャ特有の、あるいはARM環境で顕在化したデバッグが困難な問題("arm problem")の特定に、このチェックが非常に有効であったことを示唆しています。

Goランタイムは、ゴルーチンのスタックが不足した場合に動的にスタックを拡張するメカニズムを持っています。この拡張処理はnewstack関数によって行われます。しかし、何らかの理由でnewstackが呼び出された際に、期待されるゴルーチンコンテキスト(つまり、スタック拡張を必要としているゴルーチン)と、実際に現在CPU上で実行されているゴルーチンが異なる場合、それはランタイムの深刻なバグを示唆します。このような状況は、競合状態、不正なスケジューリング、あるいは低レベルのコンテキストスイッチの問題など、非常にデバッグが困難な問題に起因する可能性があります。

このチェックを追加することで、不正なゴルーチンコンテキストでnewstackが呼び出された場合に即座にパニックを発生させ、問題の根本原因を特定しやすくすることが目的です。これにより、ランタイムの安定性とデバッグの容易性が向上します。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念と構造に関する知識が必要です。

  1. ゴルーチン (Goroutine): Goにおける軽量な実行スレッドです。OSのスレッドとは異なり、Goランタイムがスケジューリングと管理を行います。各ゴルーチンは独自のスタックを持ち、必要に応じて動的に拡張されます。

  2. スタック (Stack): 関数呼び出しの際に、ローカル変数、引数、リターンアドレスなどが格納されるメモリ領域です。Goのゴルーチンスタックは比較的小さく開始し、必要に応じて自動的に拡張(または縮小)されます。

  3. newstack 関数: Goランタイムの内部関数で、ゴルーチンのスタックが不足した際に呼び出され、スタックを拡張する役割を担います。関数呼び出し時にスタックの残りが少ないと判断された場合、コンパイラが生成するプロローグコードによってmorestack関数が呼び出され、それが最終的にnewstackを呼び出します。

  4. M (Machine) と G (Goroutine) 構造体: Goランタイムのスケジューラは、M(Machine、OSスレッドを表す)とG(Goroutine)という2つの主要な構造体を中心に動作します。

    • M (Machine): OSのスレッドを表します。CPU上でコードを実行する実体です。各Mは、現在実行中のゴルーチン(curg)、スケジューラ自身のゴルーチン(g0)、シグナルハンドラ用のゴルーチン(gsignal)などのポインタを持ちます。
    • G (Goroutine): ゴルーチンを表します。スタック情報、状態、実行コンテキストなどが含まれます。
  5. m->curg: M構造体(現在のOSスレッド)が現在実行しているゴルーチン(G構造体)へのポインタです。通常、CPU上で実行されているのはこのcurgが指すゴルーチンです。

  6. m->morebuf.g: morebufM構造体の一部で、スタック拡張が必要になった際に、どのゴルーチンがスタック拡張を要求したかを示す情報(特にそのゴルーチンへのポインタ)を一時的に保持するバッファです。newstackが呼び出される直前、スタック拡張を要求したゴルーチンがこのm->morebuf.gに設定されます。

  7. runtime·throw: Goランタイム内部で致命的なエラーが発生した場合に呼び出される関数です。通常、エラーメッセージを出力してプログラムを終了させます。これは、回復不可能なランタイムの整合性問題を示すために使用されます。

技術的詳細

このコミットで追加されたチェックは、src/pkg/runtime/stack.c内のruntime·newstack関数の冒頭に挿入されています。

newstack関数は、Goプログラムが実行中にゴルーチンのスタックが不足した際に、ランタイムによって自動的に呼び出されます。この関数が呼び出される際には、スタック拡張を要求したゴルーチンがm->morebuf.gに設定されていることが前提となります。そして、このnewstack関数自体は、スタック拡張を要求したゴルーチン(つまりm->morebuf.gが指すゴルーチン)のコンテキストで実行されるべきです。

追加されたコードは以下の条件をチェックします。

if(m->morebuf.g != m->curg)

この条件は、「スタック拡張を要求したゴルーチン(m->morebuf.g)が、現在CPU上で実行されているゴルーチン(m->curg)と異なる場合」を意味します。

通常、newstackが呼び出される状況では、m->morebuf.gm->curgは同じゴルーチンを指しているはずです。なぜなら、m->curgがスタック不足に陥り、その結果としてnewstackが呼び出され、そのm->curg自身がm->morebuf.gとして設定されるからです。

もしこの条件が真となる場合、それはGoランタイムの内部状態に深刻な不整合があることを示します。例えば、以下のような状況が考えられます。

  • 不正なコンテキストスイッチ: スケジューラが誤って別のゴルーチンにコンテキストを切り替えてしまったにもかかわらず、newstackが以前のゴルーチンのコンテキストで呼び出されてしまった。
  • morebuf.gの不正な設定: newstackを呼び出す前の段階で、m->morebuf.gに誤ったゴルーチンが設定されてしまった。
  • 競合状態: 複数のゴルーチンが同時にスタック拡張を試み、ランタイムの内部状態が一時的に矛盾した。

このような不整合は、メモリ破壊、不正なプログラム動作、あるいはデッドロックなど、予測不能な結果を引き起こす可能性があります。このチェックは、そのような潜在的な問題を早期に検出し、runtime·throwによってプログラムを即座に終了させることで、デバッグを容易にし、より深刻な問題の発生を防ぎます。

コミットメッセージで言及されている「arm problem」は、おそらくARMアーキテクチャ特有の低レベルなレジスタ管理やコンテキストスイッチのバグ、あるいは特定の最適化が原因で、この種のゴルーチンコンテキストの不整合が顕在化したケースであったと推測されます。このチェックは、そのようなデバッグが困難な問題を特定するための強力な診断ツールとして機能しました。

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

変更はsrc/pkg/runtime/stack.cファイルにあります。

--- a/src/pkg/runtime/stack.c
+++ b/src/pkg/runtime/stack.c
@@ -201,6 +201,13 @@ runtime·newstack(void)\n 	bool reflectcall;\n 	uintptr free;\n \n+	if(m->morebuf.g != m->curg) {\n+		runtime·printf("runtime: newstack called from g=%p\\n"\n+			"\tm=%p m->curg=%p m->g0=%p m->gsignal=%p\\n",\n+			m->morebuf.g, m, m->curg, m->g0, m->gsignal);\n+		runtime·throw("runtime: wrong goroutine in newstack");\n+	}\n+\n 	// gp->status is usually Grunning, but it could be Gsyscall if a stack split\n 	// happens during a function call inside entersyscall.\n \tgp = m->curg;\

コアとなるコードの解説

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

	if(m->morebuf.g != m->curg) {
		runtime·printf("runtime: newstack called from g=%p\\n"
			"\tm=%p m->curg=%p m->g0=%p m->gsignal=%p\\n",
			m->morebuf.g, m, m->curg, m->g0, m->gsignal);
		runtime·throw("runtime: wrong goroutine in newstack");
	}
  1. if(m->morebuf.g != m->curg): この行が主要なチェックです。m->morebuf.gはスタック拡張を要求したゴルーチンへのポインタであり、m->curgは現在CPU上で実行されているゴルーチンへのポインタです。これらが異なる場合、ランタイムの内部状態に矛盾があることを意味します。

  2. runtime·printf(...): 上記の条件が真(つまり、不整合が検出された)の場合に実行されます。これは、デバッグ情報を標準エラー出力にプリントするためのGoランタイム内部の関数です。出力される情報は以下の通りです。

    • runtime: newstack called from g=%p: newstackがどのゴルーチン(m->morebuf.g)から呼び出されたか。
    • m=%p: 現在のM(OSスレッド)構造体のアドレス。
    • m->curg=%p: 現在のMが実行しているゴルーチン(m->curg)のアドレス。
    • m->g0=%p: スケジューラ自身のゴルーチン(g0)のアドレス。
    • m->gsignal=%p: シグナルハンドラ用のゴルーチン(gsignal)のアドレス。 これらの情報は、問題発生時のランタイムのコンテキストを詳細に把握するために非常に役立ちます。
  3. runtime·throw("runtime: wrong goroutine in newstack");: デバッグ情報の出力後、この関数が呼び出されます。runtime·throwはGoランタイムの致命的なエラーハンドラであり、指定されたメッセージと共にプログラムを即座に終了させます。これにより、不正な状態での実行が継続されることを防ぎ、問題の根本原因を特定するためのスタックトレースなどの情報を提供します。

このコードの追加により、Goランタイムはスタック拡張時のゴルーチンコンテキストの整合性をより厳密にチェックするようになり、特に低レベルなランタイムバグの検出とデバッグ能力が向上しました。

関連リンク

参考にした情報源リンク

  • Goのコミット履歴: https://github.com/golang/go/commits/master
  • Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されているhttps://golang.org/cl/12267043はGerritのチェンジリストへのリンクです)
  • Goランタイムの内部構造に関するブログ記事や解説(例: "Go's work-stealing scheduler" など)
  • Goのソースコード(src/runtime/stack.c, src/runtime/m.h, src/runtime/runtime.hなど)
  • Goのmorestacknewstackに関する技術解説記事 (例: "Go: The Good, Bad and Ugly Parts of the Runtime" など)
  • Goのmg構造体に関する解説記事