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

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

このコミットは、Go言語のランタイムにおける src/pkg/runtime/proc.c ファイルに対して行われたものです。proc.c はGoランタイムの非常に重要な部分であり、主にゴルーチン(goroutine)のスケジューリング、スタック管理、メモリ割り当て、パニック(panic)とリカバリー(recover)の処理、そしてCgoとの連携など、Goプログラムの実行を支える低レベルな機能が実装されています。このファイルは、Goの並行処理モデルと効率的なリソース管理の根幹をなす部分です。

コミット

  • コミットハッシュ: 4ac425fcddd7e3a923fe59f2375a2a75fa18ed33
  • 作者: Ian Lance Taylor iant@golang.org
  • コミット日時: 2011年11月8日 火曜日 18:16:25 -0800
  • 変更ファイル: src/pkg/runtime/proc.c
  • 変更概要: 56行の追加、12行の削除

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

https://github.com/golang/go/commit/4ac425fcddd7e3a923fe59f2375a2a75fa18ed33

元コミット内容

runtime: add comments for various functions in proc.c

R=rsc
CC=golang-dev
https://golang.org/cl/5357047

変更の背景

このコミットの主な目的は、src/pkg/runtime/proc.c 内の様々な関数にコメントを追加することです。Go言語のランタイムは非常に複雑で、低レベルな操作が多いため、コードの可読性と理解を深めることが重要です。特に、スケジューリング、スタック管理、エラーハンドリングといったGoのコア機能に関わる部分は、その動作原理を正確に把握することが開発者にとって不可欠です。

コメントの追加は、以下の点で重要です。

  1. 可読性の向上: 複雑なロジックや最適化が施された関数について、その目的、引数、戻り値、副作用などを明確にすることで、コードを読み解く労力を軽減します。
  2. メンテナンス性の向上: 将来の機能追加やバグ修正の際に、既存のコードの意図を素早く理解できるようになり、誤った変更を防ぎます。
  3. 新規開発者のオンボーディング: Goランタイムに初めて触れる開発者が、コードベースの構造と各コンポーネントの役割を効率的に学習できるようになります。
  4. デバッグの支援: 特定の関数の動作が不明瞭な場合、コメントがデバッグのヒントとなり、問題の特定と解決を早めます。

このコミットは、Goランタイムの内部構造をよりアクセスしやすく、理解しやすいものにするための継続的な努力の一環と言えます。

前提知識の解説

このコミットの変更内容を深く理解するためには、以下のGoランタイムに関する基本的な概念を把握しておく必要があります。

Goランタイム (Go Runtime)

Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクション(GC)、ゴルーチン(goroutine)のスケジューリング、チャネル(channel)の操作、メモリ管理、スタック管理、パニックとリカバリーの処理、システムコールとの連携などが含まれます。Goプログラムは、OSのプロセス上で直接実行されるのではなく、Goランタイムという抽象化レイヤーの上で動作します。

Goスケジューラ (Go Scheduler)

Goのスケジューラは、Goの並行処理モデルの核心です。OSのスレッド(M: Machine)上で、多数のゴルーチン(G: Goroutine)を効率的に実行するために、論理プロセッサ(P: Processor)という概念を導入しています。

  • G (Goroutine): Goにおける軽量な実行単位。数KB程度のスタックを持ち、数百万個作成することも可能です。
  • M (Machine): OSのスレッドに対応します。Goランタイムは、OSスレッドをMとして抽象化し、その上でGを実行します。
  • P (Processor): 論理プロセッサ。MとGの間に位置し、MがGを実行するためのコンテキストを提供します。Pは実行可能なGのキューを保持し、MはPからGを取得して実行します。GOMAXPROCS 環境変数によってPの数を制御できます。

proc.c は、このスケジューラの主要なロジック、特にMとGの管理、Gの生成と破棄、Gの実行状態の遷移などを担当しています。

スタック管理 (Stack Management)

Goのゴルーチンは、可変サイズのスタックを持ちます。初期スタックサイズは小さく(通常は数KB)、必要に応じて自動的に拡張されます。このスタックの拡張・縮小は、Goランタイムによって透過的に行われます。

  • スタックの成長 (Stack Growth): 関数呼び出しによってスタックが不足しそうになると、ランタイムはより大きな新しいスタックセグメントを割り当て、古いスタックの内容を新しいスタックにコピーします。このプロセスは「スタックスプリット(stack split)」と呼ばれます。
  • morestack / lessstack: スタックの成長と縮小を処理するためのランタイム関数です。コンパイラは、関数プロローグにスタックチェックコードを挿入し、スタックが不足しそうな場合に morestack を呼び出すようにします。
  • runtime·newstack / runtime·oldstack: morestackreflect·call などから呼ばれ、新しいスタックセグメントの割り当てや、古いスタックセグメントへの復帰を処理します。

go ステートメントと defer ステートメントの内部動作

  • go ステートメント: go キーワードに続く関数呼び出しは、新しいゴルーチンを生成し、そのゴルーチン内で関数を実行します。ランタイム内部では、runtime·newprocruntime·newproc1 といった関数が呼ばれ、新しいゴルーチン構造体(G)が割り当てられ、実行キューに追加されます。
  • defer ステートメント: defer キーワードに続く関数呼び出しは、現在の関数の実行が終了する直前(returnする前、またはpanicが発生する前)に実行されるようにスケジュールされます。ランタイム内部では、runtime·deferproc が呼ばれ、遅延実行される関数とその引数が現在のゴルーチンの遅延実行リストに登録されます。関数が終了する際には runtime·deferreturn が呼ばれ、登録された遅延関数が実行されます。

panicrecover の内部動作

  • panic: Goにおける実行時エラーのメカニズムです。panic が発生すると、現在のゴルーチンの実行は中断され、遅延関数が逆順に実行されながらスタックがアンワインド(unwind)されます。
  • recover: panic から回復するための組み込み関数です。recoverdefer 関数内でのみ有効で、panic が発生している場合にその値を捕捉し、パニックによるスタックアンワインドを停止させ、通常の実行フローに戻します。
  • proc.c には、runtime·panicrecoveryruntime·recover といった関数が実装されており、これらのメカニズムを支えています。

