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

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

このコミットは、Goランタイムにおけるスタック分割中のプリエンプション(横取り)に関するアサーションを緩和するものです。具体的には、runtime: g is running but p is not というパニックが発生する可能性のある特定の競合状態を修正し、starttheworld 関数が acquirep を呼び出してゴルーチンを再開しようとする際に、acquirep がプリエンプトされるケースに対応しています。

コミット

commit 00a757fb74c211513771338fe84ef195d3aa9d55
Author: Russ Cox <rsc@golang.org>
Date:   Mon Oct 28 19:40:40 2013 -0400

    runtime: relax preemption assertion during stack split
    
    The case can happen when starttheworld is calling acquirep
    to get things moving again and acquirep gets preempted.
    The stack trace is in golang.org/issue/6644.
    
    It is difficult to build a short test case for this, but
    the person who reported issue 6644 confirms that this
    solves the problem.
    
    Fixes #6644.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/18740044

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

https://github.com/golang/go/commit/00a757fb74c211513771338fe84ef195d3aa9d55

元コミット内容

runtime: relax preemption assertion during stack split

The case can happen when starttheworld is calling acquirep
to get things moving again and acquirep gets preempted.
The stack trace is in golang.org/issue/6644.

It is difficult to build a short test case for this, but
the person who reported issue 6644 confirms that this
solves the problem.

Fixes #6644.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/18740044

変更の背景

この変更は、Goランタイムにおける特定のパニック runtime: g is running but p is not を修正するために導入されました。このパニックは、Goのスケジューラがゴルーチン(G)、論理プロセッサ(P)、OSスレッド(M)を管理する際に発生する競合状態に起因します。

具体的には、starttheworld という関数が、停止していたゴルーチンの実行を再開するために acquirep を呼び出す際に問題が発生していました。acquirep はP(論理プロセッサ)を取得する関数ですが、この acquirep の実行中にプリエンプション(横取り)が発生することがありました。

Goランタイムには、ゴルーチンのスタックが拡張される必要がある際に runtime·newstack 関数が呼び出され、その中で様々なアサーション(前提条件のチェック)が行われます。問題のパニックは、runtime·newstack 内の if(oldstatus == Grunning && m->p == nil) というアサーションが、acquirep がプリエンプトされた状況下で誤ってトリガーされることによって引き起こされていました。

golang.org/issue/6644 で報告されたスタックトレースがこの問題を示しており、このコミットは、この特定のエッジケースにおけるアサーションの条件を緩和することで、パニックの発生を防ぐことを目的としています。この問題は再現が困難なため、報告者による修正の確認が重要視されました。

前提知識の解説

このコミットを理解するためには、Goランタイムのスケジューラ、スタック管理、およびプリエンプションに関するいくつかの重要な概念を理解する必要があります。

  1. Goroutine (G): Goにおける軽量な実行単位です。OSスレッドよりもはるかに軽量で、数百万個作成することも可能です。各ゴルーチンは独自のスタックを持ちます。
  2. Logical Processor (P): 論理プロセッサは、Goスケジューラがゴルーチンを実行するために必要なリソースのコンテキストを提供します。Pの数は通常、GOMAXPROCS 環境変数によって制御され、デフォルトではCPUのコア数に設定されます。Pは、実行可能なゴルーチンのキューを保持し、M(OSスレッド)にゴルーチンをディスパッチします。
  3. Machine (M) / OS Thread: OSスレッドは、実際にCPU上でコードを実行するOSレベルのエンティティです。Goランタイムは、Pに割り当てられたゴルーチンを実行するためにMを使用します。MはPと関連付けられ、Pが持つゴルーチンキューからゴルーチンを取り出して実行します。
  4. MPGモデル: Goのスケジューラは、G(Goroutine)、P(Logical Processor)、M(OS Thread)の3つのエンティティで構成されるMPGモデルを採用しています。
    • Gは実行されるコードです。
    • PはGを実行するためのコンテキストを提供します。
    • MはPに割り当てられたGをOS上で実行します。
  5. Stack Splitting (スタック分割): Goのゴルーチンは、最初は小さなスタック(通常は2KB)で開始されます。関数呼び出しがスタックを使い果たすと、Goランタイムは自動的にスタックを拡張します。このプロセスをスタック分割と呼びます。スタックの拡張が必要になると、runtime·newstack 関数が呼び出されます。
  6. Preemption (プリエンプション/横取り): Goランタイムは、長時間実行されるゴルーチンが他のゴルーチンの実行を妨げないように、プリエンプションメカニズムを持っています。これにより、スケジューラは実行中のゴルーチンを一時停止させ、別のゴルーチンにCPUを割り当てることができます。プリエンプションは、関数プロローグ(関数の開始時)にスタックガードページをチェックすることで行われます。gp->stackguard0 == (uintptr)StackPreempt は、プリエンプションが要求されていることを示すフラグです。
  7. starttheworld: Goランタイムが、GC(ガベージコレクション)などの理由で全てのゴルーチンの実行を一時停止(stoptheworld)した後、再びゴルーチンの実行を再開する際に呼び出される関数です。
  8. acquirep: M(OSスレッド)がP(論理プロセッサ)を取得するために呼び出す関数です。Mがゴルーチンを実行するためには、Pを所有している必要があります。
  9. m->locks: M(OSスレッド)が現在保持しているロックの数を追跡するカウンタです。このカウンタが0でない場合、Mは重要なセクションにいる可能性があり、プリエンプションやスケジューリングの変更が安全でない場合があります。
  10. Grunning: ゴルーチンの状態の一つで、現在実行中であることを示します。
  11. Gsyscall: ゴルーチンの状態の一つで、現在システムコールを実行中であることを示します。システムコール中は、Goランタイムはゴルーチンをプリエンプトできません。

