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

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

このコミットは、Goランタイムのトレースバック出力に、ゴルーチンが「誰によって作成されたか」という情報(created by)を追加する変更です。これにより、特に実行中のゴルーチン(running状態のゴルーチン)のデバッグ時において、そのゴルーチンの起源を特定しやすくなります。

コミット

commit c33d49002069e798b33d0de42d3eb5073aef7c0b
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Aug 1 19:28:38 2013 +0400

    runtime: print "created by" for running goroutines in traceback
    This allows to at least determine goroutine "identity".
    Now it looks like:
    goroutine 12 [running]:
            goroutine running on other thread; stack unavailable
    created by testing.RunTests
            src/pkg/testing/testing.go:440 +0x88e
    
    R=golang-dev, r, rsc
    CC=golang-dev
    https://golang.org/cl/12248043

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

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

元コミット内容

このコミットは、Goランタイムのトレースバック機能に、実行中のゴルーチン(running状態)の「作成元」(created by)情報を表示する機能を追加します。これにより、スタックトレースが利用できない場合でも、ゴルーチンの「識別情報」を少なくとも特定できるようになります。

変更後の出力例は以下のようになります。

goroutine 12 [running]:
        goroutine running on other thread; stack unavailable
created by testing.RunTests
        src/pkg/testing/testing.go:440 +0x88e

この変更は、golang-devメーリングリスト、rrscによってレビューされ、golang.org/cl/12248043で議論されました。

変更されたファイルは以下の通りです。

  • src/pkg/runtime/proc.c
  • src/pkg/runtime/runtime.h
  • src/pkg/runtime/traceback_arm.c
  • src/pkg/runtime/traceback_x86.c

合計で4つのファイルが変更され、19行が追加、15行が削除されました。

変更の背景

Go言語のプログラムがクラッシュしたり、デッドロックに陥ったり、あるいは単にデバッグ目的でスタックトレースが出力される際、各ゴルーチンの状態とスタック情報が提供されます。しかし、running状態のゴルーチン、特に別のOSスレッドで実行中のゴルーチンについては、そのスタック情報が常に利用できるとは限りませんでした。このような場合、トレースバックは「goroutine running on other thread; stack unavailable」といったメッセージを表示するだけで、そのゴルーチンが何のために存在し、どこで生成されたのかを特定するのが困難でした。

このコミットの背景には、このような「スタックが利用できない」ゴルーチンのデバッグを容易にするという明確な目的があります。ゴルーチンの「作成元」情報を提供することで、開発者は問題のあるゴルーチンがプログラムのどの部分で起動されたのかを迅速に把握できるようになり、デバッグの効率が大幅に向上します。特に、テストフレームワーク(例: testing.RunTests)など、多くのゴルーチンを内部的に生成するようなシステムでは、この情報は非常に有用です。

前提知識の解説

このコミットを理解するためには、以下のGo言語のランタイムとデバッグに関する基本的な知識が必要です。

  1. ゴルーチン (Goroutine): Go言語における軽量な実行スレッドです。OSのスレッドとは異なり、Goランタイムによって管理され、非常に低コストで生成・管理できます。goキーワードを使って関数呼び出しの前に記述することで、新しいゴルーチンが起動されます。

  2. スタックトレース (Stack Trace) / トレースバック (Traceback): プログラムがクラッシュしたり、特定のデバッグポイントに到達したりした際に、現在実行中の関数の呼び出し履歴(スタック)を表示するものです。これにより、プログラムの実行パスを遡り、問題の原因を特定するのに役立ちます。Go言語では、パニック発生時やruntime.Stack()関数呼び出し時などにトレースバックが出力されます。

  3. ゴルーチンの状態 (Goroutine States): Goランタイムは、ゴルーチンを様々な状態(例: running, runnable, syscall, waitingなど)で管理します。

    • running: 現在OSスレッド上で実行中のゴルーチン。
    • runnable: 実行可能だが、現在OSスレッドが割り当てられていないゴルーチン。
    • syscall: システムコールを実行中のゴルーチン。
    • waiting: チャネル操作、ロック、タイマーなどで待機中のゴルーチン。 running状態のゴルーチンは、別のOSスレッドで実行されている場合、そのスタック情報がランタイムから直接アクセスできないことがあります。
  4. proc.c, runtime.h, traceback_arm.c, traceback_x86.c: これらはGoランタイムのC言語で書かれた部分のソースファイルです。

    • proc.c: プロセス管理、スケジューラ、ゴルーチン管理など、ランタイムのコアな部分を扱います。
    • runtime.h: ランタイムのデータ構造や関数プロトタイプが定義されています。
    • traceback_arm.c, traceback_x86.c: それぞれARMアーキテクチャとx86アーキテクチャ向けのスタックトレース生成ロジックが含まれています。アーキテクチャ固有のレジスタ操作やスタックフレームの解析を行うため、異なるファイルに分かれています。
  5. G構造体: Goランタイム内部でゴルーチンを表すデータ構造です。各ゴルーチンには一意のID(goid)が割り当てられ、その状態、スタックポインタ、プログラムカウンタなどの情報が格納されています。特に、gp->gopcはゴルーチンが作成された時点のプログラムカウンタ(PC)を指します。

