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

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

このコミットは、Goランタイムに notetsleepg という新しい関数を導入するものです。この関数は notetsleep と同様の機能を提供しますが、ユーザーゴルーチン (user g) 上で呼び出されることを想定しています。これにより、entersyscall および exitsyscall の呼び出しが内部に含まれるため、システムコール中のスタック分割関数の問題を回避するのに役立ちます。

コミット

commit e97d677b4eaa6db4d70ae1d855d2cc5a4b0fdeff
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Jul 22 23:02:27 2013 +0400

    runtime: introduce notetsleepg function
    notetsleepg is the same as notetsleep, but is called on user g.
    It includes entersyscall/exitsyscall and will help to avoid
    split stack functions in syscall status.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/11681043

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

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

元コミット内容

runtime: introduce notetsleepg function notetsleepg is the same as notetsleep, but is called on user g. It includes entersyscall/exitsyscall and will help to avoid split stack functions in syscall status.

変更の背景

この変更の主な背景は、Goランタイムにおけるゴルーチンのスケジューリングとシステムコール中のスタック管理の改善にあります。特に、システムコール中にスタックが分割されることによって発生する可能性のある問題を回避することが目的です。

Goランタイムでは、ゴルーチンは非常に軽量なスレッドのようなものであり、そのスタックは必要に応じて動的に拡大・縮小します(スプリットスタック)。しかし、システムコール(syscall)を実行する際には、通常のGoコードの実行とは異なるコンテキストに入ります。システムコール中は、Goランタイムのスケジューラがゴルーチンをプリエンプト(横取り)したり、スタックを操作したりすることが難しくなります。

既存の notetsleep 関数は、主にランタイム内部のM(マシン、OSスレッド)やg0(システムゴルーチン)といった特殊なコンテキストで利用されることを想定していました。これらのコンテキストでは、スタックの分割やスケジューリングの複雑さがユーザーゴルーチンほど問題になりません。

しかし、ユーザーゴルーチンが直接スリープ状態に入る必要がある場合、entersyscallexitsyscall を適切に呼び出すことで、ランタイムがそのゴルーチンがシステムコール中であることを認識し、スケジューリングやスタック管理を適切に行う必要があります。このコミットは、この entersyscall/exitsyscall のペアを notetsleep の機能と統合し、ユーザーゴルーチンから安全に呼び出せる notetsleepg を提供することで、この問題を解決しようとしています。

具体的には、cpuprof.cmheap.csigqueue.goctime.goc といったファイルで、以前は entersyscallblock()notesleep() / notetsleep()、そして exitsyscall() を個別に呼び出していた箇所が、新しい notetsleepg() の呼び出しに置き換えられています。これにより、コードの簡潔性が向上し、かつシステムコール中のスタック管理の安全性が確保されます。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念を理解しておく必要があります。

  1. ゴルーチン (Goroutine): Go言語における軽量な実行単位です。OSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。Goランタイムがゴルーチンのスケジューリング、スタック管理、通信などを担当します。

  2. M (Machine): OSのスレッドを表します。Goランタイムは、複数のMを管理し、その上でゴルーチンを実行します。MはOSのスケジューラによってプリエンプトされます。

  3. g0 (System Goroutine): 各Mには、Goランタイム自身の処理を行うための特別なゴルーチンであるg0が関連付けられています。g0は、ユーザーゴルーチンのスケジューリング、スタックの拡大・縮小、システムコールへの移行など、ランタイムの低レベルな処理を実行します。g0のスタックは固定サイズであり、ユーザーゴルーチンのように動的に拡大・縮小することはありません。

  4. スタック分割 (Split Stack): Goのゴルーチンは、必要に応じてスタックサイズを動的に変更します。関数呼び出しの際にスタックが足りなくなると、より大きなスタックを割り当てて古いスタックの内容をコピーし、新しいスタックに切り替えます。これをスタック分割と呼びます。これにより、初期スタックサイズを小さく保ち、メモリ効率を向上させることができます。

  5. システムコール (System Call): プログラムがOSの機能(ファイルI/O、ネットワーク通信、メモリ割り当てなど)を利用するために、OSカーネルに処理を要求する仕組みです。システムコール中は、プログラムの実行コンテキストがユーザーモードからカーネルモードに切り替わります。

  6. entersyscallexitsyscall: Goランタイムが提供する内部関数で、ゴルーチンがシステムコールに入る前と出た後に呼び出されます。これらの関数は、ランタイムがゴルーチンの状態を追跡し、システムコール中のスケジューリングやガベージコレクションの動作を調整するために重要です。例えば、entersyscall が呼び出されると、ランタイムはそのゴルーチンがシステムコール中でブロックされる可能性があることを認識し、そのMを他のゴルーチンの実行に利用できるようにします。

  7. Note (通知メカニズム): Goランタイム内部で利用される低レベルな同期プリミティブです。Note は、あるゴルーチンが別のゴルーチンからの通知を待つために使用されます。notesleepNote を使ってゴルーチンをスリープさせ、notewakeup はスリープ中のゴルーチンをウェイクアップさせます。notetsleep はタイムアウト付きのスリープ機能を提供します。

  8. futex (Fast Userspace Mutex): Linuxカーネルが提供する同期プリミティブで、ユーザー空間でのロックやセマフォの実装に利用されます。競合がない場合はカーネルへの移行なしにユーザー空間で処理を完結させ、競合が発生した場合のみカーネルに処理を委ねることで、効率的な同期を実現します。lock_futex.c はこの futex を利用した実装を含んでいます。

  9. sema (Semaphore): セマフォは、リソースへのアクセスを制御するための同期プリミティブです。Goランタイムでは、内部的な同期メカニズムとしてセマフォが利用されることがあります。lock_sema.c はセマフォを利用した実装を含んでいます。

