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

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

このコミットは、Goランタイムに entersyscallblock() という新しい関数を導入するものです。これは、将来的に導入される新しいスケジューラのための準備として行われました。既存の entersyscall() 関数と同様の機能を提供しますが、システムコールがブロッキング操作であることを示すヒントを含んでいます。

コミット

commit e25f19a638835d129545a82e559c2fb621b48e0c
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Feb 20 20:21:45 2013 +0400

    runtime: introduce entersyscallblock()
    In preparation for the new scheduler.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/7386044

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

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

元コミット内容

    runtime: introduce entersyscallblock()
    In preparation for the new scheduler.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/7386044

変更の背景

このコミットの主な背景は、Goランタイムにおける新しいスケジューラの導入準備です。Goのランタイムスケジューラは、ゴルーチン(Goの軽量スレッド)の実行を管理し、OSのスレッド(M: Machine)へのマッピングを決定します。従来のスケジューラは、システムコール(特にブロッキングシステムコール)がゴルーチンの実行に与える影響を効率的に処理するための改善の余地がありました。

システムコールは、プログラムがOSのサービス(ファイルI/O、ネットワーク通信など)を要求する際に発生します。これらのシステムコールの中には、完了するまでに時間がかかり、その間ゴルーチンがブロックされる「ブロッキングシステムコール」があります。既存の entersyscall() 関数は、ゴルーチンがシステムコールに入ることをランタイムに通知しますが、そのシステムコールがブロッキングであるかどうかの情報は明示的に伝えていませんでした。

新しいスケジューラは、ブロッキングシステムコールをより効率的に扱うことで、全体の並行性とスループットを向上させることを目指していました。具体的には、ゴルーチンがブロッキングシステムコールに入った際に、そのゴルーチンが実行されていたOSスレッドを他の実行可能なゴルーチンに解放し、システムコールが完了した際にそのゴルーチンを別のOSスレッドで再開するといった最適化が考えられます。entersyscallblock() の導入は、このようなスケジューラの振る舞いを可能にするための第一歩として、ブロッキングシステムコールであることをランタイムに明示的に伝えるためのメカニズムを提供します。

前提知識の解説

Goランタイムスケジューラ (GMPモデル)

Goのランタイムスケジューラは、G-M-Pモデルとして知られる独自のスケジューリングモデルを採用しています。

  • G (Goroutine): Goにおける並行実行の単位。非常に軽量で、数百万個作成することも可能です。
  • M (Machine/OS Thread): オペレーティングシステムのスレッド。Goランタイムは、OSスレッド上でゴルーチンを実行します。
  • P (Processor/Logical Processor): 論理プロセッサ。MとGの間に位置し、MがGを実行するためのコンテキストを提供します。Pは実行可能なゴルーチンのキューを保持し、MはPからゴルーチンを取得して実行します。

このモデルの目的は、OSスケジューラに依存しすぎることなく、Goランタイム自身がゴルーチンを効率的にスケジューリングすることです。

システムコールとブロッキングシステムコール

システムコールは、ユーザー空間のプログラムがカーネル空間の機能を利用するためのインターフェースです。例えば、ファイルを開く、データを読み書きする、ネットワーク接続を確立するといった操作はシステムコールを介して行われます。

システムコールには、大きく分けて以下の2種類があります。

  • ノンブロッキングシステムコール: ほとんどすぐに完了し、呼び出し元の実行を中断しないシステムコール。
  • ブロッキングシステムコール: 完了するまでに時間がかかり、その間呼び出し元の実行を一時停止させるシステムコール。例えば、ディスクI/OやネットワークI/Oなど、外部リソースからの応答を待つ必要がある場合がこれに該当します。

Goランタイムは、ゴルーチンがブロッキングシステムコールに入った際に、そのゴルーチンが実行されていたMをPから切り離し、Pを他のMに割り当てることで、他のゴルーチンがP上で実行を継続できるようにします。これにより、ブロッキングシステムコールによってプログラム全体の並行性が損なわれるのを防ぎます。

entersyscall()exitsyscall()

Goランタイムには、ゴルーチンがシステムコールに入る直前に呼び出される entersyscall() と、システムコールから戻った直後に呼び出される exitsyscall() という関数が存在します。これらの関数は、ランタイムがゴルーチンの状態を追跡し、スケジューリングの決定を行うために使用されます。

  • entersyscall(): ゴルーチンがシステムコールに入ろうとしていることをランタイムに通知します。これにより、ランタイムは現在のゴルーチンを Gsyscall 状態に設定し、必要に応じて現在のMをPから切り離す準備をします。
  • exitsyscall(): ゴルーチンがシステムコールから戻ったことをランタイムに通知します。これにより、ランタイムはゴルーチンを Grunnable 状態に戻し、Pに再割り当てして実行キューに戻す準備をします。

