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

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

このコミットは、GoランタイムにおけるNote同期プリミティブのデバッグ出力を改善し、特に「二重ウェイクアップ(double wakeup)」の問題に関する診断情報を強化することを目的としています。src/pkg/runtime/lock_futex.cファイル内のruntime·notewakeup関数が変更され、Noteの状態が不整合になった場合に、より詳細なデバッグ情報が出力されるようになりました。

コミット

commit 98d94b589cfb03104c0bfd7d4e89b23b1d3ddf73
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Jul 31 22:03:59 2013 +0400

    runtime: better debug output for inconsistent Note
    Update #5139.
    Double wakeup on Note was reported several times,
    but no reliable reproducer.
    There also was a strange report about weird value of epoll fd.
    Maybe it's corruption of global data...
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/12182043

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

https://github.com/golang/go/commit/98d94b589cfb03104c0bfd7d4e89b23b1d3ddf73

元コミット内容

このコミットは、GoランタイムのNote同期プリミティブにおけるデバッグ出力を改善します。具体的には、Noteが二重にウェイクアップされる(double wakeup)という一貫性のない状態が報告されていたものの、再現手順が確立されていなかった問題に対応しています。また、epoll fd(ファイルディスクリプタ)の異常な値に関する報告もあり、これがグローバルデータの破損を示唆している可能性も考慮されています。この変更は、これらの問題の診断を助けるためのものです。

変更の背景

Goランタイムでは、ゴルーチン(goroutine)のスケジューリングと同期のために、Noteという低レベルの同期プリミティブが使用されています。これは、ゴルーチンを一時停止(park)させたり、再開(unpark)させたりするために使われます。

このコミットが作成された背景には、以下の問題報告がありました。

  1. Noteの二重ウェイクアップ(Double Wakeup): Noteが既にウェイクアップされているにもかかわらず、再度ウェイクアップが試みられるという現象が複数回報告されていました。これは同期プリミティブの誤用または内部状態の破損を示唆するもので、プログラムの予期せぬ動作やデッドロック、クラッシュにつながる可能性があります。しかし、この問題は再現性が低く、根本原因の特定が困難でした。
  2. epoll fdの異常値: epollはLinuxカーネルが提供するI/Oイベント通知メカニズムであり、GoランタイムはネットワークI/Oなどの非同期処理にこれを利用しています。このepoll fdが異常な値を持つという報告があり、これはメモリ破損、特にグローバルデータの破損が原因である可能性が指摘されていました。

これらの問題は、Goランタイムの安定性と信頼性に直接影響を与えるため、開発者はその原因を特定し、修正する必要がありました。このコミットは、直接的な修正ではなく、問題発生時のデバッグ情報を強化することで、将来的な原因特定と修正を容易にすることを目的としています。特に、二重ウェイクアップが発生した際に、より詳細なコンテキスト(Notekeyの以前の値)をログに出力することで、問題の分析に役立てようとしています。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGoランタイムの概念とLinuxカーネルの機能に関する知識が必要です。

  1. Goランタイム (Go Runtime): Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。ランタイムは、ゴルーチンのスケジューリング、メモリ管理(ガベージコレクション)、チャネル通信、同期プリミティブの実装、システムコールとの連携など、Goプログラムの実行に必要な低レベルの機能を提供します。C言語で書かれた部分が多く、OSの機能(futexなど)を直接利用して効率的な動作を実現しています。

  2. ゴルーチン (Goroutine): Go言語における軽量な実行スレッドです。OSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。ゴルーチンのスケジューリングはGoランタイムが行い、OSのスレッドに多重化して実行されます。

  3. Note プリミティブ: Goランタイム内部で使用される低レベルの同期プリミティブです。これは、特定のイベントが発生するまでゴルーチンを「駐車(park)」させ、イベント発生時に「ウェイクアップ(wakeup)」させるために使われます。概念的には、セマフォや条件変数に似ています。Noteは通常、runtime·notesleep(ゴルーチンを駐車させる)とruntime·notewakeup(駐車されたゴルーチンをウェイクアップさせる)という関数を通じて操作されます。Noteの内部状態は、通常、keyというフィールドで管理され、0は「駐車可能」、1は「ウェイクアップ済み」といった状態を示します。

  4. Futex (Fast Userspace Mutex): Linuxカーネルが提供する同期メカニズムです。ユーザー空間のプログラムがロックなどの同期操作を行う際に、競合が発生しない限りカーネルモードへの切り替え(システムコール)を避けることで、高速な同期を実現します。競合が発生した場合のみ、futexシステムコールを通じてカーネルに処理を委ね、スレッドをスリープさせたりウェイクアップさせたりします。GoランタイムのNoteの実装は、このfutexを内部的に利用して、ゴルーチンの駐車とウェイクアップを効率的に行っています。

  5. アトミック操作 (runtime·xchg): runtime·xchgは、Goランタイムが提供するアトミックな交換(exchange)操作です。これは、指定されたメモリ位置の値を新しい値に設定し、同時にそのメモリ位置の元の値を返す操作を、他のCPUコアやスレッドからのアクセスと競合することなく、不可分(atomic)に行うことを保証します。同期プリミティブの実装において、競合状態を避けるために不可欠な操作です。このコミットでは、n->keyの値を1に設定し、その前の値を取得するために使用されています。

  6. runtime·throw: Goランタイム内部で、回復不能な致命的なエラーが発生した場合に呼び出される関数です。これは通常、プログラムをクラッシュさせ、スタックトレースなどのデバッグ情報を出力します。panicとは異なり、recoverで捕捉することはできません。

  7. epoll: Linuxカーネルが提供するI/Oイベント通知メカニズムの一つで、多数のファイルディスクリプタ(ソケットなど)からのイベントを効率的に監視するために使用されます。Goランタイムは、ネットワーク接続からのデータ受信などのイベントをepollを通じて監視し、準備ができたゴルーチンをウェイクアップします。epoll fdとは、epollインスタンスを識別するためのファイルディスクリプタのことです。

