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

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

このコミットは、Goランタイムにおけるプリエンプション(preemption)機能を一時的に無効化するものです。具体的には、Linuxビルドで発生していたテストの失敗(runtime.deferreturnからの不明な引数フレームサイズ、およびinvalid stackエラー)を修正するために、src/pkg/runtime/proc.c内のプリエンプション関連コードの冒頭にif(1) return;を追加し、それ以降のプリエンプション処理が実行されないようにしています。これは、当時のトレースバックルーチンがプリエンプションの要件を満たしていなかったための一時的な措置です。

コミット

1da96a30395b20cb4e2059c49bee05540e36aac6

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

https://github.com/golang/go/commit/1da96a30395b20cb4e2059c49bee05540e36aac6

元コミット内容

runtime: disable preemption again to fix linux build

Otherwise the tests in pkg/runtime fail:

runtime: unknown argument frame size for runtime.deferreturn called from 0x48657b [runtime_test.func·022]
fatal error: invalid stack
...

R=golang-dev, dave
CC=golang-dev
https://golang.org/cl/11483043

変更の背景

このコミットの背景には、Goランタイムにおけるプリエンプション機能の実装と、それに伴う安定性の問題がありました。Goのランタイムは、複数のゴルーチン(goroutine)を効率的にスケジューリングするために、プリエンプションの仕組みを持っています。プリエンプションとは、実行中のゴルーチンを強制的に中断し、別のゴルーチンにCPUを割り当てることで、協調的ではない(non-cooperative)スケジューリングを実現する技術です。これにより、特定のゴルーチンがCPUを占有し続けることを防ぎ、システム全体の応答性を向上させます。

しかし、このコミットが作成された2013年7月時点では、Goランタイムのプリエンプション機能はまだ開発途上にあり、特にスタック管理やトレースバックのメカニズムが十分に堅牢ではありませんでした。コミットメッセージに明記されているように、プリエンプションが有効な状態では、pkg/runtime内のテストが失敗していました。具体的には、runtime: unknown argument frame size for runtime.deferreturnというエラーと、それに続くfatal error: invalid stackという致命的なエラーが発生していました。

これらのエラーは、プリエンプションによってゴルーチンの実行が中断された際に、スタックフレームの情報が正しく保存・復元されなかったり、defer関数の呼び出し(runtime.deferreturn)に必要なスタック情報が破損したりしたことを示唆しています。このような状況では、プログラムの安定性が損なわれるため、一時的な措置としてプリエンプションを無効化する必要がありました。コミットメッセージの「disable preemption again」という表現から、以前にも同様の問題が発生し、プリエンプションを無効化した経緯があったことが伺えます。

前提知識の解説

Goランタイム (Go Runtime)

Goランタイムは、Goプログラムの実行を管理する非常に重要なコンポーネントです。これには、ガベージコレクション(GC)、ゴルーチン(goroutine)のスケジューリング、チャネル(channel)の管理、メモリ割り当て、システムコールインターフェースなどが含まれます。Goプログラムは、OSのプロセスとして実行されますが、その内部で多数の軽量なゴルーチンを並行して実行し、GoランタイムがこれらのゴルーチンをOSのスレッドにマッピングしてスケジューリングします。

プリエンプション (Preemption)

プリエンプションとは、オペレーティングシステムやランタイムが、現在実行中のタスク(この場合はゴルーチン)を強制的に中断し、別のタスクにCPUの実行権を割り当てるメカニズムです。これにより、特定のタスクがCPUを独占するのを防ぎ、システム全体の公平性や応答性を保証します。

Goにおけるプリエンプションは、特にCPUバウンドなゴルーチンが長時間実行され、他のゴルーチンの実行を妨げる「飢餓(starvation)」状態を防ぐために重要です。初期のGoランタイムは協調的スケジューリング(cooperative scheduling)に依存しており、ゴルーチンが自発的に実行を中断する(例えば、チャネル操作やシステムコール待ちなど)までCPUを占有することができました。しかし、純粋な計算処理を行うゴルーチンは自発的に中断しないため、他のゴルーチンが実行機会を得られない問題がありました。これを解決するために、Goランタイムはタイムスライスベースのプリエンプションを導入しました。