textflag 7 の意味 (no split)

Goのコンパイラは、関数がスタックを分割(成長)する必要があるかどうかを判断し、必要に応じて morestack への呼び出しを挿入します。しかし、一部のランタイム関数、特にスタック管理やゴルーチン生成に関わる関数は、スタックの分割中に呼び出されると問題を引き起こす可能性があります。

#pragma textflag 7 は、Goのコンパイラに対する指示で、その関数がスタックを分割しない(no split)ことを意味します。これは、関数が非常に短い場合や、スタックポインタやフレームポインタに直接アクセスするような低レベルな操作を行う場合に用いられます。スタック分割中にこれらの関数が実行されると、スタックの状態が不安定になり、予期せぬ動作を引き起こす可能性があるため、明示的にスタック分割を抑制します。

このコミットでは、runtime·newprocruntime·deferprocruntime·deferreturnruntime·recover といった関数にこの textflag 7 が適用されており、追加されたコメントでその理由が説明されています。これは、これらの関数が引数にアクセスする方法や、スタックの状態に依存する性質があるためです。

技術的詳細

このコミットでコメントが追加された主な関数とその技術的詳細は以下の通りです。

  • matchmg:

    • 役割: 必要に応じて新しいM(OSスレッド)を起動し、実行可能なゴルーチンを探させるための関数です。mcpumax(最大M数)までMを起動します。
    • 詳細: スケジューラがロックされた状態で呼び出されます。既存のMがゴルーチンを探している場合でも、必要に応じて新しいMを起動し、システムのリソースを最大限に活用してゴルーチンの実行を促進します。
  • startm:

    • 役割: 新しいM(OSスレッド)を作成し、そのMが runtime·mstart 関数から実行を開始するように設定します。
    • 詳細: runtime·mstart は、新しく起動されたMが最初に実行するランタイム関数であり、そのMの初期化やゴルーチンの取得・実行ループへの移行を担います。
  • runtime·oldstack:

    • 役割: 新しいスタックセグメントを割り当てた関数から戻る際に、古いスタックセグメントに戻るために呼び出されます。
    • 詳細: runtime·lessstack から呼ばれることを想定しています。関数の戻り値は m->cret に格納されており、この関数は gobuf を使って古いスタックフレームにジャンプし、実行を再開します。
  • runtime·newstack:

    • 役割: reflect·callruntime·morestack から、新しいスタックセグメントが必要な場合に呼び出されます。
    • 詳細: m->moreframesize バイト分の新しいスタックを割り当て、m->moreargsize バイトの引数を新しいフレームにコピーします。その後、m->morepc の関数が runtime·lessstack によって呼び出されたかのように動作します。これは、スタックの成長メカニズムの核心部分です。
  • mstackalloc:

    • 役割: runtime·malg から呼ばれるフックで、スケジューラスタック上で runtime·stackalloc を呼び出すためのものです。
    • 詳細: runtime·stackalloc は、新しいスタックセグメントを割り当てる際に、スタックの成長を試みないように、スケジューラスタック上で呼び出される必要があります。この関数は、その制約を満たすために存在します。
  • runtime·malg:

    • 役割: 新しいG(ゴルーチン)を割り当て、stacksize バイト分のスタックを確保します。
    • 詳細: 新しいゴルーチン構造体を初期化し、そのスタックを割り当てます。Goプログラムで新しいゴルーチンが起動される際に内部的に呼び出される重要な関数です。
  • runtime·newproc:

    • 役割: go ステートメントによって新しいGを生成し、fn 関数を siz バイトの引数で実行するように設定します。
    • 詳細: コンパイラは go ステートメントをこの関数への呼び出しに変換します。この関数は、引数が &fn の後に連続して利用可能であることを前提としているため、スタック分割が発生すると引数がコピーされない可能性があるため、#pragma textflag 7(スタック分割なし)が適用されています。
  • runtime·newproc1:

    • 役割: fn 関数を narg バイトの引数(argp から開始)で実行し、nret バイトの結果を返す新しいGを生成します。
    • 詳細: callerpc は、このGを作成した go ステートメントのアドレスです。新しいGは実行待ちのGのキューに追加されます。runtime·newproc から内部的に呼び出されます。
  • runtime·deferproc:

    • 役割: defer ステートメントによって遅延関数 fnsiz バイトの引数で登録します。
    • 詳細: コンパイラは defer ステートメントをこの関数への呼び出しに変換します。runtime·newproc と同様に、引数が &fn の後に連続して利用可能であることを前提としているため、#pragma textflag 7 が適用されています。
  • runtime·deferreturn:

    • 役割: 遅延関数が存在する場合にそれを実行します。
    • 詳細: コンパイラは、defer を呼び出す任意の関数の終わりにこの関数への呼び出しを挿入します。遅延関数が存在する場合、runtime·jmpdefer を呼び出して遅延関数にジャンプし、deferreturn が呼び出された直前のポイントで呼び出されたかのように見せかけます。これにより、遅延関数がなくなるまで deferreturn が繰り返し呼び出されます。呼び出し元のフレームを再利用して遅延関数を呼び出すため、#pragma textflag 7 が適用されています。
  • rundefer:

    • 役割: 現在のゴルーチンのすべての遅延関数を実行します。
    • 詳細: deferreturn が繰り返し呼び出されることで、この関数が最終的にすべての遅延関数を処理します。
  • printpanics:

    • 役割: 現在アクティブなすべてのパニック情報を出力します。
    • 詳細: プログラムがクラッシュする際に、デバッグ情報としてパニックスタックを出力するために使用されます。
  • runtime·panic:

    • 役割: 組み込み関数 panic の実装です。
    • 詳細: panic が発生した際に、ランタイムがどのようにスタックをアンワインドし、遅延関数を実行するかを制御します。
  • recovery:

    • 役割: パニック後に遅延関数が recover を呼び出した際に、スタックをアンワインドし、遅延関数の呼び出し元が正常に復帰したかのように実行を継続するように調整します。
    • 詳細: recover が成功した場合の実行フローを制御する重要な関数です。
  • runtime·recover:

    • 役割: 組み込み関数 recover の実装です。
    • 詳細: 信頼性高く呼び出し元のスタックセグメントを見つける必要があるため、#pragma textflag 7 が適用されています。
  • runtime·Gosched:

    • 役割: runtime.Gosched 関数の実装です。
    • 詳細: 現在のゴルーチンを一時停止し、他のゴルーチンにCPUを譲ります。スケジューラに制御を戻すために使用されます。
  • runtime·gomaxprocsfunc:

    • 役割: runtime.GOMAXPROCS 関数の実装です。
    • 詳細: 論理プロセッサ(P)の数を設定します。スケジューラの動作に直接影響を与えます。
  • runtime·sigprof:

    • 役割: SIGPROF シグナルを受信した場合に呼び出されます。
    • 詳細: CPUプロファイリングのために使用され、定期的にスタックトレースを収集します。
  • runtime·setcpuprofilerate:

    • 役割: fn 関数を hz 回/秒の頻度でトレースバックとともに呼び出すように設定します。
    • 詳細: CPUプロファイリングのレートを設定するための関数です。
  • os·setenv_c:

    • 役割: Cgoがロードされている場合にC環境を更新します。
    • 詳細: os.Setenv から呼び出され、Cライブラリの環境変数を設定する際に使用されます。