技術的詳細

このコミットの技術的詳細を掘り下げると、主に以下の点が挙げられます。

  1. runtime·printcreatedby(G* gp) 関数の導入と公開: 以前はstatic void printcreatedby(G *gp)として定義されていた関数が、void runtime·printcreatedby(G *gp)として公開され、runtime.hにプロトタイプが追加されました。これにより、ランタイムの他の部分からこの関数を呼び出して、ゴルーチンの作成元情報を出力できるようになりました。 この関数は、引数としてゴルーチン構造体G* gpを受け取ります。

  2. gp->gopc の利用: runtime·printcreatedby関数は、ゴルーチン構造体gpgopcフィールドを利用します。gopcは、そのゴルーチンがgoキーワードによって起動された時点のプログラムカウンタ(PC)を保持しています。このPC値を使ってruntime·findfunc(pc)により対応する関数情報(Func構造体)を取得し、runtime·funcname(f)で関数名を取得します。

  3. スタックフレームの表示条件 runtime·showframe(f, gp): runtime·printcreatedby関数内で、runtime·showframe(f, gp)という条件が追加されました。これは、特定のフレーム(この場合はゴルーチン作成元のフレーム)を表示すべきかどうかを判断するためのものです。これにより、不要な内部フレームの表示を抑制し、より意味のある情報のみを出力するようになります。

  4. メインゴルーチン (goid 1) の除外: runtime·printcreatedby関数は、gp->goid != 1という条件を含んでいます。これは、メインゴルーチン(IDが1のゴルーチン)については「created by」情報を表示しないことを意味します。メインゴルーチンはプログラムのエントリポイントであり、通常は他のゴルーチンによって作成されるものではないため、この情報は不要と判断されたと考えられます。

  5. PCQuantum の導入: traceback_arm.ctraceback_x86.cruntime·printcreatedby関数内で、tracepc -= sizeof(uintptr);またはtracepc--;となっていた部分が、tracepc -= PCQuantum;に変更されました。 PCQuantumは、Goの命令ポインタ(PC)の増分単位を表すランタイム定数です。これはアーキテクチャによって異なり、例えばx86では1バイト、ARMでは4バイトなどとなります。この変更は、funcline関数に渡すPC値を正確に調整し、正しいソースコードの行番号を取得するために重要です。CALL命令の直前のPCを指すように調整することで、funclineが正確な行情報を返すことを保証します。

  6. runtime·tracebackothers での呼び出し: src/pkg/runtime/proc.cruntime·tracebackothers関数内で、Grunning状態のゴルーチンに対して、スタックが利用できない場合にruntime·printcreatedby(gp)が呼び出されるようになりました。 以前はGrunning状態のゴルーチンに対しては「goroutine running on other thread; stack unavailable」というメッセージのみが表示されていましたが、この変更により、そのメッセージの後に「created by ...」の情報が追加されるようになりました。

これらの変更により、Goランタイムは、デバッグ時にゴルーチンの起源に関するより豊富な情報を提供できるようになり、特にスタックが利用できない状況でのデバッグ能力が向上しました。

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