defer文とruntime.deferreturn

Goのdefer文は、関数がリターンする直前に実行される関数呼び出しをスケジュールするために使用されます。これは、リソースの解放(ファイルのクローズ、ロックの解除など)を確実に行うために非常に便利です。

defer文がコンパイルされると、Goコンパイラはdeferされた関数呼び出しを、ランタイムが提供するruntime.deferprocという関数に変換します。そして、関数がリターンする際には、runtime.deferreturnという関数が呼び出され、スケジュールされたdefer関数が実行されます。

runtime.deferreturnが正しく機能するためには、スタックフレームの情報(どの関数がどこから呼び出されたか、引数は何かなど)が正確である必要があります。プリエンプションによってゴルーチンの実行が中断され、スタックの状態が不整合になると、deferreturnがスタック情報を正しく解釈できなくなり、「unknown argument frame size」や「invalid stack」といったエラーが発生する可能性があります。

スタック管理とトレースバック

Goのゴルーチンは、比較的小さなスタック(通常は数KBから始まり、必要に応じて動的に拡張・縮小される)を持っています。スタックは、関数のローカル変数、引数、リターンアドレスなどを格納するために使用されます。

トレースバックとは、プログラムがクラッシュしたり、特定のイベントが発生したりした際に、現在の実行パス(どの関数がどの関数を呼び出したか)をスタック情報から逆順に辿って表示するプロセスです。デバッグやエラー解析において非常に重要な情報となります。

プリエンプションが導入されると、ゴルーチンは任意の時点で中断される可能性があります。このとき、スタックの状態が常に一貫しており、中断された時点からでも正確なトレースバックが可能である必要があります。もしスタックが不整合な状態でプリエンプションが発生すると、トレースバックが不可能になったり、誤った情報を提供したりする可能性があります。このコミットの背景にある問題は、まさにプリエンプション時のスタックの整合性とトレースバックの堅牢性に関するものでした。

src/pkg/runtime/proc.c

src/pkg/runtime/proc.cは、GoランタイムのC言語部分で、主にプロセスの管理、ゴルーチンのスケジューリング、M(Machine)とP(Processor)の管理など、Goの並行処理モデルの核心部分を実装しています。プリエンプションのロジックもこのファイルに含まれています。

技術的詳細

このコミットは、Goランタイムのプリエンプションメカニズムが、当時のスタック管理およびトレースバックルーチンの能力を超えていたことを示しています。

Goランタイムのプリエンプションは、通常、タイマー割り込みやシステムコールからの復帰時など、特定のタイミングでトリガーされます。プリエンプションがトリガーされると、ランタイムは現在実行中のゴルーチンのコンテキスト(レジスタの状態、プログラムカウンタ、スタックポインタなど)を保存し、そのゴルーチンを「実行可能(runnable)」状態に戻して、別のゴルーチンにCPUを割り当てます。

問題は、このコンテキストの保存と復元、特にスタックの状態の保存と、その後のトレースバックの正確性に関するものでした。コミットメッセージにあるunknown argument frame size for runtime.deferreturnというエラーは、defer呼び出しの処理中に、スタック上の引数フレームのサイズがランタイムにとって未知または不正な状態であったことを意味します。これは、プリエンプションによってスタックが中途半端な状態で中断され、deferreturnが期待するスタックレイアウトと実際のスタックレイアウトが一致しなくなったために発生したと考えられます。

また、fatal error: invalid stackは、スタック自体が破損しているか、ランタイムがスタックとして認識できない状態になっていることを示唆しています。これは非常に深刻なエラーであり、プログラムのクラッシュに直結します。

コミットの変更は、proc.c内のpreemptone関数(単一のPに対するプリエンプションを試みる関数)の冒頭に、無条件でリターンするif(1) return;を追加することで、この関数がそれ以降のプリエンプションロジックを実行しないようにしています。これにより、プリエンプションが完全に無効化され、上記のエラーが発生しなくなります。