これらのコメントは、Goランタイムの内部動作、特にスタック管理、ゴルーチン生成、パニック/リカバリー、そしてスケジューリングの複雑な相互作用を理解する上で非常に貴重な情報を提供しています。

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

このコミットでは、src/pkg/runtime/proc.c ファイルに多数のコメントが追加され、一部の古いコメントが削除されています。以下に、主要な変更箇所を抜粋して示します。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -739,8 +739,6 @@ struct CgoThreadStart
 };
 
 // Kick off new m's as needed (up to mcpumax).
-// There are already `other' other cpus that will
-// start looking for goroutines shortly.
 // Sched is locked.
 static void
 matchmg(void)
@@ -763,6 +761,7 @@ matchmg(void)
 	}
 }
 
+// Create a new m.  It will start off with a call to runtime·mstart.
 static M*
 startm(void)
 {
@@ -995,6 +994,9 @@ runtime·exitsyscall(void)
 	g->gcstack = nil;
 }
 
+// Called from runtime·lessstack when returning from a function which
+// allocated a new stack segment.  The function's return value is in
+// m->cret.
 void
 runtime·oldstack(void)
 {
@@ -1026,6 +1031,11 @@ runtime·oldstack(void)
 	runtime·gogo(&old.gobuf, m->cret);
 }
 
+// Called from reflect·call or from runtime·morestack when a new
+// stack segment is needed.  Allocate a new stack big enough for
+// m->moreframesize bytes, copy m->moreargsize bytes to the new frame,
+// and then act as though runtime·lessstack called the function at
+// m->morepc.
 void
 runtime·newstack(void)
 {
@@ -1113,6 +1124,10 @@ runtime·newstack(void)
 	*(int32*)345 = 123;	// never return
 }
 
+// Hook used by runtime·malg to call runtime·stackalloc on the
+// scheduler stack.  This exists because runtime·stackalloc insists
+// on being called on the scheduler stack, to avoid trying to grow
+// the stack while allocating a new stack segment.
 static void
 mstackalloc(G *gp)
 {
@@ -1120,6 +1135,7 @@ mstackalloc(G *gp)
 	runtime·gogo(&gp->sched, 0);
 }
 
+// Allocate a new g, with a stack big enough for stacksize bytes.
 G*
 runtime·malg(int32 stacksize)
 {
@@ -1146,15 +1162,13 @@ runtime·malg(int32 stacksize)
 	return newg;
 }
 
-/*
- * Newproc and deferproc need to be textflag 7
- * (no possible stack split when nearing overflow)
- * because they assume that the arguments to fn
- * are available sequentially beginning at &arg0.
- * If a stack split happened, only the one word
- * arg0 would be copied.  It's okay if any functions
- * they call split the stack below the newproc frame.
- */
+// Create a new g running fn with siz bytes of arguments.
+// Put it on the queue of g's waiting to run.
+// The compiler turns a go statement into a call to this.
+// Cannot split the stack because it assumes that the arguments
+// are available sequentially after &fn; they would not be
+// copied if a stack split occurred.  It's OK for this to call
+// functions that split the stack.
 #pragma textflag 7
 void
 runtime·newproc(int32 siz, byte* fn, ...)
@@ -1168,6 +1182,10 @@ runtime·newproc(int32 siz, byte* fn, ...)\
 	runtime·newproc1(fn, argp, siz, 0, runtime·getcallerpc(&siz));
 }
 
+// Create a new g running fn with narg bytes of arguments starting
+// at argp and returning nret bytes of results.  callerpc is the
+// address of the go statement that created this.  The new g is put
+// on the queue of g's waiting to run.
 G*
 runtime·newproc1(byte *fn, byte *argp, int32 narg, int32 nret, void *callerpc)
 {
@@ -1228,6 +1246,12 @@ runtime·newproc1(byte *fn, byte *argp, int32 narg, int32 nret, void *callerpc)
 //printf(" goid=%d\n", newg->goid);\
 }
 
+// Create a new deferred function fn with siz bytes of arguments.
+// The compiler turns a defer statement into a call to this.
+// Cannot split the stack because it assumes that the arguments
+// are available sequentially after &fn; they would not be
+// copied if a stack split occurred.  It's OK for this to call
+// functions that split the stack.
 #pragma textflag 7
 uintptr
 runtime·deferproc(int32 siz, byte* fn, ...)
@@ -1256,6 +1280,16 @@ runtime·deferproc(int32 siz, byte* fn, ...)
 	return 0;
 }
 
