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

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

このコミットは、Goランタイムにプリエンプション(横取り)のための関数を導入するものです。現時点ではこれらの関数は使用されていませんが、将来的なプリエンプティブスケジューラの実装に向けた基盤となる変更です。具体的には、src/pkg/runtime/proc.cpreemptall および preemptone 関数が追加され、src/pkg/runtime/stack.hStackPreempt という定数が定義されています。

コミット

commit 354ec5166668cae9be899c82e20c38b32ae3b867
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Jun 3 13:20:17 2013 +0400

    runtime: introduce preemption function (not used for now)
    This is part of preemptive scheduler.
    
    R=golang-dev, cshapiro, iant
    CC=golang-dev
    https://golang.org/cl/9843046

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

https://github.com/golang/go/commit/354ec5166668cae9be899c82e20c38b32ae3b867

元コミット内容

runtime: introduce preemption function (not used for now)
This is part of preemptive scheduler.

変更の背景

このコミットは、Go言語のランタイムにおけるスケジューラの進化の一環として行われました。Goの初期のスケジューラは、協調的(cooperative)なスケジューリングモデルを採用していました。これは、ゴルーチンが明示的にスケジューラに制御を返す(例えば、チャネル操作やシステムコールなど)まで、CPUを占有し続けることを意味します。このモデルはシンプルで効率的ですが、長時間実行される計算集約的なゴルーチンが存在する場合、他のゴルーチンがCPU時間を全く得られず、プログラム全体の応答性が低下する「飢餓(starvation)」の問題を引き起こす可能性がありました。

この問題を解決するために、Go開発チームはプリエンプティブ(preemptive)スケジューラの導入を進めていました。プリエンプティブスケジューラは、ゴルーチンが自発的に制御を返さなくても、ランタイムが強制的にゴルーチンの実行を中断し、別のゴルーチンにCPUを割り当てることができます。これにより、すべてのゴルーチンが公平にCPU時間を共有し、応答性が向上します。

このコミットは、そのプリエンプティブスケジューラを実現するための初期ステップであり、ゴルーチンを「プリエンプト(横取り)」するためのメカニズムを導入しています。コミットメッセージに「(not used for now)」とあるように、この時点ではまだ実際のプリエンプションは有効になっていませんでしたが、将来の完全なプリエンプティブスケジューラへの布石として、そのための関数とシグナルが準備されました。特に、スタック検査を利用したプリエンプションのアイデアがこの時期に検討されており、stackguard0 の値を変更することで、スタックオーバーフローチェックをトリガーし、ランタイムに制御を戻すという手法が採用されました。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念とスケジューリングに関する知識が必要です。

  1. Goroutine (ゴルーチン): Go言語における軽量な実行単位です。OSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行できます。
  2. M (Machine/Thread): OSのスレッドを表します。Goランタイムは、OSのスレッド上でゴルーチンを実行します。
  3. P (Processor): 論理的なプロセッサを表します。Goランタイムは、MとPを関連付け、Pがゴルーチンを実行するためのコンテキストを提供します。Pは、実行可能なゴルーチンのキューを保持します。
  4. G-M-P スケジューリングモデル: Goランタイムのスケジューラは、Goroutine (G)、Machine (M)、Processor (P) の3つのエンティティで構成されます。GはM上で実行され、MはPにアタッチされます。Pは実行可能なGのキューを管理し、MにGをディスパッチします。
  5. 協調的スケジューリング (Cooperative Scheduling): ゴルーチンが自発的にスケジューラに制御を返すまで実行を継続する方式です。Goでは、チャネル操作、time.Sleep、システムコールなどが制御を返すポイントとなります。
  6. プリエンプティブスケジューリング (Preemptive Scheduling): ランタイムがゴルーチンの実行を強制的に中断し、別のゴルーチンにCPUを割り当てる方式です。これにより、長時間実行されるゴルーチンが他のゴルーチンの実行を妨げることを防ぎます。
  7. スタックガード (Stack Guard): Goのゴルーチンは可変長のスタックを使用します。スタックガードは、スタックオーバーフローを防ぐためのメカニズムです。各ゴルーチンのスタックには、スタックの境界を示す特別な値(stackguard0)が設定されています。関数呼び出し時にスタックの使用量が増えると、このstackguard0の値と比較され、スタックが不足しそうになるとランタイムに制御が戻り、スタックが拡張されます。
  8. StackPreempt: このコミットで導入された特別な値で、stackguard0に設定されることで、スタックオーバーフローチェックを意図的にトリガーし、ゴルーチンをプリエンプトするためのシグナルとして機能します。