コメントにある// Preemption requires more robust traceback routines.という記述は、この問題の根本原因が、プリエンプションが任意の時点で発生してもスタックの状態を正確に把握し、トレースバックできるような堅牢なルーチンがまだ存在しなかったことにあると明確に述べています。// For now, disable.は、これが一時的な回避策であることを示しています。

// The if(1) silences a compiler warning about the rest of the // function being unreachable.というコメントは、if(1) return;というコードが、その後のコードを常に到達不能にするため、コンパイラが「到達不能なコード」に関する警告を出すのを防ぐための慣用的な記述であることを説明しています。これは、開発者が意図的にコードを無効化していることをコンパイラに伝えるためのものです。

このコミットは、Goランタイムの進化の過程で、並行処理の複雑な側面(特にプリエンプションとスタック管理の相互作用)を安定させるための試行錯誤があったことを示しています。プリエンプションはGoの並行処理モデルにおいて非常に重要な機能ですが、その実装はスタックの動的な性質やガベージコレクションとの連携など、多くの技術的課題を伴います。

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

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -2175,6 +2175,12 @@ preemptone(P *p)
 	M *mp;
 	G *gp;
 
+// Preemption requires more robust traceback routines.
+// For now, disable.
+// The if(1) silences a compiler warning about the rest of the
+// function being unreachable.
+if(1) return;
+
 	mp = p->m;
 	if(mp == nil || mp == m)
 		return;

コアとなるコードの解説

変更はsrc/pkg/runtime/proc.cファイルのpreemptone関数内で行われています。

preemptone関数は、特定のプロセッサ(P)に紐付けられたM(OSスレッド)上で実行されているゴルーチンに対して、プリエンプションを試みる役割を担っています。

追加された6行は以下の通りです。

// Preemption requires more robust traceback routines.
// For now, disable.
// The if(1) silences a compiler warning about the rest of the
// function being unreachable.
if(1) return;
  1. // Preemption requires more robust traceback routines.

    • このコメントは、プリエンプション機能が正しく動作するためには、より堅牢なトレースバックルーチンが必要であることを明確に述べています。これは、プリエンプションによってゴルーチンが任意の時点で中断されるため、その時点でのスタックの状態を正確に把握し、デバッグ情報やエラーレポートのためにトレースバックを生成できる能力が不可欠であることを示唆しています。当時のGoランタイムのトレースバック機能は、プリエンプションが導入された際の複雑なスタック状態の変化に対応しきれていなかったと考えられます。
  2. // For now, disable.

    • このコメントは、この変更が一時的な措置であり、プリエンプション機能を一時的に無効化していることを示しています。根本的な問題(堅牢なトレースバックルーチンの欠如)が解決されるまでの回避策として導入されました。
  3. // The if(1) silences a compiler warning about the rest of the // function being unreachable.

    • if(1)は常に真となる条件式です。その直後にreturn;が続くため、この行が実行されると、preemptone関数の残りのコードは決して実行されません。コンパイラはこのような「到達不能なコード」に対して警告を出すことがありますが、if(1)という明示的な条件を挟むことで、開発者が意図的にそのコードパスを無効化していることをコンパイラに伝え、警告を抑制する効果があります。これはC言語プログラミングにおける一般的なイディオムの一つです。
  4. if(1) return;

    • この行が、プリエンプションを無効化する実際のコードです。preemptone関数が呼び出されると、この行が最初に評価され、無条件にreturnします。これにより、preemptone関数の本来の目的であるプリエンプション処理(ゴルーチンのコンテキスト保存、スケジューリングなど)が一切実行されなくなります。結果として、Goランタイムはプリエンプションを行わなくなり、Linuxビルドでのテスト失敗が解消されます。

この変更は、Goランタイムの安定性を確保するための実用的なアプローチであり、機能の完全な実装よりも、まずは動作するシステムを維持することを優先した判断と言えます。

関連リンク

参考にした情報源リンク

  • 特になし (コミットメッセージとコードから直接情報を抽出しました)
  • Go言語のプリエンプションに関する一般的な知識
  • Go言語のランタイムとスタック管理に関する一般的な知識
  • C言語のプログラミングイディオムに関する一般的な知識# [インデックス 16799] ファイルの概要