+// Run a deferred function if there is one.
+// The compiler inserts a call to this at the end of any
+// function which calls defer.
+// If there is a deferred function, this will call runtime·jmpdefer,
+// which will jump to the deferred function such that it appears
+// to have been called by the caller of deferreturn at the point
+// just before deferreturn was called.  The effect is that deferreturn
+// is called again and again until there are no more deferred functions.
+// Cannot split the stack because we reuse the caller's frame to
+// call the deferred function.
 #pragma textflag 7
 void
 runtime·deferreturn(uintptr arg0)
@@ -1277,6 +1311,7 @@ runtime·deferreturn(uintptr arg0)
 	runtime·jmpdefer(fn, argp);
 }
 
+// Run all deferred functions for the current goroutine.
 static void
 rundefer(void)
 {
@@ -1318,6 +1353,7 @@ unwindstack(G *gp, byte *sp)
 	}
 }
 
+// Print all currently active panics.  Used when crashing.
 static void
 printpanics(Panic *p)
 {
@@ -1334,6 +1370,7 @@ printpanics(Panic *p)
 
 static void recovery(G*);
 
+// The implementation of the predeclared function panic.
 void
 runtime·panic(Eface e)
 {
@@ -1376,6 +1413,9 @@ runtime·panic(Eface e)
 	runtime·dopanic(0);
 }
 
+// Unwind the stack after a deferred function calls recover
+// after a panic.  Then arrange to continue running as though
+// the caller of the deferred function returned normally.
 static void
 recovery(G *gp)
 {
@@ -1407,7 +1447,10 @@ recovery(G *gp)
 	runtime·gogo(&gp->sched, 1);
 }
 
-#pragma textflag 7	/* no split, or else g->stackguard is not the stack for fp */
+// The implementation of the predeclared function recover.
+// Cannot split the stack because it needs to reliably
+// find the stack segment of its caller.
+#pragma textflag 7
 void
 runtime·recover(byte *argp, Eface ret)
 {
@@ -1519,6 +1562,7 @@ runtime·Gosched(void)
 	runtime·gosched();
 }
 
+// Implementation of runtime.GOMAXPROCS.
 // delete when scheduler is stronger
 int32
 runtime·gomaxprocsfunc(int32 n)
@@ -1634,6 +1678,7 @@ static struct {
 	uintptr pcbuf[100];
 } prof;
 
+// Called if we receive a SIGPROF signal.
 void
 runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp)
 {
@@ -1653,6 +1698,7 @@ runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp)
 	runtime·unlock(&prof);\
 }
 