技術的詳細

このコミットの核心は、Goランタイムがゴルーチンをプリエンプトするための「シグナル」をどのように送るか、という点にあります。Goのプリエンプションは、OSのシグナルやタイマー割り込みに依存するのではなく、既存のスタック検査メカニズムを巧妙に利用しています。

  1. StackPreempt 定数の導入: src/pkg/runtime/stack.hStackPreempt という新しい定数が定義されました。この値は 0xfffffade という非常に大きなアドレス値に設定されています。

    // Goroutine preemption request.
    // Stored into g->stackguard0 to cause split stack check failure.
    // Must be greater than any real sp.
    StackPreempt = (uintptr)(intptr)0xfffffade,
    

    このコメントが示すように、StackPreemptg->stackguard0 に格納され、スタック分割チェック(split stack check)を意図的に失敗させるために使用されます。通常のスタックポインタ(sp)はこれよりもはるかに小さい値を取るため、spstackguard0と比較される際に、常にStackPreemptの方が大きく、スタックが不足していると判断されるようになります。

  2. preemptone 関数の実装: src/pkg/runtime/proc.cpreemptone(P *p) 関数が追加されました。この関数は、特定のP(プロセッサ)上で現在実行中のゴルーチンをプリエンプトするよう試みます。

    static void
    preemptone(P *p)
    {
        M *mp;
        G *gp;
    
        mp = p->m;
        if(mp == nil || mp == m)
            return;
        gp = mp->curg;
        if(gp == nil || gp == mp->g0)
            return;
        gp->stackguard0 = StackPreempt;
    }
    

    この関数は以下のステップを実行します。

    • p->m から、そのPにアタッチされているM(OSスレッド)を取得します。
    • Mがnilであるか、または現在のM(m)と同じであれば、処理をスキップします。これは、自分自身をプリエンプトしようとしないためです。
    • mp->curg から、そのM上で現在実行中のG(ゴルーチン)を取得します。
    • Gがnilであるか、またはスケジューラ自身のゴルーチン(mp->g0)であれば、処理をスキップします。スケジューラゴルーチンはプリエンプトの対象外です。
    • 最後に、対象のゴルーチン gpstackguard0 フィールドに StackPreempt の値を設定します。

    この変更により、次にこのゴルーチンが関数呼び出しを行う際、スタック検査が実行されます。stackguard0StackPreemptに設定されているため、スタック検査は必ず失敗し、ランタイムのスタック拡張ルーチン(morestackなど)が呼び出されます。このルーチン内で、ランタイムはゴルーチンがプリエンプト要求を受けていることを認識し、実行を中断してスケジューラに制御を戻すことができます。

  3. preemptall 関数の実装: src/pkg/runtime/proc.cpreemptall() 関数が追加されました。この関数は、現在実行中のすべてのP(プロセッサ)に対して preemptone を呼び出すことで、すべてのゴルーチンをプリエンプトするよう試みます。

    static void
    preemptall(void)
    {
        P *p;
        int32 i;
    
        for(i = 0; i < runtime·gomaxprocs; i++) {
            p = runtime·allp[i];
            if(p == nil || p->status != Prunning)
                continue;
            preemptone(p);
        }
    }
    

    この関数は、runtime·gomaxprocs(利用可能な論理プロセッサの数)の数だけループし、各PがPrunning状態(ゴルーチンを実行中)であれば、preemptoneを呼び出します。

注意点: コミットメッセージにあるように、これらの関数は「(not used for now)」でした。これは、この時点ではまだこれらのプリエンプションメカニズムを実際にトリガーする上位のロジック(例えば、タイマー割り込みやシステムコールからの定期的なチェック)が実装されていなかったことを意味します。このコミットは、プリエンプションの「手段」を準備するものであり、その「トリガー」は後続のコミットで実装されることになります。また、preemptoneのコメントにあるように、これは「best-effort」(最善努力)であり、常に成功するとは限りません。特に、runtime·newstackのようなランタイム内部の重要な処理を実行中のゴルーチンは、プリエンプション要求を無視する可能性があります。

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