このコミットは、Goランタイムにおけるプリエンプション(preemption)機能を一時的に無効化するものです。具体的には、Linuxビルドで発生していたテストの失敗(runtime.deferreturnからの不明な引数フレームサイズ、およびinvalid stackエラー)を修正するために、src/pkg/runtime/proc.c内のプリエンプション関連コードの冒頭にif(1) return;を追加し、それ以降のプリエンプション処理が実行されないようにしています。これは、当時のトレースバックルーチンがプリエンプションの要件を満たしていなかったための一時的な措置です。

コミット

1da96a30395b20cb4e2059c49bee05540e36aac6

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

https://github.com/golang.org/go/commit/1da96a30395b20cb4e2059c49bee05540e36aac6

元コミット内容

runtime: disable preemption again to fix linux build

Otherwise the tests in pkg/runtime fail:

runtime: unknown argument frame size for runtime.deferreturn called from 0x48657b [runtime_test.func·022]
fatal error: invalid stack
...

R=golang-dev, dave
CC=golang-dev
https://golang.org/cl/11483043

変更の背景

このコミットの背景には、Goランタイムにおけるプリエンプション機能の実装と、それに伴う安定性の問題がありました。Goのランタイムは、複数のゴルーチン(goroutine)を効率的にスケジューリングするために、プリエンプションの仕組みを持っています。プリエンプションとは、実行中のゴルーチンを強制的に中断し、別のゴルーチンにCPUを割り当てることで、協調的ではない(non-cooperative)スケジューリングを実現する技術です。これにより、特定のゴルーチンがCPUを占有し続けることを防ぎ、システム全体の応答性を向上させます。

しかし、このコミットが作成された2013年7月時点では、Goランタイムのプリエンプション機能はまだ開発途上にあり、特にスタック管理やトレースバックのメカニズムが十分に堅牢ではありませんでした。コミットメッセージに明記されているように、プリエンプションが有効な状態では、pkg/runtime内のテストが失敗していました。具体的には、runtime: unknown argument frame size for runtime.deferreturnというエラーと、それに続くfatal error: invalid stackという致命的なエラーが発生していました。

これらのエラーは、プリエンプションによってゴルーチンの実行が中断された際に、スタックフレームの情報が正しく保存・復元されなかったり、defer関数の呼び出し(runtime.deferreturn)に必要なスタック情報が破損したりしたことを示唆しています。このような状況では、プログラムの安定性が損なわれるため、一時的な措置としてプリエンプションを無効化する必要がありました。コミットメッセージの「disable preemption again」という表現から、以前にも同様の問題が発生し、プリエンプションを無効化した経緯があったことが伺えます。

前提知識の解説

Goランタイム (Go Runtime)

Goランタイムは、Goプログラムの実行を管理する非常に重要なコンポーネントです。これには、ガベージコレクション(GC)、ゴルーチン(goroutine)のスケジューリング、チャネル(channel)の管理、メモリ割り当て、システムコールインターフェースなどが含まれます。Goプログラムは、OSのプロセスとして実行されますが、その内部で多数の軽量なゴルーチンを並行して実行し、GoランタイムがこれらのゴルーチンをOSのスレッドにマッピングしてスケジューリングします。

プリエンプション (Preemption)

プリエンプションとは、オペレーティングシステムやランタイムが、現在実行中のタスク(この場合はゴルーチン)を強制的に中断し、別のタスクにCPUの実行権を割り当てるメカニズムです。これにより、特定のタスクがCPUを独占するのを防ぎ、システム全体の公平性や応答性を保証します。

Goにおけるプリエンプションは、特にCPUバウンドなゴルーチンが長時間実行され、他のゴルーチンの実行を妨げる「飢餓(starvation)」状態を防ぐために重要です。初期のGoランタイムは協調的スケジューリング(cooperative scheduling)に依存しており、ゴルーチンが自発的に実行を中断する(例えば、チャネル操作やシステムコール待ちなど)までCPUを占有することができました。しかし、純粋な計算処理を行うゴルーチンは自発的に中断しないため、他のゴルーチンが実行機会を得られない問題がありました。これを解決するために、Goランタイムはタイムスライスベースのプリエンプションを導入しました。