このコミットにおけるコアとなるコードの変更箇所は以下の通りです。

  1. src/pkg/runtime/proc.c: runtime·tracebackothers関数内で、Grunning状態のゴルーチンに対する処理が変更されました。

    --- a/src/pkg/runtime/proc.c
    +++ b/src/pkg/runtime/proc.c
    @@ -267,9 +267,10 @@ runtime·tracebackothers(G *me)
     			continue;
     		runtime·printf("\n");
     		runtime·goroutineheader(gp);
    -		if(gp->status == Grunning)
    +		if(gp->status == Grunning) {
     			runtime·printf("\tgoroutine running on other thread; stack unavailable\n");
    -		else
    +			runtime·printcreatedby(gp);
    +		} else
     			runtime·traceback(gp->sched.pc, gp->sched.sp, gp->sched.lr, gp);
     	}
     }
    

    Grunning状態のゴルーチンに対して、runtime·printcreatedby(gp)が呼び出されるようになりました。

  2. src/pkg/runtime/runtime.h: runtime·printcreatedby関数のプロトタイプが追加され、外部から呼び出し可能になりました。

    --- a/src/pkg/runtime/runtime.h
    +++ b/src/pkg/runtime/runtime.h
    @@ -1039,6 +1039,7 @@ Hchan*\truntime·makechan_c(ChanType*, int64);
     void\truntime·chansend(ChanType*, Hchan*, byte*, bool*, void*);\n
     void\truntime·chanrecv(ChanType*, Hchan*, byte*, bool*, bool*);\n
     bool\truntime·showframe(Func*, G*);\n
    +void\truntime·printcreatedby(G*);\n
     \n
     void\truntime·ifaceE2I(InterfaceType*, Eface, Iface*);\n
    
  3. src/pkg/runtime/traceback_arm.c および src/pkg/runtime/traceback_x86.c: printcreatedby関数がstaticからvoid runtime·printcreatedby(G *gp)に変更され、runtime·showframe(f, gp)の条件が追加され、PCQuantumが使用されるようになりました。

    src/pkg/runtime/traceback_arm.c の変更例:

    --- a/src/pkg/runtime/traceback_arm.c
    +++ b/src/pkg/runtime/traceback_arm.c
    @@ -193,20 +193,21 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,\n     	return n;\t\t\n     }\n     \n    -static void\n    -printcreatedby(G *gp)\n    +void\n    +runtime·printcreatedby(G *gp)\n     {\n     	int32 line;\n     	uintptr pc, tracepc;\n     	Func *f;\n     	String file;\n     \n    -\tif((pc = gp->gopc) != 0 && (f = runtime·findfunc(pc)) != nil\n    -\t\t&& runtime·showframe(f, gp) && gp->goid != 1) {\n    +\t// Show what created goroutine, except main goroutine (goid 1).\n    +\tif((pc = gp->gopc) != 0 && (f = runtime·findfunc(pc)) != nil &&\n    +\t\truntime·showframe(f, gp) && gp->goid != 1) {\n     		runtime·printf("created by %s\n", runtime·funcname(f));\n     		tracepc = pc;\t// back up to CALL instruction for funcline.\n     		if(pc > f->entry)\n    -\t\t\ttracepc -= sizeof(uintptr);\n    +\t\t\ttracepc -= PCQuantum;\n     		line = runtime·funcline(f, tracepc, &file);\n     		runtime·printf("\t%S:%d", file, line);\n     		if(pc > f->entry)\n    @@ -229,7 +230,7 @@ runtime·traceback(uintptr pc, uintptr sp, uintptr lr, G *gp)\n     	// If that means we print nothing at all, repeat forcing all frames printed.\n     	if(runtime·gentraceback(pc, sp, lr, gp, 0, nil, 100, nil, nil, false) == 0)\n     		runtime·gentraceback(pc, sp, lr, gp, 0, nil, 100, nil, nil, true);\n    -\tprintcreatedby(gp);\n    +\truntime·printcreatedby(gp);\n     }\n     ```
    `runtime·traceback`関数からも`runtime·printcreatedby(gp)`が呼び出されるようになりました。
    
    

コアとなるコードの解説

このコミットの核となる変更は、Goランタイムがゴルーチンのトレースバックを生成する際に、そのゴルーチンがどこで作成されたかという情報を追加するロジックです。

  1. runtime·tracebackothers の役割: この関数は、現在実行中のゴルーチン(me)以外のすべてのゴルーチンについてトレースバックを生成する役割を担っています。デバッグ時やパニック発生時に、システム内のすべてのゴルーチンの状態を把握するために呼び出されます。

  2. Grunning 状態の特殊性: if(gp->status == Grunning) の条件は、対象のゴルーチンが現在OSスレッド上で実行中であることを示します。このようなゴルーチンは、そのスタックが別のOSスレッドに存在するため、Goランタイムが直接スタックを読み取ることが困難な場合があります。そのため、以前は「goroutine running on other thread; stack unavailable」という一般的なメッセージしか表示されませんでした。

  3. runtime·printcreatedby(gp) の呼び出し: このコミットの主要な変更は、Grunning状態のゴルーチンに対して、スタックが利用できない場合でもruntime·printcreatedby(gp)を呼び出すようにした点です。これにより、スタックトレース自体は得られなくても、ゴルーチンの作成元という重要なコンテキスト情報が得られるようになりました。

  4. runtime·printcreatedby の内部動作:

    • gp->gopc を使用して、ゴルーチンが作成された時点のプログラムカウンタ(PC)を取得します。
    • runtime·findfunc(pc) でそのPCに対応する関数情報(Func構造体)を取得します。
    • runtime·showframe(f, gp) で、このフレームを表示すべきかどうかを判断します。これは、ランタイム内部のヘルパー関数などをフィルタリングし、ユーザーにとって意味のある情報のみを表示するためのものです。
    • gp->goid != 1 で、メインゴルーチン(ID 1)を除外します。
    • runtime·printf("created by %s\n", runtime·funcname(f)); で、作成元の関数名を出力します。
    • tracepc -= PCQuantum; でPCを調整し、runtime·funcline(f, tracepc, &file); で正確なファイル名と行番号を取得します。PCQuantumは、Goの命令ポインタの増分単位であり、アーキテクチャに依存するオフセットを正確に計算するために重要です。
    • 最後に、ファイル名、行番号、および関数エントリからのオフセット(+0x...)を出力します。

この一連の変更により、Goランタイムのデバッグ出力がより詳細になり、特にデッドロックやハングアップなどの問題でスタックが利用できないrunning状態のゴルーチンを追跡する際に、その起源を特定する手助けとなります。

関連リンク

  • Go言語のゴルーチンに関する公式ドキュメントやチュートリアル
  • Goランタイムのソースコード(特にsrc/runtimeディレクトリ)
  • Go言語のデバッグツール(delveなど)のドキュメント
  • Goのトレースバックに関する議論やブログ記事

参考にした情報源リンク