+// Arrange to call fn with a traceback hz times a second.
 void
 runtime·setcpuprofilerate(void (*fn)(uintptr*, int32), int32 hz)
 {
@@ -1683,6 +1729,8 @@ runtime·setcpuprofilerate(void (*fn)(uintptr*, int32), int32 hz)
 
 void (*libcgo_setenv)(byte**);
 
+// Update the C environment if cgo is loaded.
+// Called from os.Setenv.
 void
 os·setenv_c(String k, String v)
 {

コアとなるコードの解説

上記の変更箇所は、Goランタイムの proc.c 内の様々な関数に、その役割、動作原理、および特定の制約(特にスタック分割に関する textflag 7 の理由)を説明するコメントを追加しています。

例えば、runtime·newproc の変更を見てみましょう。

変更前:

/*
 * Newproc and deferproc need to be textflag 7
 * (no possible stack split when nearing overflow)
 * because they assume that the arguments to fn
 * are available sequentially beginning at &arg0.
 * If a stack split happened, only the one word
 * arg0 would be copied.  It's okay if any functions
 * they call split the stack below the newproc frame.
 */
#pragma textflag 7
void
runtime·newproc(int32 siz, byte* fn, ...)

変更後:

// Create a new g running fn with siz bytes of arguments.
// Put it on the queue of g's waiting to run.
// The compiler turns a go statement into a call to this.
// Cannot split the stack because it assumes that the arguments
// are available sequentially after &fn; they would not be
// copied if a stack split occurred.  It's OK for this to call
// functions that split the stack.
#pragma textflag 7
void
runtime·newproc(int32 siz, byte* fn, ...)

この変更では、古いCスタイルのコメントブロックが、より現代的なC++スタイルの行コメントに置き換えられています。内容は基本的に同じですが、より簡潔で読みやすくなっています。

新しいコメントは、以下の点を明確にしています。

  • 関数の目的: 「fnsiz バイトの引数で実行する新しいGを作成する。」
  • スケジューリング: 「実行待ちのGのキューに入れる。」
  • コンパイラの役割: 「コンパイラは go ステートメントをこれへの呼び出しに変換する。」
  • スタック分割の制約: 「スタックを分割できない。なぜなら、引数が &fn の後に連続して利用可能であることを前提としており、スタック分割が発生した場合、それらはコピーされないからである。」
  • 許容される動作: 「この関数が呼び出す関数が newproc フレームの下でスタックを分割することは問題ない。」

特に「Cannot split the stack...」の部分は、#pragma textflag 7 がなぜ必要であるかという技術的な理由を詳細に説明しており、Goランタイムの低レベルなスタック管理の複雑さを浮き彫りにしています。引数がスタック上に連続して配置されていることを前提とする関数では、スタック分割によって引数の配置が変更されると、関数が正しく動作しなくなる可能性があるため、このような制約が設けられています。

他の関数についても同様に、その機能、呼び出し元、呼び出し先の関係、そしてスタック管理やスケジューリングに関する特定の制約が詳細にコメントされています。これにより、Goランタイムの内部構造がより透過的になり、開発者がその動作を深く理解するための手助けとなります。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (src/pkg/runtime/proc.c および関連ファイル)
  • Go言語の公式ドキュメント
  • Goランタイムに関する一般的な技術記事やブログポスト (具体的なURLは検索時に参照しましたが、特定の記事を直接引用したものではありません)

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

このコミットは、Go言語のランタイムにおける src/pkg/runtime/proc.c ファイルに対して行われたものです。proc.c はGoランタイムの非常に重要な部分であり、主にゴルーチン(goroutine)のスケジューリング、スタック管理、メモリ割り当て、パニック(panic)とリカバリー(recover)の処理、そしてCgoとの連携など、Goプログラムの実行を支える低レベルな機能が実装されています。このファイルは、Goの並行処理モデルと効率的なリソース管理の根幹をなす部分です。

コミット

  • コミットハッシュ: 4ac425fcddd7e3a923fe59f2375a2a75fa18ed33
  • 作者: Ian Lance Taylor iant@golang.org
  • コミット日時: 2011年11月8日 火曜日 18:16:25 -0800
  • 変更ファイル: src/pkg/runtime/proc.c
  • 変更概要: 56行の追加、12行の削除

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

https://github.com/golang/go/commit/4ac425fcddd7e3a923fe59f2375a2a75fa18ed33

元コミット内容

runtime: add comments for various functions in proc.c

R=rsc
CC=golang-dev
https://golang.org/cl/5357047

変更の背景

このコミットの主な目的は、src/pkg/runtime/proc.c 内の様々な関数にコメントを追加することです。Go言語のランタイムは非常に複雑で、低レベルな操作が多いため、コードの可読性と理解を深めることが重要です。特に、スケジューリング、スタック管理、エラーハンドリングといったGoのコア機能に関わる部分は、その動作原理を正確に把握することが開発者にとって不可欠です。

コメントの追加は、以下の点で重要です。

  1. 可読性の向上: 複雑なロジックや最適化が施された関数について、その目的、引数、戻り値、副作用などを明確にすることで、コードを読み解く労力を軽減します。
  2. メンテナンス性の向上: 将来の機能追加やバグ修正の際に、既存のコードの意図を素早く理解できるようになり、誤った変更を防ぎます。
  3. 新規開発者のオンボーディング: Goランタイムに初めて触れる開発者が、コードベースの構造と各コンポーネントの役割を効率的に学習できるようになります。
  4. デバッグの支援: 特定の関数の動作が不明瞭な場合、コメントがデバッグのヒントとなり、問題の特定と解決を早めます。

このコミットは、Goランタイムの内部構造をよりアクセスしやすく、理解しやすいものにするための継続的な努力の一環と言えます。

前提知識の解説

このコミットの変更内容を深く理解するためには、以下のGoランタイムに関する基本的な概念を把握しておく必要があります。

Goランタイム (Go Runtime)

Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクション(GC)、ゴルーチン(goroutine)のスケジューリング、チャネル(channel)の操作、メモリ管理、スタック管理、パニックとリカバリーの処理、システムコールとの連携などが含まれます。Goプログラムは、OSのプロセス上で直接実行されるのではなく、Goランタイムという抽象化レイヤーの上で動作します。

Goスケジューラ (Go Scheduler)

Goのスケジューラは、Goの並行処理モデルの核心です。OSのスレッド(M: Machine)上で、多数のゴルーチン(G: Goroutine)を効率的に実行するために、論理プロセッサ(P: Processor)という概念を導入しています。これはM-P-Gモデルとして知られています。

  • G (Goroutine): Goにおける軽量な実行単位。数KB程度のスタックを持ち、数百万個作成することも可能です。OSスレッドよりもはるかに軽量で、Goランタイムによって管理されます。
  • M (Machine/OS Thread): オペレーティングシステムのスレッドに対応します。Goランタイムは、OSスレッドをMとして抽象化し、その上でGを実行します。MはGoコードを実行するためにPと関連付けられている必要があります。
  • P (Processor/Logical Processor): 論理プロセッサ。MとGの間に位置し、MがGを実行するためのコンテキストを提供します。Pは実行可能なGのローカル実行キュー(LRQ)を保持し、MはPからGを取得して実行します。GOMAXPROCS 環境変数によってPの数を制御でき、通常は利用可能な論理CPUの数に設定されます。

proc.c は、このスケジューラの主要なロジック、特にMとGの管理、Gの生成と破棄、Gの実行状態の遷移などを担当しています。また、アイドル状態のPが他のPのローカル実行キューからゴルーチンを「盗む」ワークスティーリング(Work Stealing)アルゴリズムも実装されており、リソースの効率的な利用を保証します。

スタック管理 (Stack Management)

Goのゴルーチンは、可変サイズのスタックを持ちます。初期スタックサイズは小さく(通常は数KB)、必要に応じて自動的に拡張されます。このスタックの拡張・縮小は、Goランタイムによって透過的に行われます。

  • スタックの成長 (Stack Growth): 関数呼び出しによってスタックが不足しそうになると、ランタイムはより大きな新しいスタックセグメントを割り当て、古いスタックの内容を新しいスタックにコピーします。このプロセスは「スタックスプリット(stack split)」と呼ばれます。
  • morestack / lessstack: スタックの成長と縮小を処理するためのランタイム関数です。コンパイラは、関数プロローグにスタックチェックコードを挿入し、スタックが不足しそうな場合に morestack を呼び出すようにします。
  • runtime·newstack / runtime·oldstack: morestackreflect·call などから呼ばれ、新しいスタックセグメントの割り当てや、古いスタックセグメントへの復帰を処理します。

go ステートメントと defer ステートメントの内部動作

  • go ステートメント: go キーワードに続く関数呼び出しは、新しいゴルーチンを生成し、そのゴルーチン内で関数を実行します。ランタイム内部では、runtime·newprocruntime·newproc1 といった関数が呼ばれ、新しいゴルーチン構造体(G)が割り当てられ、実行キューに追加されます。
  • defer ステートメント: defer キーワードに続く関数呼び出しは、現在の関数の実行が終了する直前(returnする前、またはpanicが発生する前)に実行されるようにスケジュールされます。ランタイム内部では、runtime·deferproc が呼ばれ、遅延実行される関数とその引数が現在のゴルーチンの遅延実行リストに登録されます。関数が終了する際には runtime·deferreturn が呼ばれ、登録された遅延関数が実行されます。

panicrecover の内部動作

  • panic: Goにおける実行時エラーのメカニズムです。panic が発生すると、現在のゴルーチンの実行は中断され、遅延関数が逆順に実行されながらスタックがアンワインド(unwind)されます。
  • recover: panic から回復するための組み込み関数です。recoverdefer 関数内でのみ有効で、panic が発生している場合にその値を捕捉し、パニックによるスタックアンワインドを停止させ、通常の実行フローに戻します。
  • proc.c には、runtime·panicrecoveryruntime·recover といった関数が実装されており、これらのメカニズムを支えています。

textflag 7 の意味 (no split)

Goのコンパイラは、関数がスタックを分割(成長)する必要があるかどうかを判断し、必要に応じて morestack への呼び出しを挿入します。しかし、一部のランタイム関数、特にスタック管理やゴルーチン生成に関わる関数は、スタックの分割中に呼び出されると問題を引き起こす可能性があります。

#pragma textflag 7 は、Goのコンパイラに対する指示で、その関数がスタックを分割しない(no split)ことを意味します。これは、関数が非常に短い場合や、スタックポインタやフレームポインタに直接アクセスするような低レベルな操作を行う場合に用いられます。スタック分割中にこれらの関数が実行されると、スタックの状態が不安定になり、予期せぬ動作を引き起こす可能性があるため、明示的にスタック分割を抑制します。

このコミットでは、runtime·newprocruntime·deferprocruntime·deferreturnruntime·recover といった関数にこの textflag 7 が適用されており、追加されたコメントでその理由が説明されています。これは、これらの関数が引数にアクセスする方法や、スタックの状態に依存する性質があるためです。

技術的詳細

このコミットでコメントが追加された主な関数とその技術的詳細は以下の通りです。

  • matchmg:

    • 役割: 必要に応じて新しいM(OSスレッド)を起動し、実行可能なゴルーチンを探させるための関数です。mcpumax(最大M数)までMを起動します。
    • 詳細: スケジューラがロックされた状態で呼び出されます。既存のMがゴルーチンを探している場合でも、必要に応じて新しいMを起動し、システムのリソースを最大限に活用してゴルーチンの実行を促進します。
  • startm:

    • 役割: 新しいM(OSスレッド)を作成し、そのMが runtime·mstart 関数から実行を開始するように設定します。
    • 詳細: runtime·mstart は、新しく起動されたMが最初に実行するランタイム関数であり、そのMの初期化やゴルーチンの取得・実行ループへの移行を担います。
  • runtime·oldstack:

    • 役割: 新しいスタックセグメントを割り当てた関数から戻る際に、古いスタックセグメントに戻るために呼び出されます。
    • 詳細: runtime·lessstack から呼ばれることを想定しています。関数の戻り値は m->cret に格納されており、この関数は gobuf を使って古いスタックフレームにジャンプし、実行を再開します。
  • runtime·newstack:

    • 役割: reflect·callruntime·morestack から、新しいスタックセグメントが必要な場合に呼び出されます。
    • 詳細: m->moreframesize バイト分の新しいスタックを割り当て、m->moreargsize バイトの引数を新しいフレームにコピーします。その後、m->morepc の関数が runtime·lessstack によって呼び出されたかのように動作します。これは、スタックの成長メカニズムの核心部分です。
  • mstackalloc:

    • 役割: runtime·malg から呼ばれるフックで、スケジューラスタック上で runtime·stackalloc を呼び出すためのものです。
    • 詳細: runtime·stackalloc は、新しいスタックセグメントを割り当てる際に、スタックの成長を試みないように、スケジューラスタック上で呼び出される必要があります。この関数は、その制約を満たすために存在します。
  • runtime·malg:

    • 役割: 新しいG(ゴルーチン)を割り当て、stacksize バイト分のスタックを確保します。
    • 詳細: 新しいゴルーチン構造体を初期化し、そのスタックを割り当てます。Goプログラムで新しいゴルーチンが起動される際に内部的に呼び出される重要な関数です。
  • runtime·newproc:

    • 役割: go ステートメントによって新しいGを生成し、fn 関数を siz バイトの引数で実行するように設定します。
    • 詳細: コンパイラは go ステートメントをこの関数への呼び出しに変換します。この関数は、引数が &fn の後に連続して利用可能であることを前提としているため、スタック分割が発生すると引数がコピーされない可能性があるため、#pragma textflag 7(スタック分割なし)が適用されています。
  • runtime·newproc1:

    • 役割: fn 関数を narg バイトの引数(argp から開始)で実行し、nret バイトの結果を返す新しいGを生成します。
    • 詳細: callerpc は、このGを作成した go ステートメントのアドレスです。新しいGは実行待ちのGのキューに追加されます。runtime·newproc から内部的に呼び出されます。
  • runtime·deferproc:

    • 役割: defer ステートメントによって遅延関数 fnsiz バイトの引数で登録します。
    • 詳細: コンパイラは defer ステートメントをこの関数への呼び出しに変換します。runtime·newproc と同様に、引数が &fn の後に連続して利用可能であることを前提としているため、#pragma textflag 7 が適用されています。
  • runtime·deferreturn:

    • 役割: 遅延関数が存在する場合にそれを実行します。
    • 詳細: コンパイラは、defer を呼び出す任意の関数の終わりにこの関数への呼び出しを挿入します。遅延関数が存在する場合、runtime·jmpdefer を呼び出して遅延関数にジャンプし、deferreturn が呼び出された直前のポイントで呼び出されたかのように見せかけます。これにより、遅延関数がなくなるまで deferreturn が繰り返し呼び出されます。呼び出し元のフレームを再利用して遅延関数を呼び出すため、#pragma textflag 7 が適用されています。
  • rundefer:

    • 役割: 現在のゴルーチンのすべての遅延関数を実行します。
    • 詳細: deferreturn が繰り返し呼び出されることで、この関数が最終的にすべての遅延関数を処理します。
  • printpanics:

    • 役割: 現在アクティブなすべてのパニック情報を出力します。
    • 詳細: プログラムがクラッシュする際に、デバッグ情報としてパニックスタックを出力するために使用されます。
  • runtime·panic:

    • 役割: 組み込み関数 panic の実装です。
    • 詳細: panic が発生した際に、ランタイムがどのようにスタックをアンワインドし、遅延関数を実行するかを制御します。
  • recovery:

    • 役割: パニック後に遅延関数が recover を呼び出した際に、スタックをアンワインドし、遅延関数の呼び出し元が正常に復帰したかのように実行を継続するように調整します。
    • 詳細: recover が成功した場合の実行フローを制御する重要な関数です。
  • runtime·recover:

    • 役割: 組み込み関数 recover の実装です。
    • 詳細: 信頼性高く呼び出し元のスタックセグメントを見つける必要があるため、#pragma textflag 7 が適用されています。
  • runtime·Gosched:

    • 役割: runtime.Gosched 関数の実装です。
    • 詳細: 現在のゴルーチンを一時停止し、他のゴルーチンにCPUを譲ります。スケジューラに制御を戻すために使用されます。
  • runtime·gomaxprocsfunc:

    • 役割: runtime.GOMAXPROCS 関数の実装です。
    • 詳細: 論理プロセッサ(P)の数を設定します。スケジューラの動作に直接影響を与えます。
  • runtime·sigprof:

    • 役割: SIGPROF シグナルを受信した場合に呼び出されます。
    • 詳細: CPUプロファイリングのために使用され、定期的にスタックトレースを収集します。
  • runtime·setcpuprofilerate:

    • 役割: fn 関数を hz 回/秒の頻度でトレースバックとともに呼び出すように設定します。
    • 詳細: CPUプロファイリングのレートを設定するための関数です。
  • os·setenv_c:

    • 役割: Cgoがロードされている場合にC環境を更新します。
    • 詳細: os.Setenv から呼び出され、Cライブラリの環境変数を設定する際に使用されます。

これらのコメントは、Goランタイムの内部動作、特にスタック管理、ゴルーチン生成、パニック/リカバリー、そしてスケジューリングの複雑な相互作用を理解する上で非常に貴重な情報を提供しています。

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

このコミットでは、src/pkg/runtime/proc.c ファイルに多数のコメントが追加され、一部の古いコメントが削除されています。以下に、主要な変更箇所を抜粋して示します。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -739,8 +739,6 @@ struct CgoThreadStart
 };
 
 // Kick off new m's as needed (up to mcpumax).
-// There are already `other' other cpus that will
-// start looking for goroutines shortly.
 // Sched is locked.
 static void
 matchmg(void)
@@ -763,6 +761,7 @@ matchmg(void)
 	}
 }
 
