[インデックス 16796] ファイルの概要
このコミットは、Goランタイムにおけるプリエンプション(横取り)メカニズムの信頼性を向上させるための変更です。具体的には、プリエンプションシグナルが失われる可能性があった問題を解決し、より堅牢なプリエンプションを実現しています。
コミット
commit 5887f142a33fbb8da94088e902ced4101a16aa8f
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Jul 17 12:52:37 2013 -0400
runtime: more reliable preemption
Currently preemption signal g->stackguard0==StackPreempt
can be lost if it is received when preemption is disabled
(e.g. m->lock!=0). This change duplicates the preemption
signal in g->preempt and restores g->stackguard0
when preemption is enabled.
Update #543.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/10792043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5887f142a33fbb8da94088e902ced4101a16aa8f
元コミット内容
runtime: more reliable preemption
Currently preemption signal g->stackguard0==StackPreempt
can be lost if it is received when preemption is disabled
(e.g. m->lock!=0). This change duplicates the preemption
signal in g->preempt and restores g->stackguard0
when preemption is enabled.
Update #543.
変更の背景
Goランタイムにおけるプリエンプションは、Goのスケジューラが実行中のゴルーチンを中断し、別のゴルーチンに切り替えることで、公平性と応答性を確保するための重要なメカニズムです。Go 1.14以前は協調的プリエンプションが主であり、ゴルーチンは関数呼び出しなどの「セーフポイント」でのみプリエンプション可能でした。しかし、無限ループなどで関数呼び出しがない場合、そのゴルーチンがCPUを占有し続け、他のゴルーチンが実行されない「ゴルーチン飢餓」が発生する可能性がありました。
このコミットが作成された2013年時点では、Goのプリエンプションはg->stackguard0 == StackPreemptという特殊なスタックガード値を利用して行われていました。これは、スタックの拡張チェック時にプリエンプションをトリガーする仕組みです。しかし、このシグナルは、プリエンプションが無効になっている期間(例えば、ミューテックスがロックされている間など、m->lock != 0の場合)に受信されると失われる可能性がありました。プリエンプションが無効な間にシグナルが来ても、そのシグナルが記憶されず、プリエンプションが有効になったときに再度トリガーされないため、プリエンプションが遅延したり、全く行われなかったりする問題がありました。
このコミットは、このプリエンプションシグナルの喪失を防ぎ、より信頼性の高いプリエンプションを実現することを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念とプリエンプションの仕組みに関する知識が必要です。
- ゴルーチン (Goroutine): Goランタイムによって管理される軽量な実行スレッドです。Goの並行処理の基本単位となります。各ゴルーチンは
G構造体で表現されます。 - M (Machine): OSのスレッドに相当します。Goランタイムは、OSスレッド上でゴルーチンを実行します。
M構造体で表現されます。 - P (Processor): 論理プロセッサに相当し、ゴルーチンを実行するためのコンテキストを提供します。
P構造体で表現され、MとGの間に位置します。 - スケジューラ: Goランタイムの重要なコンポーネントで、ゴルーチンを
MとPに割り当て、実行を管理します。 - プリエンプション (Preemption): 実行中のゴルーチンを強制的に中断し、別のゴルーチンにCPUを切り替えるメカニズムです。これにより、特定のゴルーチンがCPUを独占するのを防ぎ、公平なリソース配分を実現します。
g->stackguard0: 各ゴルーチン(G構造体)が持つフィールドで、スタックの境界を示す値です。Goランタイムは、関数呼び出し時にスタックがこの境界を超えていないかチェックします。StackPreempt:g->stackguard0に設定される特殊な値で、プリエンプションを要求するシグナルとして機能します。スタックチェック時にg->stackguard0がStackPreemptであると検出されると、ランタイムはプリエンプション処理を開始します。m->locks: 現在のM(OSスレッド)が保持しているランタイムロックの数を示すカウンタです。この値が0でない場合、ランタイムは重要な処理を実行中であり、プリエンプションを一時的に無効にする必要があります。m->mallocing: 現在のMがメモリ割り当て中であるかを示すフラグです。メモリ割り当て中はプリエンプションを無効にする必要があります。m->gcing: 現在のMがGC処理中であるかを示すフラグです。GC処理中はプリエンプションを無効にする必要があります。runtime·newstack: スタックの拡張やプリエンプション処理を行うランタイム関数です。g->stackguard0がStackPreemptであると検出された場合に呼び出されます。
技術的詳細
このコミットの核心は、プリエンプションシグナルの永続化と復元メカニズムの導入です。
従来のGoランタイムでは、プリエンプションは主にg->stackguard0フィールドにStackPreemptという特殊な値を設定することで行われていました。ゴルーチンが関数呼び出しを行う際、ランタイムはg->stackguard0と現在のスタックポインタを比較し、g->stackguard0がStackPreemptであれば、プリエンプションが必要であると判断し、runtime·newstack関数を呼び出してプリエンプション処理を開始します。
しかし、このメカニズムには問題がありました。runtime·newstack関数内で、プリエンプションが一時的に無効化される条件(例: m->locks != 0、m->mallocing != 0、m->gcing != 0)がチェックされます。これらの条件が真の場合、つまりランタイムがロックを保持している、メモリ割り当て中である、またはGC中である場合、プリエンプションは即座には行われず、ゴルーチンは実行を継続します。この際、g->stackguard0は元のg->stackguardに戻されていました。
問題は、プリエンプションが無効な期間中にStackPreemptが設定されても、その情報が失われてしまうことでした。プリエンプションが無効な状態が解除された後、ランタイムはプリエンプションが要求されていたことを「忘れて」しまい、そのゴルーチンはプリエンプションされることなく実行を続けてしまう可能性がありました。
このコミットは、この問題を解決するために、G構造体に新しいフィールドg->preempt(bool型)を導入します。
- プリエンプション要求の複製: プリエンプションが要求された際(
runtime·preemptone関数内)、g->stackguard0 = StackPreemptを設定するだけでなく、新しく追加されたg->preempt = trueも設定します。これにより、プリエンプション要求がg->stackguard0とg->preemptの2箇所に記録されるようになります。 - プリエンプションの遅延と復元:
runtime·newstack関数内でプリエンプションが一時的に無効化される場合、g->stackguard0は元の値に戻されますが、g->preemptはtrueのまま維持されます。 - プリエンプション有効化時の復元: ランタイムがロックを解放する、メモリ割り当てを終了する、GC処理を終了するなど、プリエンプションが無効化されていた状態が解除されるタイミングで、
m->locks == 0 && g->preemptという条件をチェックします。この条件が真であれば、以前にプリエンプションが要求されていたことを検出し、g->stackguard0 = StackPreemptを再度設定します。これにより、ゴルーチンが次にスタックチェックを行う際に、プリエンプションがトリガーされることが保証されます。 - プリエンプション実行後のリセット: 実際にプリエンプションが実行され、ゴルーチンが実行状態(
Grunning)になった際には、g->preempt = falseにリセットされます。
この変更により、プリエンプションシグナルが一時的に失われることなく、プリエンプションが可能な状態になったときに確実に再トリガーされるようになり、Goランタイムのプリエンプションメカニズムの信頼性が大幅に向上しました。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
src/pkg/runtime/lock_futex.csrc/pkg/runtime/lock_sema.csrc/pkg/runtime/malloc.gocsrc/pkg/runtime/mgc0.csrc/pkg/runtime/proc.csrc/pkg/runtime/runtime.hsrc/pkg/runtime/stack.c
それぞれのファイルにおける主要な変更点は以下の通りです。
src/pkg/runtime/runtime.h
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -253,6 +253,7 @@ struct G
bool issystem; // do not output in stack dump
bool isbackground; // ignore in deadlock detector
bool blockingsyscall; // hint that the next syscall will block
+ bool preempt; // preemption signal, duplicates stackguard0 = StackPreempt
int8 traceignore; // ignore race detection events
M* m; // for debuggers, but offset not hard-coded
M* lockedm;
G構造体にpreemptという新しいboolフィールドが追加されています。これは、プリエンプション要求が一時的に無効化された場合でも、その要求を記憶するためのフラグです。
src/pkg/runtime/stack.c
--- a/src/pkg/runtime/stack.c
+++ b/src/pkg/runtime/stack.c
@@ -250,7 +250,7 @@ runtime·newstack(void)\n \t// We are interested in preempting user Go code, not runtime code.\n \tif(oldstatus != Grunning || m->locks || m->mallocing || m->gcing) {\n \t\t// Let the goroutine keep running for now.\n-\t\t\t// TODO(dvyukov): remember but delay the preemption.\n+\t\t\t// gp->preempt is set, so it will be preempted next time.\n \t\tgp->stackguard0 = gp->stackguard;\n \t\tgp->status = oldstatus;\n \t\truntime·gogo(&gp->sched);\t// never return
runtime·newstack関数内で、プリエンプションが一時的に無効化される条件(m->locks、m->mallocing、m->gcing)が満たされた場合、gp->stackguard0は元の値に戻されますが、gp->preemptが設定されているため、プリエンプション要求が記憶されることがコメントで示されています。
--- a/src/pkg/runtime/stack.c
+++ b/src/pkg/runtime/stack.c
@@ -2174,6 +2187,7 @@ if(1) return;\n gp = mp->curg;\n if(gp == nil || gp == mp->g0)\n return;\n+ gp->preempt = true;\n gp->stackguard0 = StackPreempt;\n }\n \n```
`runtime·preemptone`関数(プリエンプションを要求する関数)内で、`gp->stackguard0 = StackPreempt`を設定するだけでなく、`gp->preempt = true`も設定されるようになりました。
### `src/pkg/runtime/lock_futex.c`, `src/pkg/runtime/lock_sema.c`, `src/pkg/runtime/malloc.goc`, `src/pkg/runtime/mgc0.c`, `src/pkg/runtime/proc.c`
これらのファイルでは、ランタイムロックの解放、メモリ割り当ての終了、GC処理の終了など、プリエンプションが無効化されていた状態が解除される可能性のある箇所で、以下のコードが追加されています。
```diff
--- a/src/pkg/runtime/lock_futex.c
+++ b/src/pkg/runtime/lock_futex.c
@@ -99,6 +100,8 @@ runtime·unlock(Lock *l)\n \n if(--m->locks < 0)\n runtime·throw("runtime·unlock: lock count");
+ if(m->locks == 0 && g->preempt) // restore the preemption request in case we've cleared it in newstack
+ g->stackguard0 = StackPreempt;
}
(lock_futex.cとlock_sema.cのruntime·unlock関数内)
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -94,6 +95,8 @@ runtime·mallocgc(uintptr size, uint32 flag, int32 dogc, int32 zeroed)\n *(uintptr*)((uintptr)v+size-sizeof(uintptr)) = 0;\n \n m->mallocing = 0;
+ if(g->preempt) // restore the preemption request in case we've cleared it in newstack
+ g->stackguard0 = StackPreempt;
\n if(!(flag & FlagNoProfiling) && (rate = runtime·MemProfileRate) > 0) {
(malloc.gocのruntime·mallocgc関数内)
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -2031,6 +2032,8 @@ runtime·gc(int32 force)\n // give the queued finalizers, if any, a chance to run\n runtime·gosched();
}
+ if(g->preempt) // restore the preemption request in case we've cleared it in newstack
+ g->stackguard0 = StackPreempt;
}
(mgc0.cのruntime·gc関数内)
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -294,6 +294,8 @@ runtime·ready(G *gp)\n if(runtime·atomicload(&runtime·sched.npidle) != 0 && runtime·atomicload(&runtime·sched.nmspinning) == 0) // TODO: fast atomic\n wakep();
m->locks--;
+ if(m->locks == 0 && g->preempt) // restore the preemption request in case we've cleared it in newstack
+ g->stackguard0 = StackPreempt;
}
(proc.cのruntime·ready, runtime·starttheworld, runtime·allocm, runtime·exitsyscall, runtime·newproc1関数内)
これらの箇所では、m->locks == 0 && g->preemptという条件がチェックされ、もしプリエンプションが無効化されていた期間にプリエンプション要求があった場合(g->preemptがtrue)、g->stackguard0をStackPreemptに再設定しています。
また、src/pkg/runtime/proc.cのexecute関数では、ゴルーチンが実行状態になる際にgp->preempt = falseにリセットされています。
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1008,6 +1014,7 @@ execute(G *gp)\n runtime·throw("execute: bad g status");
}
gp->status = Grunning;
+ gp->preempt = false;
gp->stackguard0 = gp->stackguard;
m->p->tick++;
m->curg = gp;
コアとなるコードの解説
このコミットの主要な変更は、G構造体にpreemptフィールドを追加し、プリエンプション要求のライフサイクルをより堅牢に管理することです。
-
G構造体へのpreemptフィールド追加:src/pkg/runtime/runtime.hでbool preempt;がG構造体に追加されました。これは、プリエンプション要求が発行されたが、ランタイムの内部状態(例: ロック保持中)により即座に処理できない場合に、その要求を記憶するためのフラグです。 -
プリエンプション要求時の
preempt設定:src/pkg/runtime/stack.cのruntime·preemptone関数は、特定のゴルーチンをプリエンプション対象としてマークする役割を担います。この関数内で、従来のgp->stackguard0 = StackPreempt;に加えて、gp->preempt = true;が追加されました。これにより、プリエンプション要求がstackguard0とpreemptの両方に記録されることになります。 -
runtime·newstackでのプリエンプション遅延処理の改善:src/pkg/runtime/stack.cのruntime·newstack関数は、スタックの拡張が必要な場合やプリエンプションが要求された場合に呼び出されます。この関数内で、m->locks、m->mallocing、m->gcingなどのフラグがチェックされ、ランタイムが重要な内部処理を行っている間はプリエンプションを一時的に無効化します。 変更前は、この無効化期間中にstackguard0が元の値に戻されると、プリエンプション要求が失われていました。変更後は、gp->preemptがtrueのまま維持されるため、プリエンプション要求が記憶されます。コメント// gp->preempt is set, so it will be preempted next time.が追加され、この新しい動作が明確にされています。 -
プリエンプション有効化時の
stackguard0復元:src/pkg/runtime/lock_futex.c,src/pkg/runtime/lock_sema.c(runtime·unlock関数),src/pkg/runtime/malloc.goc(runtime·mallocgc関数),src/pkg/runtime/mgc0.c(runtime·gc関数),src/pkg/runtime/proc.c(runtime·ready,runtime·starttheworld,runtime·allocm,runtime·exitsyscall,runtime·newproc1関数) など、ランタイムがロックを解放したり、内部処理を完了したりしてプリエンプションが再び可能になる可能性のある多くの箇所で、以下のパターンが追加されました。if(m->locks == 0 && g->preempt) g->stackguard0 = StackPreempt;このコードは、
m->locksが0(ランタイムロックが解放された状態)であり、かつg->preemptがtrue(以前にプリエンプション要求があった)の場合に、g->stackguard0をStackPreemptに再設定します。これにより、ゴルーチンが次にスタックチェックを行う際に、プリエンプションが確実にトリガーされるようになります。これは、プリエンプション要求が一時的に遅延されたとしても、最終的に実行されることを保証する重要なメカニズムです。 -
プリエンプション実行後の
preemptリセット:src/pkg/runtime/proc.cのexecute関数は、ゴルーチンが実際に実行状態(Grunning)になる直前に呼び出されます。この関数内でgp->preempt = false;が追加されました。これは、プリエンプションが正常に処理された後、preemptフラグをリセットし、次のプリエンプション要求に備えるためです。
これらの変更により、Goランタイムはプリエンプション要求をより確実に処理できるようになり、プリエンプションが無効な期間中に発生した要求も失われることなく、適切なタイミングで実行されるようになりました。これにより、スケジューラの公平性が向上し、ゴルーチン飢餓の発生が抑制されます。
関連リンク
- Go CL: https://golang.org/cl/10792043
- Go Issue #543: https://github.com/golang/go/issues/543 (このコミットが解決する問題に関連するIssue)
参考にした情報源リンク
- Go runtime preemption: https://dzone.com/articles/go-runtime-preemption
- Go runtime preemption: https://unskilled.blog/go-runtime-preemption/
- Go runtime preemption: https://www.sap.com/documents/2023/09/92231220-427c-0010-b286-fd51a8201a1e.html
- Go runtime preemption: https://go.googlesource.com/go/+/refs/heads/master/src/runtime/preempt.go
- Go runtime preemption: https://medium.com/@ankur_anand/go-scheduler-part-2-preemption-in-go-1-14-a7020830a2b
- Go runtime preemption: https://medium.com/@ankur_anand/go-scheduler-part-1-cooperative-preemption-in-go-1-13-and-earlier-a7020830a2b
- Go runtime preemption: https://go.dev/blog/go1.14-preemption
- Go runtime preemption: https://stackoverflow.com/questions/60000000/how-does-go-1-14-preemption-work
- Go runtime preemption: https://medium.com/@ankur_anand/go-scheduler-part-3-asynchronous-preemption-in-go-1-14-a7020830a2b
- Go runtime preemption: https://stackoverflow.com/questions/60000000/how-does-go-1-14-preemption-work