技術的詳細

このコミットが修正する問題は、runtime·newstack 関数内の特定のアサーションが、starttheworldacquirep の相互作用におけるエッジケースで誤ってトリガーされることでした。

runtime·newstack は、ゴルーチンのスタック拡張が必要になった際に呼び出されます。この関数内には、ランタイムの整合性を保証するためのいくつかのアサーションが含まれています。問題となったのは以下の行です。

if(oldstatus == Grunning && m->p == nil)
    runtime·throw("runtime: g is running but p is not");

このアサーションは、「ゴルーチンが実行中 (Grunning) であるにもかかわらず、そのゴルーチンを実行しているM(OSスレッド)がP(論理プロセッサ)を所有していない (m->p == nil)」という矛盾した状態を検出するためのものです。通常、ゴルーチンが実行中であるならば、それを実行しているMは必ずPを所有しているはずだからです。

しかし、starttheworldacquirep を呼び出してPを取得しようとしている最中に、acquirep 自体がプリエンプトされるという稀な競合状態が発生することがありました。この状況では、MはまだPを完全に取得していない (m->p == nil) にもかかわらず、プリエンプションによって runtime·newstack が呼び出され、その時点でのゴルーチンの状態が Grunning であると判断されることがありました。これにより、上記のアサーションが誤ってトリガーされ、パニックが発生していました。

このコミットは、このアサーションに && m->locks == 0 という条件を追加することで、この問題を解決します。

if(oldstatus == Grunning && m->p == nil && m->locks == 0)
    runtime·throw("runtime: g is running but p is not");

m->locks は、Mが現在保持しているロックの数を追跡するカウンタです。m->locks > 0 の場合、Mはランタイムの重要なセクション(例えば、スケジューラがロックを保持している間など)にいることを意味します。このような状況では、Mはプリエンプトされるべきではなく、また、m->p == nil であっても、それは一時的な状態である可能性があります。

acquirep の実行中にプリエンプションが発生し、runtime·newstack が呼び出されるような状況では、acquirep は通常、ランタイムの内部ロックを保持しています。したがって、m->locks は0ではないはずです。この追加された条件により、m->locks > 0 の場合にはこのアサーションがスキップされ、acquirep がPを安全に取得するまで待機できるようになります。これにより、正当な競合状態での誤ったパニックが回避されます。

この変更は、Goランタイムの堅牢性を高め、特定の稀な競合状態における安定性を向上させるものです。

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

--- a/src/pkg/runtime/stack.c
+++ b/src/pkg/runtime/stack.c
@@ -255,7 +255,7 @@ runtime·newstack(void)\n \tif(gp->stackguard0 == (uintptr)StackPreempt) {\n \t\tif(gp == m->g0)\n \t\t\truntime·throw(\"runtime: preempt g0\");\n-\t\tif(oldstatus == Grunning && m->p == nil)\n+\t\tif(oldstatus == Grunning && m->p == nil && m->locks == 0)\n \t\t\truntime·throw(\"runtime: g is running but p is not\");\n \t\tif(oldstatus == Gsyscall && m->locks == 0)\n \t\t\truntime·throw(\"runtime: stack split during syscall\");\n```

## コアとなるコードの解説

変更は `src/pkg/runtime/stack.c` ファイルの `runtime·newstack` 関数内の一行です。

元のコード:
```c
if(oldstatus == Grunning && m->p == nil)
    runtime·throw("runtime: g is running but p is not");

変更後のコード:

if(oldstatus == Grunning && m->p == nil && m->locks == 0)
    runtime·throw("runtime: g is running but p is not");

この変更は、既存のアサーション oldstatus == Grunning && m->p == nil に、追加の条件 && m->locks == 0 を加えるものです。

  • oldstatus == Grunning: 現在のゴルーチン(G)が実行中状態であることを示します。
  • m->p == nil: 現在のOSスレッド(M)が論理プロセッサ(P)を所有していないことを示します。
  • m->locks == 0: 現在のOSスレッド(M)がランタイムの内部ロックを何も保持していないことを示します。

この修正により、Mがランタイムの内部ロックを保持している(つまり m->locks > 0 である)間は、たとえ oldstatus == Grunning かつ m->p == nil であっても、このアサーションはトリガーされなくなります。これは、acquirep のような関数がPを取得しようとしている最中に、一時的に m->p == nil の状態になることがあり、その際にランタイムのロックを保持しているため、このアサーションが誤ってパニックを引き起こすのを防ぐためです。

この変更は、Goランタイムのスケジューラとスタック管理の間の微妙な競合状態を考慮に入れ、より堅牢な動作を保証します。

関連リンク

参考にした情報源リンク

  • Goのスケジューラに関する一般的な情報源 (例: Goのドキュメント、ブログ記事など)
  • Goランタイムのソースコード (特に src/runtime/ ディレクトリ)
  • Goのスタック管理とプリエンプションに関する技術記事