技術的詳細

このコミットでは、runtime·entersyscallblock() という新しい関数が導入されています。コミットメッセージにもあるように、この関数は「新しいスケジューラのための準備」であり、システムコールがブロッキング操作であることを示すヒントを提供することを目的としています。

初期の実装では、runtime·entersyscallblock()runtime·entersyscall() の単なるコピーです。これは、新しいスケジューラがまだ完全に実装されていないため、現時点ではブロッキングヒントが無視されることを意味します。しかし、この関数の導入により、将来的にスケジューラがブロッキングシステムコールを特別に扱うためのフックが提供されます。

runtime·entersyscallblock() の内部では、以下の処理が行われます(runtime·entersyscall() と同じロジック):

  1. プロファイリングの停止: m->profilehz > 0 の場合、CPUプロファイリングを一時的に停止します。システムコール中はユーザーコードが実行されていないため、プロファイリングは意味がありません。
  2. ゴルーチンの状態保存: 現在のゴルーチン g のスケジューラ関連の状態(スタックポインタ sp、スタックベース stackbase、スタックガード stackguard)を g->sched に保存します。これは、システムコール中にGC(ガベージコレクション)やトレースバックが必要になった場合に備えるためです。
  3. ゴルーチン状態の変更: ゴルーチンの状態を Gsyscall に設定します。これは、ゴルーチンがシステムコール中であることを示します。
  4. スタックの一貫性チェック: 保存されたスタック情報が有効であるかどうかの基本的なチェックを行います。
  5. 高速パス (Fast path): runtime·sched.atomic というアトミック変数を使って、スケジューラの状態をチェックし、ロックなしでシステムコールに入れるかどうかを判断します。これは、スケジューラが停止状態でない場合や、利用可能なMが十分にある場合に、ロックのオーバーヘッドを避けるための最適化です。
  6. 低速パス (Slow path): 高速パスが失敗した場合、schedlock() を取得してスケジューラをロックし、より詳細なチェックと調整を行います。
    • atomic_gwaiting(v) が真の場合、matchmg() を呼び出して、待機中のMとPをマッチングさせます。
    • atomic_waitstop(v) が真で、かつ atomic_mcpu(v) <= atomic_mcpumax(v) の場合、スケジューラが停止状態にあり、かつ利用可能なMが少ないため、runtime·sched.atomicwaitstopShift ビットをクリアし、runtime·sched.stopped ノートをウェイクアップします。これは、スケジューラが停止状態から復帰するのを助けるためです。
  7. ゴルーチンの状態再保存: notewakeupmatchmg の呼び出しによって g->sched が変更された可能性があるため、再度 runtime·gosave(&g->sched) を呼び出して状態を保存します。
  8. スケジューラのアンロック: schedunlock() を呼び出してスケジューラのロックを解放します。

この entersyscallblock() の導入により、将来的にGoランタイムは、ブロッキングシステムコールに入ったゴルーチンをよりインテリジェントに処理できるようになります。例えば、ブロッキングシステムコールに入ったゴルーチンが実行されていたMをPから切り離し、そのPを他の実行可能なゴルーチンに割り当てることで、CPUリソースの有効活用が可能になります。

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

このコミットでは、主に以下のファイルが変更されています。

  • src/pkg/runtime/cpuprof.c
  • src/pkg/runtime/mheap.c
  • src/pkg/runtime/proc.c
  • src/pkg/runtime/runtime.h
  • src/pkg/runtime/sigqueue.goc
  • src/pkg/runtime/time.goc

これらのファイルにおいて、既存の runtime·entersyscall() の呼び出しが runtime·entersyscallblock() に置き換えられています。

src/pkg/runtime/proc.c

このファイルでは、runtime·entersyscallblock() 関数の実装が追加されています。

