[インデックス 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(システムゴルーチン)といった特殊なコンテキストで利用されることを想定していました。これらのコンテキストでは、スタックの分割やスケジューリングの複雑さがユーザーゴルーチンほど問題になりません。
しかし、ユーザーゴルーチンが直接スリープ状態に入る必要がある場合、entersyscall
と exitsyscall
を適切に呼び出すことで、ランタイムがそのゴルーチンがシステムコール中であることを認識し、スケジューリングやスタック管理を適切に行う必要があります。このコミットは、この entersyscall
/exitsyscall
のペアを notetsleep
の機能と統合し、ユーザーゴルーチンから安全に呼び出せる notetsleepg
を提供することで、この問題を解決しようとしています。
具体的には、cpuprof.c
、mheap.c
、sigqueue.goc
、time.goc
といったファイルで、以前は entersyscallblock()
と notesleep()
/ notetsleep()
、そして exitsyscall()
を個別に呼び出していた箇所が、新しい notetsleepg()
の呼び出しに置き換えられています。これにより、コードの簡潔性が向上し、かつシステムコール中のスタック管理の安全性が確保されます。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念を理解しておく必要があります。
-
ゴルーチン (Goroutine): Go言語における軽量な実行単位です。OSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。Goランタイムがゴルーチンのスケジューリング、スタック管理、通信などを担当します。
-
M (Machine): OSのスレッドを表します。Goランタイムは、複数のMを管理し、その上でゴルーチンを実行します。MはOSのスケジューラによってプリエンプトされます。
-
g0 (System Goroutine): 各Mには、Goランタイム自身の処理を行うための特別なゴルーチンであるg0が関連付けられています。g0は、ユーザーゴルーチンのスケジューリング、スタックの拡大・縮小、システムコールへの移行など、ランタイムの低レベルな処理を実行します。g0のスタックは固定サイズであり、ユーザーゴルーチンのように動的に拡大・縮小することはありません。
-
スタック分割 (Split Stack): Goのゴルーチンは、必要に応じてスタックサイズを動的に変更します。関数呼び出しの際にスタックが足りなくなると、より大きなスタックを割り当てて古いスタックの内容をコピーし、新しいスタックに切り替えます。これをスタック分割と呼びます。これにより、初期スタックサイズを小さく保ち、メモリ効率を向上させることができます。
-
システムコール (System Call): プログラムがOSの機能(ファイルI/O、ネットワーク通信、メモリ割り当てなど)を利用するために、OSカーネルに処理を要求する仕組みです。システムコール中は、プログラムの実行コンテキストがユーザーモードからカーネルモードに切り替わります。
-
entersyscall
とexitsyscall
: Goランタイムが提供する内部関数で、ゴルーチンがシステムコールに入る前と出た後に呼び出されます。これらの関数は、ランタイムがゴルーチンの状態を追跡し、システムコール中のスケジューリングやガベージコレクションの動作を調整するために重要です。例えば、entersyscall
が呼び出されると、ランタイムはそのゴルーチンがシステムコール中でブロックされる可能性があることを認識し、そのMを他のゴルーチンの実行に利用できるようにします。 -
Note
(通知メカニズム): Goランタイム内部で利用される低レベルな同期プリミティブです。Note
は、あるゴルーチンが別のゴルーチンからの通知を待つために使用されます。notesleep
はNote
を使ってゴルーチンをスリープさせ、notewakeup
はスリープ中のゴルーチンをウェイクアップさせます。notetsleep
はタイムアウト付きのスリープ機能を提供します。 -
futex
(Fast Userspace Mutex): Linuxカーネルが提供する同期プリミティブで、ユーザー空間でのロックやセマフォの実装に利用されます。競合がない場合はカーネルへの移行なしにユーザー空間で処理を完結させ、競合が発生した場合のみカーネルに処理を委ねることで、効率的な同期を実現します。lock_futex.c
はこのfutex
を利用した実装を含んでいます。 -
sema
(Semaphore): セマフォは、リソースへのアクセスを制御するための同期プリミティブです。Goランタイムでは、内部的な同期メカニズムとしてセマフォが利用されることがあります。lock_sema.c
はセマフォを利用した実装を含んでいます。
技術的詳細
このコミットの核心は、notetsleepg
関数の導入とその利用です。
notetsleepg
の目的と機能:
- ユーザーゴルーチンからの呼び出し:
notetsleepg
は、ユーザーゴルーチン (user g
) から安全に呼び出されることを目的としています。従来のnotesleep
やnotetsleep
は、主にランタイム内部の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.c
とsrc/pkg/runtime/lock_sema.c
: これらのファイルは、それぞれfutex
とセマフォに基づいた低レベルなロックメカニズムの実装を含んでいます。notetsleepg
の具体的な実装が追加されています。これは、notetsleepg
がnotetsleep
を内部で呼び出し、その前後に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.c
と src/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(¬e, tick);
- runtime·exitsyscall();
// 変更後
+ runtime·notetsleepg(¬e, tick);
// 別の箇所
// 変更前
- runtime·entersyscallblock();
- runtime·notesleep(¬e);
- runtime·exitsyscall();
// 変更後
+ runtime·notetsleepg(¬e, -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;
}
この関数は、以下の重要なステップを実行します。
-
g == m->g0
のチェック:notetsleepg
はユーザーゴルーチン向けに設計されているため、g0
(システムゴルーチン) から呼び出されることを防ぎます。g0
はランタイムの内部処理に使用され、スタック管理やスケジューリングの挙動がユーザーゴルーチンとは異なるため、notetsleepg
のロジックが適用できない可能性があります。runtime·throw
はGoのパニックに相当し、プログラムを異常終了させます。 -
runtime·entersyscallblock()
: この関数は、現在のゴルーチンがシステムコールに似たブロック状態に入ろうとしていることをGoランタイムに通知します。これにより、ランタイムは以下の処理を行うことができます。- プリエンプションの無効化: システムコール中は、Goランタイムによるゴルーチンのプリエンプション(横取り)が一時的に無効になることがあります。これは、システムコール中にスタックが移動すると問題が発生する可能性があるためです。
- Mの解放: 現在のM(OSスレッド)がこのゴルーチンによってブロックされる可能性があるため、ランタイムは他の実行可能なゴルーチンをこのMに割り当てたり、新しいMを起動したりして、CPUリソースを有効活用します。
- ガベージコレクションの調整: GCは、システムコール中のゴルーチンのスタックをスキャンする際に特別な考慮が必要になる場合があります。
entersyscallblock
はGCが安全に動作するための情報を提供します。
-
runtime·notetsleep(n, ns)
: これは実際のスリープ処理を行う関数です。Note
オブジェクトn
を使用して、別のゴルーチンからの通知を待機するか、ns
で指定されたナノ秒だけスリープします。notetsleep
は、タイムアウトが発生した場合はfalse
を、通知によってウェイクアップされた場合はtrue
を返します。 -
runtime·exitsyscall()
: この関数は、現在のゴルーチンがシステムコールに似たブロック状態から抜けたことをGoランタイムに通知します。これにより、ランタイムは以下の処理を行うことができます。- プリエンプションの再有効化: 必要に応じて、ゴルーチンのプリエンプションが再び有効になります。
- Mの再利用: ゴルーチンがブロック状態から戻ったため、ランタイムは再びこのゴルーチンをM上で実行可能として扱います。
- スケジューラの調整: ランタイムは、このゴルーチンが再び実行可能になったことをスケジューラに伝え、適切なタイミングで実行を再開させます。
この notetsleepg
の導入により、Goランタイムはユーザーゴルーチンがブロックされる際に、より正確かつ安全にその状態を管理できるようになり、特にシステムコールとスタック分割に関連する複雑な問題を軽減します。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/
- Goランタイムのソースコード: https://github.com/golang/go/tree/master/src/runtime
- Goのスケジューラに関する解説記事 (例: "Go's work-stealing scheduler"): 多くの技術ブログやカンファレンス発表で詳細が解説されています。
参考にした情報源リンク
- 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のスタック分割の仕組みを理解するために参照しました。
entersyscall
とexitsyscall
の役割に関するGoランタイムのドキュメントや解説。- コミットメッセージと変更されたソースコード。