技術的詳細

このコミットの技術的な変更は、src/pkg/runtime/lock_futex.cファイル内のruntime·notewakeup関数に集中しています。この関数は、Noteプリミティブを使用して駐車されているゴルーチンをウェイクアップする役割を担います。

変更前のruntime·notewakeup関数は、以下のようなロジックでした。

void
runtime·notewakeup(Note *n)
{
    if(runtime·xchg((uint32*)&n->key, 1))
        runtime·throw("notewakeup - double wakeup");
    runtime·futexwakeup((uint32*)&n->key, 1);
}

ここで、runtime·xchg((uint32*)&n->key, 1)は、n->keyの値をアトミックに1に設定し、その操作前のn->keyの値を返します。

  • もしn->keyの元の値が0であれば、これはNoteが「駐車可能」な状態であり、ウェイクアップが初めて行われることを意味します。runtime·xchgは0を返し、if文の条件は偽となり、処理はruntime·futexwakeupに進みます。
  • もしn->keyの元の値が1であれば、これはNoteが既に「ウェイクアップ済み」の状態であるにもかかわらず、再度ウェイクアップが試みられたことを意味します。runtime·xchgは1を返し、if文の条件は真となり、runtime·throw("notewakeup - double wakeup");が呼び出され、プログラムがクラッシュします。

このロジックは、二重ウェイクアップを検出し、致命的なエラーとして扱うという点では正しいです。しかし、クラッシュメッセージが「notewakeup - double wakeup」という一般的なものであり、n->keyの具体的な値が何であったかという情報が欠落していました。この「何であったか」という情報は、デバッグにおいて非常に重要です。例えば、n->keyが1以外の予期せぬ値であった場合、それは単なる二重ウェイクアップではなく、メモリ破損など、より深刻な問題を示唆する可能性があるからです。

変更後のruntime·notewakeup関数は以下のようになります。

void
runtime·notewakeup(Note *n)
{
    uint32 old;

    old = runtime·xchg((uint32*)&n->key, 1);
    if(old != 0) {
        runtime·printf("notewakeup - double wakeup (%d)\n", old);
        runtime·throw("notewakeup - double wakeup");
    }
    runtime·futexwakeup((uint32*)&n->key, 1);
}