defer文とruntime.deferreturn

Goのdefer文は、関数がリターンする直前に実行される関数呼び出しをスケジュールするために使用されます。これは、リソースの解放(ファイルのクローズ、ロックの解除など)を確実に行うために非常に便利です。

defer文がコンパイルされると、Goコンパイラはdeferされた関数呼び出しを、ランタイムが提供するruntime.deferprocという関数に変換します。そして、関数がリターンする際には、runtime.deferreturnという関数が呼び出され、スケジュールされたdefer関数が実行されます。

runtime.deferreturnが正しく機能するためには、スタックフレームの情報(どの関数がどこから呼び出されたか、引数は何かなど)が正確である必要があります。プリエンプションによってゴルーチンの実行が中断され、スタックの状態が不整合になると、deferreturnがスタック情報を正しく解釈できなくなり、「unknown argument frame size」や「invalid stack」といったエラーが発生する可能性があります。

スタック管理とトレースバック

Goのゴルーチンは、比較的小さなスタック(通常は数KBから始まり、必要に応じて動的に拡張・縮小される)を持っています。スタックは、関数のローカル変数、引数、リターンアドレスなどを格納するために使用されます。

トレースバックとは、プログラムがクラッシュしたり、特定のイベントが発生したりした際に、現在の実行パス(どの関数がどの関数を呼び出したか)をスタック情報から逆順に辿って表示するプロセスです。デバッグやエラー解析において非常に重要な情報となります。

プリエンプションが導入されると、ゴルーチンは任意の時点で中断される可能性があります。このとき、スタックの状態が常に一貫しており、中断された時点からでも正確なトレースバックが可能である必要があります。もしスタックが不整合な状態でプリエンプションが発生すると、トレースバックが不可能になったり、誤った情報を提供したりする可能性があります。このコミットの背景にある問題は、まさにプリエンプション時のスタックの整合性とトレースバックの堅牢性に関するものでした。

src/pkg/runtime/proc.c

src/pkg/runtime/proc.cは、GoランタイムのC言語部分で、主にプロセスの管理、ゴルーチンのスケジューリング、M(Machine)とP(Processor)の管理など、Goの並行処理モデルの核心部分を実装しています。プリエンプションのロジックもこのファイルに含まれています。

技術的詳細

このコミットは、Goランタイムのプリエンプションメカニズムが、当時のスタック管理およびトレースバックルーチンの能力を超えていたことを示しています。

Goランタイムのプリエンプションは、通常、タイマー割り込みやシステムコールからの復帰時など、特定のタイミングでトリガーされます。プリエンプションがトリガーされると、ランタイムは現在実行中のゴルーチンのコンテキスト(レジスタの状態、プログラムカウンタ、スタックポインタなど)を保存し、そのゴルーチンを「実行可能(runnable)」状態に戻して、別のゴルーチンにCPUを割り当てます。

問題は、このコンテキストの保存と復元、特にスタックの状態の保存と、その後のトレースバックの正確性に関するものでした。コミットメッセージにあるunknown argument frame size for runtime.deferreturnというエラーは、defer呼び出しの処理中に、スタック上の引数フレームのサイズがランタイムにとって未知または不正な状態であったことを意味します。これは、プリエンプションによってスタックが中途半端な状態で中断され、deferreturnが期待するスタックレイアウトと実際のスタックレイアウトが一致しなくなったために発生したと考えられます。

また、fatal error: invalid stackは、スタック自体が破損しているか、ランタイムがスタックとして認識できない状態になっていることを示唆しています。これは非常に深刻なエラーであり、プログラムのクラッシュに直結します。

コミットの変更は、proc.c内のpreemptone関数(単一のPに対するプリエンプションを試みる関数)の冒頭に、無条件でリターンするif(1) return;を追加することで、この関数がそれ以降のプリエンプションロジックを実行しないようにしています。これにより、プリエンプションが完全に無効化され、上記のエラーが発生しなくなります。