src/pkg/runtime/proc.c

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -110,6 +110,8 @@ static G* globrunqget(P*);
 static P* pidleget(void);
 static void pidleput(P*);
 static void injectglist(G*);
+static void preemptall(void);
+static void preemptone(P*);
 
 // The bootstrap sequence is:
 //
@@ -2073,6 +2075,45 @@ retake(uint32 *ticks)
 	return n;
 }
 
+// Tell all goroutines that they have been preempted and they should stop.
+// This function is purely best-effort.  It can fail to inform a goroutine if a
+// processor just started running it.
+// No locks need to be held.
+static void
+preemptall(void)
+{
+	P *p;
+	int32 i;
+
+	for(i = 0; i < runtime·gomaxprocs; i++) {
+		p = runtime·allp[i];
+		if(p == nil || p->status != Prunning)
+			continue;
+		preemptone(p);
+	}
+}
+
+// Tell the goroutine running on processor P to stop.
+// This function is purely best-effort.  It can incorrectly fail to inform the
+// goroutine.  It can send inform the wrong goroutine.  Even if it informs the
+// correct goroutine, that goroutine might ignore the request if it is
+// simultaneously executing runtime·newstack.
+// No lock needs to be held.
+static void
+preemptone(P *p)
+{
+	M *mp;
+	G *gp;
+
+	mp = p->m;
+	if(mp == nil || mp == m)
+		return;
+	gp = mp->curg;
+	if(gp == nil || gp == mp->g0)
+		return;
+	return;
+gp->stackguard0 = StackPreempt;
+}
+
 // Put mp on midle list.
 // Sched must be locked.
 static void

src/pkg/runtime/stack.h

--- a/src/pkg/runtime/stack.h
+++ b/src/pkg/runtime/stack.h
@@ -105,4 +105,9 @@ enum {
 	// The actual size can be smaller than this but cannot be larger.
 	// Checked in proc.c's runtime.malg.
 	StackTop = 72,
+\
+	// Goroutine preemption request.
+	// Stored into g->stackguard0 to cause split stack check failure.
+	// Must be greater than any real sp.
+	StackPreempt = (uintptr)(intptr)0xfffffade,
 };

コアとなるコードの解説

src/pkg/runtime/proc.c では、preemptallpreemptone の2つの新しい静的関数が追加されています。 preemptall は、runtime·gomaxprocs の数だけループし、各P(プロセッサ)が現在ゴルーチンを実行中(Prunningステータス)であれば、そのPに対して preemptone を呼び出します。これにより、システム内のすべての実行中のゴルーチンにプリエンプション要求を送ろうとします。

preemptone は、特定のPにアタッチされたM(OSスレッド)上で実行中のG(ゴルーチン)を見つけ、そのゴルーチンの stackguard0 フィールドに StackPreempt の値を設定します。stackguard0 は通常、スタックの境界を示すために使用される値ですが、ここに StackPreempt という特殊な値を設定することで、次にそのゴルーチンが関数呼び出しを行う際に発生するスタック検査を意図的に失敗させます。この失敗により、ランタイムのスタック拡張ルーチン(morestackなど)が呼び出され、そこでプリエンプション要求が処理され、ゴルーチンがスケジューラに制御を戻す機会が与えられます。

src/pkg/runtime/stack.h では、StackPreempt という新しい列挙定数が定義されています。この値は、g->stackguard0 に設定されることで、スタック分割チェックを失敗させるための特別なマーカーとして機能します。その値 0xfffffade は、実際のスタックポインタが取りうる値よりも常に大きくなるように選ばれています。

これらの変更は、Goランタイムがゴルーチンの実行を強制的に中断し、スケジューラに制御を戻すための低レベルなメカニズムを確立するものです。これにより、Goのスケジューラは協調的スケジューリングの制約から解放され、より公平で応答性の高いプリエンプティブスケジューリングを実現するための道が開かれました。

関連リンク

  • Goのスケジューラに関する公式ドキュメントやブログ記事(このコミットが作成された2013年頃のGoのスケジューリングモデルの進化に関する情報)
  • Goのスタック管理とmorestackに関する技術記事

参考にした情報源リンク