技術的詳細

このコミットの核心は、notetsleepg 関数の導入とその利用です。

notetsleepg の目的と機能:

  • ユーザーゴルーチンからの呼び出し: notetsleepg は、ユーザーゴルーチン (user g) から安全に呼び出されることを目的としています。従来の notesleepnotetsleep は、主にランタイム内部のg0ゴルーチンやMのコンテキストで利用されていました。
  • entersyscall/exitsyscall の内包: notetsleepg の内部では、entersyscallblock()exitsyscall() が呼び出されます。これにより、notetsleepg を呼び出すユーザーゴルーチンは、ランタイムに対して「これからシステムコールに似たブロック操作に入る」ことを明示的に通知し、その後「ブロック操作から戻った」ことを通知します。
  • システムコール中のスタック分割問題の回避: システムコール中にスタック分割が発生すると、Goランタイムのスタック管理メカニズムが正しく機能しない可能性があります。特に、システムコール中にスタックが移動すると、カーネルが期待するスタックポインタとGoランタイムが認識するスタックポインタの間に不整合が生じ、クラッシュや予期せぬ動作を引き起こす可能性があります。entersyscallblock() は、ゴルーチンがシステムコールのようなブロック状態に入ることをランタイムに伝え、ランタイムがそのゴルーチンのスタックを固定したり、他のゴルーチンにMを譲ったりするなどの適切な処理を行うことを可能にします。これにより、システムコール中のスタック分割による問題を回避します。
  • コードの簡潔化: 以前は entersyscallblock(), notesleep()/notetsleep(), exitsyscall() の3つの関数呼び出しが必要だった箇所が、notetsleepg() の1つの呼び出しに集約されます。これにより、コードがより簡潔になり、可読性と保守性が向上します。
  • g0での呼び出し禁止: notetsleepg はユーザーゴルーチン向けに設計されているため、g0ゴルーチンから呼び出された場合には runtime·throw("notetsleepg on g0") によってパニックを引き起こすようになっています。これは、g0が固定スタックを持ち、entersyscall/exitsyscall の管理がユーザーゴルーチンとは異なるためです。