// The same as runtime·entersyscall(), but with a hint that the syscall is blocking.
// The hint is ignored at the moment, and it's just a copy of runtime·entersyscall().
#pragma textflag 7
void
runtime·entersyscallblock(void)
{
	uint32 v;

	if(m->profilehz > 0)
		runtime·setprof(false);

	// Leave SP around for gc and traceback.
	runtime·gosave(&g->sched);
	g->gcsp = g->sched.sp;
	g->gcstack = g->stackbase;
	g->gcguard = g->stackguard;
	g->status = Gsyscall;
	if(g->gcsp < g->gcguard-StackGuard || g->gcstack < g->gcsp) {
		// runtime·printf("entersyscall inconsistent %p [%p,%p]\\n",
		//	g->gcsp, g->gcguard-StackGuard, g->gcstack);
		runtime·throw("entersyscall");
	}

	// Fast path.
	// The slow path inside the schedlock/schedunlock will get
	// through without stopping if it does:
	//	mcpu--
	//	gwait not true
	//	waitstop && mcpu <= mcpumax not true
	// If we can do the same with a single atomic add,
	// then we can skip the locks.
	v = runtime·xadd(&runtime·sched.atomic, -1<<mcpuShift);
	if(!atomic_gwaiting(v) && (!atomic_waitstop(v) || atomic_mcpu(v) > atomic_mcpumax(v)))
		return;

	schedlock();
	v = runtime·atomicload(&runtime·sched.atomic);
	if(atomic_gwaiting(v)) {
		matchmg();
		v = runtime·atomicload(&runtime·sched.atomic);
	}
	if(atomic_waitstop(v) && atomic_mcpu(v) <= atomic_mcpumax(v)) {
		runtime·xadd(&runtime·sched.atomic, -1<<waitstopShift);
		runtime·notewakeup(&runtime·sched.stopped);
	}

	// Re-save sched in case one of the calls
	// (notewakeup, matchmg) triggered something using it.
	runtime·gosave(&g->sched);

	schedunlock();
}

src/pkg/runtime/runtime.h

このファイルでは、runtime·entersyscallblock() の関数プロトタイプが追加されています。

void	runtime·entersyscall(void);
void	runtime·entersyscallblock(void); // <-- 追加
void	runtime·exitsyscall(void);

その他のファイル (cpuprof.c, mheap.c, sigqueue.goc, time.goc)

これらのファイルでは、ブロッキング操作を伴う可能性のある箇所で runtime·entersyscall() の呼び出しが runtime·entersyscallblock() に変更されています。

  • src/pkg/runtime/cpuprof.c: getprofile 関数内で、新しいログを待つ際に runtime·entersyscallblock() が使用されます。
  • src/pkg/runtime/mheap.c: runtime·MHeap_Scavenger 関数内で、ヒープのスカベンジング(メモリ解放)処理中に runtime·entersyscallblock() が使用されます。
  • src/pkg/runtime/sigqueue.goc: signal_recv 関数内で、シグナルを待機する際に runtime·entersyscallblock() が使用されます。
  • src/pkg/runtime/time.goc: timerproc 関数内で、タイマーの待機中に runtime·entersyscallblock() が使用されます。

これらの変更は、それぞれの処理がブロッキング操作(待機、スリープなど)を伴うため、新しいスケジューラがこれらの状況をより適切に処理できるようにするための準備です。

コアとなるコードの解説

このコミットのコアとなる変更は、runtime·entersyscallblock() 関数の導入とその呼び出し箇所の変更です。

runtime·entersyscallblock() は、runtime·entersyscall() と全く同じ実装を持っていますが、その名前が示すように、システムコールがブロッキング操作であることをランタイムに「ヒント」として与えることを意図しています。

このヒントは、将来のGoランタイムスケジューラの改善において重要な役割を果たすことになります。例えば、以下のようなシナリオが考えられます。

  1. Mの解放: ゴルーチンが entersyscallblock() を呼び出してブロッキングシステムコールに入ると、ランタイムは現在のM(OSスレッド)がこのゴルーチンによってブロックされることを認識します。このMは、P(論理プロセッサ)から切り離され、Pは他の実行可能なゴルーチンに割り当てられた別のMに引き継がれることができます。これにより、ブロッキングシステムコール中でもCPUリソースがアイドル状態になるのを防ぎ、他のゴルーチンの実行を継続できます。
  2. システムコール完了後の再スケジューリング: システムコールが完了し、ゴルーチンが exitsyscall() を呼び出してランタイムに戻ると、ランタイムはゴルーチンを再度実行可能な状態にし、利用可能なPに割り当てて実行キューに戻します。この際、ブロッキングシステムコールであったという情報があれば、スケジューラはより効率的な再スケジューリング戦略を選択できる可能性があります。

現時点ではヒントが無視されるとはいえ、この変更はGoランタイムの進化における重要なマイルストーンであり、より高性能で効率的な並行処理を実現するための基盤を築くものです。

関連リンク

  • Goのスケジューラに関する公式ドキュメントやブログ記事:

参考にした情報源リンク