+// Create a new m.  It will start off with a call to runtime·mstart.
 static M*
 startm(void)
 {
@@ -995,6 +994,9 @@ runtime·exitsyscall(void)
 	g->gcstack = nil;
 }
 
+// Called from runtime·lessstack when returning from a function which
+// allocated a new stack segment.  The function's return value is in
+// m->cret.
 void
 runtime·oldstack(void)
 {
@@ -1026,6 +1031,11 @@ runtime·oldstack(void)
 	runtime·gogo(&old.gobuf, m->cret);
 }
 
+// Called from reflect·call or from runtime·morestack when a new
+// stack segment is needed.  Allocate a new stack big enough for
+// m->moreframesize bytes, copy m->moreargsize bytes to the new frame,
+// and then act as though runtime·lessstack called the function at
+// m->morepc.
 void
 runtime·newstack(void)
 {
@@ -1113,6 +1124,10 @@ runtime·newstack(void)
 	*(int32*)345 = 123;	// never return
 }
 
+// Hook used by runtime·malg to call runtime·stackalloc on the
+// scheduler stack.  This exists because runtime·stackalloc insists
+// on being called on the scheduler stack, to avoid trying to grow
+// the stack while allocating a new stack segment.
 static void
 mstackalloc(G *gp)
 {
@@ -1120,6 +1135,7 @@ mstackalloc(G *gp)
 	runtime·gogo(&gp->sched, 0);
 }
 