影響を受けるファイルと変更点:

  • src/pkg/runtime/cpuprof.c: CPUプロファイリングのロジックで、プロファイルログの待機中に notesleep を使用していた箇所が notetsleepg に変更されています。これにより、プロファイリング中のゴルーチンの状態遷移がより正確にランタイムに伝わるようになります。
  • src/pkg/runtime/lock_futex.csrc/pkg/runtime/lock_sema.c: これらのファイルは、それぞれ futex とセマフォに基づいた低レベルなロックメカニズムの実装を含んでいます。notetsleepg の具体的な実装が追加されています。これは、notetsleepgnotetsleep を内部で呼び出し、その前後に entersyscallblock()exitsyscall() を配置するラッパー関数であることを示しています。
  • src/pkg/runtime/mheap.c: メモリヒープのスカベンジャー(ガベージコレクションの一部)のロジックで、スリープを伴う処理が notetsleepg に変更されています。これにより、GC関連の内部処理がシステムコール状態を適切に管理できるようになります。
  • src/pkg/runtime/runtime.h: notetsleepg 関数のプロトタイプ宣言が追加されています。コメントも更新され、notesleep/notetsleep がg0で呼び出されることが多いのに対し、notetsleepg はユーザーゴルーチンで呼び出されることが明記されています。
  • src/pkg/runtime/sigqueue.goc: シグナルキューの処理で、シグナルを待機するために notesleep を使用していた箇所が notetsleepg に変更されています。これにより、シグナルハンドリング中のゴルーチンの状態が適切に管理されます。
  • src/pkg/runtime/time.goc: タイマー処理のロジックで、タイマーイベントを待機するために notetsleep を使用していた箇所が notetsleepg に変更されています。これにより、タイマーゴルーチンのスリープがシステムコール状態を適切に管理できるようになります。

この変更は、Goランタイムの内部的な堅牢性と正確性を向上させるものであり、特にシステムコールとゴルーチンのスケジューリングが密接に関連する部分において、潜在的なバグやパフォーマンスの問題を回避するのに役立ちます。

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

このコミットのコアとなる変更は、src/pkg/runtime/lock_futex.csrc/pkg/runtime/lock_sema.c に追加された runtime·notetsleepg 関数の実装、および既存のコードベースで entersyscallblock/notesleep/notetsleep/exitsyscall の組み合わせが notetsleepg に置き換えられた点です。

src/pkg/runtime/lock_futex.c および src/pkg/runtime/lock_sema.c における notetsleepg の追加:

// src/pkg/runtime/lock_futex.c (同様のコードが src/pkg/runtime/lock_sema.c にも追加)
+bool
+runtime·notetsleepg(Note *n, int64 ns)
+{
+	bool res;
+
+	if(g == m->g0)
+		runtime·throw("notetsleepg on g0");
+	runtime·entersyscallblock();
+	res = runtime·notetsleep(n, ns);
+	runtime·exitsyscall();
+	return res;
+}

src/pkg/runtime/cpuprof.c における変更例:

// 変更前
-	runtime·entersyscallblock();
-	runtime·notesleep(&p->wait);
-	runtime·exitsyscall();
// 変更後
+	runtime·notetsleepg(&p->wait, -1);

src/pkg/runtime/mheap.c における変更例:

// 変更前
-		runtime·entersyscallblock();
-		runtime·notetsleep(&note, tick);
-		runtime·exitsyscall();
// 変更後
+		runtime·notetsleepg(&note, tick);

// 別の箇所
// 変更前
-			runtime·entersyscallblock();
-			runtime·notesleep(&note);
-			runtime·exitsyscall();
// 変更後
+			runtime·notetsleepg(&note, -1);

src/pkg/runtime/runtime.h における宣言の追加:

// 変更前 (抜粋)
// bool	runtime·notetsleep(Note*, int64);  // false - timeout
// 変更後 (抜粋)
// bool	runtime·notetsleep(Note*, int64);  // false - timeout
+bool	runtime·notetsleepg(Note*, int64);  // false - timeout

コアとなるコードの解説

runtime·notetsleepg 関数の実装は非常にシンプルですが、その背後にある意図はGoランタイムの堅牢性を高める上で重要です。