コメントにある// Preemption requires more robust traceback routines.という記述は、この問題の根本原因が、プリエンプションが任意の時点で発生してもスタックの状態を正確に把握し、トレースバックできるような堅牢なルーチンがまだ存在しなかったことにあると明確に述べています。// For now, disable.は、これが一時的な回避策であることを示しています。

// The if(1) silences a compiler warning about the rest of the // function being unreachable.というコメントは、if(1) return;というコードが、その後のコードを常に到達不能にするため、コンパイラが「到達不能なコード」に関する警告を出すのを防ぐための慣用的な記述であることを説明しています。これは、開発者が意図的にコードを無効化していることをコンパイラに伝えるためのものです。

このコミットは、Goランタイムの進化の過程で、並行処理の複雑な側面(特にプリエンプションとスタック管理の相互作用)を安定させるための試行錯誤があったことを示しています。プリエンプションはGoの並行処理モデルにおいて非常に重要な機能ですが、その実装はスタックの動的な性質やガベージコレクションとの連携など、多くの技術的課題を伴います。

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

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -2175,6 +2175,12 @@ preemptone(P *p)
 	M *mp;
 	G *gp;
 
+// Preemption requires more robust traceback routines.
+// For now, disable.
+// The if(1) silences a compiler warning about the rest of the
+// function being unreachable.
+if(1) return;
+
 	mp = p->m;
 	if(mp == nil || mp == m)
 		return;

コアとなるコードの解説

変更はsrc/pkg/runtime/proc.cファイルのpreemptone関数内で行われています。

preemptone関数は、特定のプロセッサ(P)に紐付けられたM(OSスレッド)上で実行されているゴルーチンに対して、プリエンプションを試みる役割を担っています。

追加された6行は以下の通りです。

// Preemption requires more robust traceback routines.
// For now, disable.
// The if(1) silences a compiler warning about the rest of the
// function being unreachable.
if(1) return;
  1. // Preemption requires more robust traceback routines.

    • このコメントは、プリエンプション機能が正しく動作するためには、より堅牢なトレースバックルーチンが必要であることを明確に述べています。これは、プリエンプションによってゴルーチンが任意の時点で中断されるため、その時点でのスタックの状態を正確に把握し、デバッグ情報やエラーレポートのためにトレースバックを生成できる能力が不可欠であることを示唆しています。当時のGoランタイムのトレースバック機能は、プリエンプションが導入された際の複雑なスタック状態の変化に対応しきれていなかったと考えられます。
  2. // For now, disable.

    • このコメントは、この変更が一時的な措置であり、プリエンプション機能を一時的に無効化していることを示しています。根本的な問題(堅牢なトレースバックルーチンの欠如)が解決されるまでの回避策として導入されました。
  3. // The if(1) silences a compiler warning about the rest of the // function being unreachable.

    • if(1)は常に真となる条件式です。その直後にreturn;が続くため、この行が実行されると、preemptone関数の残りのコードは決して実行されません。コンパイラはこのような「到達不能なコード」に対して警告を出すことがありますが、if(1)という明示的な条件を挟むことで、開発者が意図的にそのコードパスを無効化していることをコンパイラに伝え、警告を抑制する効果があります。これはC言語プログラミングにおける一般的なイディオムの一つです。
  4. if(1) return;

    • この行が、プリエンプションを無効化する実際のコードです。preemptone関数が呼び出されると、この行が最初に評価され、無条件にreturnします。これにより、preemptone関数の本来の目的であるプリエンプション処理(ゴルーチンのコンテキスト保存、スケジューリングなど)が一切実行されなくなります。結果として、Goランタイムはプリエンプションを行わなくなり、Linuxビルドでのテスト失敗が解消されます。

この変更は、Goランタイムの安定性を確保するための実用的なアプローチであり、機能の完全な実装よりも、まずは動作するシステムを維持することを優先した判断と言えます。

関連リンク

参考にした情報源リンク

  • 特になし (コミットメッセージとコードから直接情報を抽出しました)
  • Go言語のプリエンプションに関する一般的な知識
  • Go言語のランタイムとスタック管理に関する一般的な知識
  • C言語のプログラミングイディオムに関する一般的な知識