+// Allocate a new g, with a stack big enough for stacksize bytes.
 G*
 runtime·malg(int32 stacksize)
 {
@@ -1146,15 +1162,13 @@ runtime·malg(int32 stacksize)
 	return newg;
 }
 
-/*
- * Newproc and deferproc need to be textflag 7
- * (no possible stack split when nearing overflow)
- * because they assume that the arguments to fn
- * are available sequentially beginning at &arg0.
- * If a stack split happened, only the one word
- * arg0 would be copied.  It's okay if any functions
- * they call split the stack below the newproc frame.
- */
+// Create a new g running fn with siz bytes of arguments.
+// Put it on the queue of g's waiting to run.
+// The compiler turns a go statement into a call to this.
+// Cannot split the stack because it assumes that the arguments
+// are available sequentially after &fn; they would not be
+// copied if a stack split occurred.  It's OK for this to call
+// functions that split the stack.
 #pragma textflag 7
 void
 runtime·newproc(int32 siz, byte* fn, ...)
@@ -1168,6 +1182,10 @@ runtime·newproc(int32 siz, byte* fn, ...)\
 	runtime·newproc1(fn, argp, siz, 0, runtime·getcallerpc(&siz));
 }
 
+// Create a new g running fn with narg bytes of arguments starting
+// at argp and returning nret bytes of results.  callerpc is the
+// address of the go statement that created this.  The new g is put
+// on the queue of g's waiting to run.
 G*
 runtime·newproc1(byte *fn, byte *argp, int32 narg, int32 nret, void *callerpc)
 {
@@ -1228,6 +1246,12 @@ runtime·newproc1(byte *fn, byte *argp, int32 narg, int32 nret, void *callerpc)
 //printf(" goid=%d\n", newg->goid);\
 }
 
+// Create a new deferred function fn with siz bytes of arguments.
+// The compiler turns a defer statement into a call to this.
+// Cannot split the stack because it assumes that the arguments
+// are available sequentially after &fn; they would not be
+// copied if a stack split occurred.  It's OK for this to call
+// functions that split the stack.
 #pragma textflag 7
 uintptr
 runtime·deferproc(int32 siz, byte* fn, ...)