bool
runtime·notetsleepg(Note *n, int64 ns)
{
	bool res; // notetsleepの結果を格納する変数

	// ユーザーゴルーチン (g) 上で実行されていることを確認
	// もしg0 (システムゴルーチン) 上で呼び出された場合はパニック
	if(g == m->g0)
		runtime·throw("notetsleepg on g0");

	// システムコールブロック状態に入ることをランタイムに通知
	// これにより、ランタイムはこのゴルーチンがブロックされる可能性があることを認識し、
	// スケジューリングやスタック管理を適切に行う準備をする
	runtime·entersyscallblock();

	// 実際のタイムアウト付きスリープ処理を実行
	// ここでゴルーチンは指定されたNote (n) を待機するか、nsで指定された時間だけスリープする
	res = runtime·notetsleep(n, ns);

	// システムコールブロック状態から抜けることをランタイムに通知
	// これにより、ランタイムはゴルーチンが通常の実行状態に戻ったことを認識し、
	// 必要に応じてスケジューリングを再開する
	runtime·exitsyscall();

	// notetsleepの結果 (タイムアウトしたかどうかなど) を返す
	return res;
}

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

  1. g == m->g0 のチェック: notetsleepg はユーザーゴルーチン向けに設計されているため、g0 (システムゴルーチン) から呼び出されることを防ぎます。g0 はランタイムの内部処理に使用され、スタック管理やスケジューリングの挙動がユーザーゴルーチンとは異なるため、notetsleepg のロジックが適用できない可能性があります。runtime·throw はGoのパニックに相当し、プログラムを異常終了させます。

  2. runtime·entersyscallblock(): この関数は、現在のゴルーチンがシステムコールに似たブロック状態に入ろうとしていることをGoランタイムに通知します。これにより、ランタイムは以下の処理を行うことができます。

    • プリエンプションの無効化: システムコール中は、Goランタイムによるゴルーチンのプリエンプション(横取り)が一時的に無効になることがあります。これは、システムコール中にスタックが移動すると問題が発生する可能性があるためです。
    • Mの解放: 現在のM(OSスレッド)がこのゴルーチンによってブロックされる可能性があるため、ランタイムは他の実行可能なゴルーチンをこのMに割り当てたり、新しいMを起動したりして、CPUリソースを有効活用します。
    • ガベージコレクションの調整: GCは、システムコール中のゴルーチンのスタックをスキャンする際に特別な考慮が必要になる場合があります。entersyscallblock はGCが安全に動作するための情報を提供します。
  3. runtime·notetsleep(n, ns): これは実際のスリープ処理を行う関数です。Note オブジェクト n を使用して、別のゴルーチンからの通知を待機するか、ns で指定されたナノ秒だけスリープします。notetsleep は、タイムアウトが発生した場合は false を、通知によってウェイクアップされた場合は true を返します。

  4. runtime·exitsyscall(): この関数は、現在のゴルーチンがシステムコールに似たブロック状態から抜けたことをGoランタイムに通知します。これにより、ランタイムは以下の処理を行うことができます。

    • プリエンプションの再有効化: 必要に応じて、ゴルーチンのプリエンプションが再び有効になります。
    • Mの再利用: ゴルーチンがブロック状態から戻ったため、ランタイムは再びこのゴルーチンをM上で実行可能として扱います。
    • スケジューラの調整: ランタイムは、このゴルーチンが再び実行可能になったことをスケジューラに伝え、適切なタイミングで実行を再開させます。

この notetsleepg の導入により、Goランタイムはユーザーゴルーチンがブロックされる際に、より正確かつ安全にその状態を管理できるようになり、特にシステムコールとスタック分割に関連する複雑な問題を軽減します。

関連リンク

参考にした情報源リンク

  • Goのコミット履歴: https://github.com/golang/go/commits/master
  • GoのIssueトラッカー (関連するCLやIssue): https://go.dev/issue
  • Goの内部実装に関する技術ブログやドキュメント (例: "Go scheduler: M, P, G"): Goのランタイムに関する深い理解を得るために参照しました。
  • futex およびセマフォに関する一般的なOSの同期プリミティブの知識。
  • Goのスタック管理に関する情報 (例: "Go's stack management"): Goのスタック分割の仕組みを理解するために参照しました。
  • entersyscallexitsyscall の役割に関するGoランタイムのドキュメントや解説。
  • コミットメッセージと変更されたソースコード。