この変更のポイントは以下の通りです。

  1. old変数の導入: runtime·xchg((uint32*)&n->key, 1)の戻り値がoldというuint32型の変数に明示的に格納されるようになりました。
  2. 条件分岐の変更: if(old != 0)という条件に変更されました。これは、n->keyの元の値が0でなかった場合に、二重ウェイクアップ(またはそれ以上の異常な状態)が発生したと判断します。
  3. 詳細なデバッグ出力: if(old != 0)のブロック内で、runtime·printf("notewakeup - double wakeup (%d)\n", old);という行が追加されました。これにより、クラッシュする前に、n->keyの元の値(old)が標準エラー出力に表示されるようになります。このoldの値が1であれば通常の二重ウェイクアップですが、例えば2やそれ以上の値、あるいは非常に大きな値であれば、それはメモリ破損など、Noteの内部状態が予期せぬ形で書き換えられた可能性を示唆します。

この改善により、再現性の低い二重ウェイクアップの問題が発生した際に、より具体的な診断情報が得られるようになり、根本原因の特定が格段に容易になります。特に、epoll fdの異常値の報告と合わせて、グローバルデータの破損の可能性が示唆されていたため、このような詳細なデバッグ出力は、メモリ破損の兆候を捉える上で非常に有効です。

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

変更はsrc/pkg/runtime/lock_futex.cファイル内のruntime·notewakeup関数にあります。

--- a/src/pkg/runtime/lock_futex.c
+++ b/src/pkg/runtime/lock_futex.c
@@ -118,8 +118,13 @@ runtime·noteclear(Note *n)
 void
 runtime·notewakeup(Note *n)
 {
-	if(runtime·xchg((uint32*)&n->key, 1))
+	uint32 old;
+
+	old = runtime·xchg((uint32*)&n->key, 1);
+	if(old != 0) {
+		runtime·printf("notewakeup - double wakeup (%d)\\n", old);
 		runtime·throw("notewakeup - double wakeup");
+	}
 	runtime·futexwakeup((uint32*)&n->key, 1);
 }

コアとなるコードの解説

変更されたruntime·notewakeup関数は、Note構造体へのポインタnを受け取ります。

  1. uint32 old; Notekeyフィールドの元の値を保持するための32ビット符号なし整数型変数oldを宣言します。

  2. old = runtime·xchg((uint32*)&n->key, 1); ここで、アトミックな交換操作が行われます。

    • &n->key: Note構造体nkeyフィールドのアドレスをuint32型へのポインタとして渡します。
    • 1: n->keyに設定する新しい値です。Noteがウェイクアップされた状態を示すために1を設定します。
    • runtime·xchg関数は、n->keyの現在の値をアトミックに1に設定し、設定前のn->keyの値を返します。この戻り値がold変数に格納されます。
  3. if(old != 0) { ... } old変数の値が0でなかった場合、この条件ブロックが実行されます。

    • oldが0であるべきなのは、Noteが初期状態(駐車可能状態)であり、今回初めてウェイクアップされる場合です。
    • oldが0でなかった場合、それはNoteが既にウェイクアップ済みであった(old == 1)か、あるいは何らかのメモリ破損によってkeyが予期せぬ値になっていたことを意味します。どちらの場合も、Noteの不整合な状態を示します。
  4. runtime·printf("notewakeup - double wakeup (%d)\\n", old); oldが0でなかった場合、この行が実行され、標準エラー出力にデバッグメッセージが出力されます。メッセージには、"notewakeup - double wakeup (%d)\n"という文字列と、old変数の具体的な値が含まれます。これにより、二重ウェイクアップが発生した際のn->keyの元の状態がログに記録され、デバッグ情報として利用できます。

  5. runtime·throw("notewakeup - double wakeup"); デバッグメッセージの出力後、runtime·throwが呼び出され、プログラムは致命的なエラーとしてクラッシュします。これは、Noteの二重ウェイクアップがGoランタイムにとって回復不能なエラーであると判断されているためです。

  6. runtime·futexwakeup((uint32*)&n->key, 1); oldが0であった(つまり、正常な初回ウェイクアップであった)場合、この行が実行されます。これは、Linuxカーネルのfutexシステムコールを呼び出し、n->keyを待機しているゴルーチンをウェイクアップします。

この変更により、単にエラーでクラッシュするだけでなく、クラッシュの原因となったNotekeyの具体的な値がログに残るため、問題の根本原因(例えば、単なるロジックエラーか、より深刻なメモリ破損か)を特定するための重要な手がかりとなります。

関連リンク

参考にした情報源リンク