@@ -1256,6 +1280,16 @@ runtime·deferproc(int32 siz, byte* fn, ...)
 	return 0;
 }
 
+// Run a deferred function if there is one.
+// The compiler inserts a call to this at the end of any
+// function which calls defer.
+// If there is a deferred function, this will call runtime·jmpdefer,
+// which will jump to the deferred function such that it appears
+// to have been called by the caller of deferreturn at the point
+// just before deferreturn was called.  The effect is that deferreturn
+// is called again and again until there are no more deferred functions.
+// Cannot split the stack because we reuse the caller's frame to
+// call the deferred function.
 #pragma textflag 7
 void
 runtime·deferreturn(uintptr arg0)
@@ -1277,6 +1311,7 @@ runtime·deferreturn(uintptr arg0)
 	runtime·jmpdefer(fn, argp);
 }
 
+// Run all deferred functions for the current goroutine.
 static void
 rundefer(void)
 {
@@ -1318,6 +1353,7 @@ unwindstack(G *gp, byte *sp)
 	}
 }
 
+// Print all currently active panics.  Used when crashing.
 static void
 printpanics(Panic *p)
 {
@@ -1334,6 +1370,7 @@ printpanics(Panic *p)
 
 static void recovery(G*);
 
+// The implementation of the predeclared function panic.
 void
 runtime·panic(Eface e)
 {
@@ -1376,6 +1413,9 @@ runtime·panic(Eface e)
 	runtime·dopanic(0);
 }
 
+// Unwind the stack after a deferred function calls recover
+// after a panic.  Then arrange to continue running as though
+// the caller of the deferred function returned normally.
 static void
 recovery(G *gp)
 {
@@ -1407,7 +1447,10 @@ recovery(G *gp)
 	runtime·gogo(&gp->sched, 1);
 }
 
-#pragma textflag 7	/* no split, or else g->stackguard is not the stack for fp */
+// The implementation of the predeclared function recover.
+// Cannot split the stack because it needs to reliably
+// find the stack segment of its caller.
+#pragma textflag 7
 void
 runtime·recover(byte *argp, Eface ret)
 {
@@ -1519,6 +1562,7 @@ runtime·Gosched(void)
 	runtime·gosched();
 }
 
+// Implementation of runtime.GOMAXPROCS.
 // delete when scheduler is stronger
 int32
 runtime·gomaxprocsfunc(int32 n)
@@ -1634,6 +1678,7 @@ static struct {
 	uintptr pcbuf[100];
 } prof;
 
+// Called if we receive a SIGPROF signal.
 void
 runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp)
 {
@@ -1653,6 +1698,7 @@ runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp)
 	runtime·unlock(&prof);\
 }
 
+// Arrange to call fn with a traceback hz times a second.
 void
 runtime·setcpuprofilerate(void (*fn)(uintptr*, int32), int32 hz)
 {
@@ -1683,6 +1729,8 @@ runtime·setcpuprofilerate(void (*fn)(uintptr*, int32), int32 hz)
 
 void (*libcgo_setenv)(byte**);
 
+// Update the C environment if cgo is loaded.
+// Called from os.Setenv.
 void
 os·setenv_c(String k, String v)
 {

コアとなるコードの解説

上記の変更箇所は、Goランタイムの proc.c 内の様々な関数に、その役割、動作原理、および特定の制約(特にスタック分割に関する textflag 7 の理由)を説明するコメントを追加しています。

例えば、runtime·newproc の変更を見てみましょう。

変更前:

/*
 * Newproc and deferproc need to be textflag 7
 * (no possible stack split when nearing overflow)
 * because they assume that the arguments to fn
 * are available sequentially beginning at &arg0.
 * If a stack split happened, only the one word
 * arg0 would be copied.  It's okay if any functions
 * they call split the stack below the newproc frame.
 */
#pragma textflag 7
void
runtime·newproc(int32 siz, byte* fn, ...)

変更後:

// Create a new g running fn with siz bytes of arguments.
// Put it on the queue of g's waiting to run.
// The compiler turns a go statement into a call to this.
// Cannot split the stack because it assumes that the arguments
// are available sequentially after &fn; they would not be
// copied if a stack split occurred.  It's OK for this to call
// functions that split the stack.
#pragma textflag 7
void
runtime·newproc(int32 siz, byte* fn, ...)

この変更では、古いCスタイルのコメントブロックが、より現代的なC++スタイルの行コメントに置き換えられています。内容は基本的に同じですが、より簡潔で読みやすくなっています。

新しいコメントは、以下の点を明確にしています。

  • 関数の目的: 「fnsiz バイトの引数で実行する新しいGを作成する。」
  • スケジューリング: 「実行待ちのGのキューに入れる。」
  • コンパイラの役割: 「コンパイラは go ステートメントをこれへの呼び出しに変換する。」
  • スタック分割の制約: 「スタックを分割できない。なぜなら、引数が &fn の後に連続して利用可能であることを前提としており、スタック分割が発生した場合、それらはコピーされないからである。」
  • 許容される動作: 「この関数が呼び出す関数が newproc フレームの下でスタックを分割することは問題ない。」

特に「Cannot split the stack...」の部分は、#pragma textflag 7 がなぜ必要であるかという技術的な理由を詳細に説明しており、Goランタイムの低レベルなスタック管理の複雑さを浮き彫りにしています。引数がスタック上に連続して配置されていることを前提とする関数では、スタック分割によって引数の配置が変更されると、関数が正しく動作しなくなる可能性があるため、このような制約が設けられています。

他の関数についても同様に、その機能、呼び出し元、呼び出し先の関係、そしてスタック管理やスケジューリングに関する特定の制約が詳細にコメントされています。これにより、Goランタイムの内部構造がより透過的になり、開発者がその動作を深く理解するための手助けとなります。

関連リンク

参考にした情報源リンク