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

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

このコミットは、Go言語のランタイムにおけるfutexsleepテストの修正に関するものです。具体的には、src/pkg/runtime/futex_test.goファイルが変更されています。

コミット

commit a594f7ddd719eaf5b610bd8ed34662ed43585761
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Sat Mar 8 07:34:40 2014 +0900

    runtime: fix futexsleep test on freebsd/386
    
    Fixes #7194.
    
    LGTM=bradfitz
    R=golang-codereviews, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/72310043

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

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

元コミット内容

このコミットは、GoランタイムのfutexsleepテストがFreeBSD/386アーキテクチャで正しく動作しない問題を修正します。具体的には、futexsleepが予期せず早期に終了してしまうケースに対応するため、テストロジックが改善されています。これはGoイシュー#7194を解決するものです。

変更の背景

このコミットの背景には、Goランタイムのfutexsleepテストが特定の環境、特にFreeBSD/386で失敗するという問題がありました。futexsleepは、Linux、DragonFly BSD、FreeBSDなどのUnix系OSで利用される、ユーザー空間のロックや同期プリミティブを効率的に実装するためのカーネル機能であるfutex(Fast Userspace muTex)に関連するGoランタイムの関数です。

Goイシュー#7194("runtime: futexsleep test fails on freebsd/386")によると、このテストはFreeBSD/386環境で、特に「2038年問題」に関連するような非常に大きなタイムアウト値(1<<31 + 100秒、つまり約68年後)を設定した場合に、予期せず早期に終了してしまうという報告がありました。これは、FreeBSD 10カーネルにおけるユーザー空間ミューテックスのtimedwaitのセマンティクス変更が原因である可能性が示唆されています。

この問題は、futexsleepが期待通りに指定された時間スリープせず、早期にウェイクアップしてしまうことを意味し、ランタイムの同期メカニズムの信頼性に影響を与える可能性がありました。そのため、この特定の環境でのテストの挙動を修正し、テストが誤って失敗しないようにする必要がありました。

前提知識の解説

Futex (Fast Userspace muTex)

Futexは、Linuxカーネルが提供する同期プリミティブであり、ユーザー空間で高速なロックやセマフォを実装するために使用されます。Futexの主な目的は、競合が少ない場合にはカーネルへのシステムコールを回避し、ユーザー空間だけで同期処理を完結させることで、パフォーマンスを向上させることです。競合が発生した場合にのみ、カーネルに介入を要求します。

Futexの基本的な操作は以下の2つです。

  1. FUTEX_WAIT (または futex_sleep): 指定されたメモリアドレス(futex変数)の値が期待する値と異なる場合、またはタイムアウトが発生するまで、現在のプロセス(またはスレッド)をスリープさせます。
  2. FUTEX_WAKE (または futex_wakeup): 指定されたメモリアドレス(futex変数)で待機しているプロセス(またはスレッド)をウェイクアップします。

Goランタイムでは、ゴルーチンのスケジューリング、ミューテックスの実装、チャネルの同期など、様々な低レベルの同期処理にfutexが利用されています。

2038年問題 (Year 2038 problem)

2038年問題は、Unix時間(エポック秒)を32ビット符号付き整数で表現しているシステムにおいて、2038年1月19日03時14分07秒 (UTC) を超えるとオーバーフローが発生し、時刻が負の値として解釈されてしまう問題です。これにより、時刻に関連する処理が誤動作する可能性があります。

このコミットのテストでは、1<<31 + 100という非常に大きなタイムアウト値が使用されています。これは、32ビット符号付き整数の最大値(2^31 - 1秒)を超える値であり、2038年問題の影響をシミュレート、またはその境界条件でのfutexsleepの挙動を確認しようとしています。FreeBSD/386のような32ビットシステムでは、このような大きなタイムアウト値の扱いが問題となることがあります。

Goのruntimeパッケージ

runtimeパッケージは、Goプログラムの実行環境を管理する低レベルな機能を提供します。これには、ゴルーチンのスケジューリング、メモリ管理(ガベージコレクション)、システムコールインターフェースなどが含まれます。FutexsleepFutexwakeupのような関数は、このパッケージ内で定義され、OSのfutex機能と直接連携して動作します。

GOOSGOARCH

Goのビルドシステムでは、GOOS環境変数でターゲットOS(例: linux, freebsd)を、GOARCH環境変数でターゲットアーキテクチャ(例: amd64, 386)を指定します。このコミットでは、freebsd/386という特定の組み合わせで問題が発生していることが示されています。

技術的詳細

変更されたfutex_test.goファイルは、futexsleep関数の動作を検証するためのテストコードです。このテストは、futexsleepが指定されたタイムアウト期間が経過するまでスリープし続けることを確認することを目的としています。

元のテストコードでは、単一のfutexsleep呼び出しに対して、非常に長いタイムアウト(2038年問題に関連する値)を設定し、time.After(time.Second)で1秒後にウェイクアップを試みていました。もしfutexsleepが1秒以内に終了してしまった場合、テストは失敗と判断されます。

しかし、FreeBSD/386環境では、この長いタイムアウトが原因でfutexsleepが予期せず早期に終了してしまう問題がありました。これは、カーネルがこのような大きなタイムアウト値を正しく処理できない、または32ビットシステムでの時間表現の限界に起因する可能性があります。

新しいテストコードでは、この問題をより堅牢に扱うために以下の変更が加えられています。

  1. 複数のテストケースの導入:

    • beforeY2038: 2038年問題の影響を受けない比較的短いタイムアウト(1日)を設定。
    • afterY2038: 2038年問題の影響を受ける可能性のある非常に長いタイムアウト(約68年後)を設定。 これにより、異なるタイムアウト値でのfutexsleepの挙動を個別に検証できるようになりました。
  2. テストゴルーチンの並列実行とチャネルによる結果収集:

    • 各テストケースはそれぞれ独立したゴルーチンで実行されます。
    • 各ゴルーチンはfutexsleepが終了した際に、結果をチャネル(tt.ch)に送信します。
    • メインのテストゴルーチンはselect文を使用して、これらのチャネルからの結果、または1秒のタイムアウトを待ちます。
  3. FreeBSD/386の特殊なハンドリング:

    • afterY2038テストケースが早期に終了した場合、runtime.GOOS == "freebsd" && runtime.GOARCH == "386"という条件で、特別なログメッセージを出力し、テストを失敗としないようにしています。これは、FreeBSD 10カーネルのtimedwaitセマンティクスの変更により、この環境ではfutexsleepが2038年以降のタイムアウトで正しく動作しない可能性があることを認識し、その挙動を許容するためです。
    • この変更は、特定の環境での既知の制限を考慮しつつ、他の環境でのfutexsleepの正しい動作を検証し続けるためのものです。

この修正により、FreeBSD/386環境でのテストの誤った失敗が回避され、同時に他の環境でのfutexsleepの健全性が引き続き検証されるようになりました。

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

変更はsrc/pkg/runtime/futex_test.goファイルに集中しています。

--- a/src/pkg/runtime/futex_test.go
+++ b/src/pkg/runtime/futex_test.go
@@ -2,33 +2,70 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.\n
 
-// Futex is only available on Dragonfly, FreeBSD and Linux.
-// The race detector emits calls to split stack functions so it breaks the test.
+// Futex is only available on DragonFly BSD, FreeBSD and Linux.
+// The race detector emits calls to split stack functions so it breaks
+// the test.
+\n
 // +build dragonfly freebsd linux
 // +build !race
 \n
 package runtime_test
 \n
 import (\n-\t. \"runtime\"\n+\t\"runtime\"\n \t\"testing\"\n \t\"time\"\n )\n \n+type futexsleepTest struct {\n+\tmtx uint32\n+\tns  int64\n+\tmsg string\n+\tch  chan futexsleepTest\n+}\n+\n+var futexsleepTests = []futexsleepTest{\n+\tbeforeY2038: {mtx: 0, ns: 86400 * 1e9, msg: \"before the year 2038\", ch: make(chan futexsleepTest, 1)},\n+\tafterY2038:  {mtx: 0, ns: (1<<31 + 100) * 1e9, msg: \"after the year 2038\", ch: make(chan futexsleepTest, 1)},\n+}\n+\n+const (\n+\tbeforeY2038 = iota\n+\tafterY2038\n+)\n+\n func TestFutexsleep(t *testing.T) {\n-\tch := make(chan bool, 1)\n-\tvar dummy uint32\n \tstart := time.Now()\n-\tgo func() {\n-\t\tEntersyscall()\n-\t\tFutexsleep(&dummy, 0, (1<<31+100)*1e9)\n-\t\tExitsyscall()\n-\t\tch <- true\n-\t}()\n-\tselect {\n-\tcase <-ch:\n-\t\tt.Errorf(\"futexsleep finished early after %s!\", time.Since(start))\n-\tcase <-time.After(time.Second):\n-\t\tFutexwakeup(&dummy, 1)\n+\tfor _, tt := range futexsleepTests {\n+\t\tgo func(tt futexsleepTest) {\n+\t\t\truntime.Entersyscall()\n+\t\t\truntime.Futexsleep(&tt.mtx, tt.mtx, tt.ns)\n+\t\t\truntime.Exitsyscall()\n+\t\t\ttt.ch <- tt\n+\t\t}(tt)\n+\t}\n+loop:\n+\tfor {\n+\t\tselect {\n+\t\tcase tt := <-futexsleepTests[beforeY2038].ch:\n+\t\t\tt.Errorf(\"futexsleep test %q finished early after %s\", tt.msg, time.Since(start))\n+\t\t\tbreak loop\n+\t\tcase tt := <-futexsleepTests[afterY2038].ch:\n+\t\t\t// Looks like FreeBSD 10 kernel has changed\n+\t\t\t// the semantics of timedwait on userspace\n+\t\t\t// mutex to make broken stuff look broken.\n+\t\t\tswitch {\n+\t\t\tcase runtime.GOOS == \"freebsd\" && runtime.GOARCH == \"386\":\n+\t\t\t\tt.Log(\"freebsd/386 may not work correctly after the year 2038, see golang.org/issue/7194\")\n+\t\t\tdefault:\n+\t\t\t\tt.Errorf(\"futexsleep test %q finished early after %s\", tt.msg, time.Since(start))\n+\t\t\t\tbreak loop\n+\t\t\t}\n+\t\tcase <-time.After(time.Second):\n+\t\t\tbreak loop\n+\t\t}\n+\t}\n+\tfor _, tt := range futexsleepTests {\n+\t\truntime.Futexwakeup(&tt.mtx, 1)\n \t}\n }\n```

## コアとなるコードの解説

### 変更点1: `runtime`パッケージのインポート方法

```diff
-\t. "runtime"
+\t"runtime"

これは、runtimeパッケージの関数を修飾なしで呼び出す(例: Futexsleep)のではなく、runtime.Futexsleepのように明示的に修飾して呼び出すように変更されたことを示します。これはコードの可読性と保守性を向上させる一般的なGoの慣習です。

変更点2: futexsleepTest構造体とテストケースの定義

type futexsleepTest struct {
	mtx uint32
	ns  int64
	msg string
	ch  chan futexsleepTest
}

var futexsleepTests = []futexsleepTest{
	beforeY2038: {mtx: 0, ns: 86400 * 1e9, msg: "before the year 2038", ch: make(chan futexsleepTest, 1)},
	afterY2038:  {mtx: 0, ns: (1<<31 + 100) * 1e9, msg: "after the year 2038", ch: make(chan futexsleepTest, 1)},
}

const (
	beforeY2038 = iota
	afterY2038
)

futexsleepTest構造体は、各テストケースのパラメータ(mtx: futex変数、ns: スリープ時間(ナノ秒)、msg: テストケースの説明、ch: 結果を送信するためのチャネル)をカプセル化します。 futexsleepTestsは、beforeY2038(1日)とafterY2038(約68年)という2つの異なるタイムアウト値を持つテストケースを定義しています。これにより、短いタイムアウトと非常に長いタイムアウトの両方でfutexsleepの挙動を検証できます。

変更点3: テストロジックの変更

 func TestFutexsleep(t *testing.T) {
-\tch := make(chan bool, 1)
-\tvar dummy uint32
 \tstart := time.Now()
-\tgo func() {\n-\t\tEntersyscall()\n-\t\tFutexsleep(&dummy, 0, (1<<31+100)*1e9)\n-\t\tExitsyscall()\n-\t\tch <- true\n-\t}()\n-\tselect {\n-\tcase <-ch:\n-\t\tt.Errorf(\"futexsleep finished early after %s!\", time.Since(start))\n-\tcase <-time.After(time.Second):\n-\t\tFutexwakeup(&dummy, 1)\n+\tfor _, tt := range futexsleepTests {\n+\t\tgo func(tt futexsleepTest) {\n+\t\t\truntime.Entersyscall()\n+\t\t\truntime.Futexsleep(&tt.mtx, tt.mtx, tt.ns)\n+\t\t\truntime.Exitsyscall()\n+\t\t\ttt.ch <- tt\n+\t\t}(tt)\n+\t}\n+loop:\n+\tfor {\n+\t\tselect {\n+\t\tcase tt := <-futexsleepTests[beforeY2038].ch:\n+\t\t\tt.Errorf(\"futexsleep test %q finished early after %s\", tt.msg, time.Since(start))\n+\t\t\tbreak loop\n+\t\tcase tt := <-futexsleepTests[afterY2038].ch:\n+\t\t\t// Looks like FreeBSD 10 kernel has changed\n+\t\t\t// the semantics of timedwait on userspace\n+\t\t\t// mutex to make broken stuff look broken.\n+\t\t\tswitch {\n+\t\t\tcase runtime.GOOS == \"freebsd\" && runtime.GOARCH == \"386\":\n+\t\t\t\tt.Log(\"freebsd/386 may not work correctly after the year 2038, see golang.org/issue/7194\")\n+\t\t\tdefault:\n+\t\t\t\tt.Errorf(\"futexsleep test %q finished early after %s\", tt.msg, time.Since(start))\n+\t\t\t\tbreak loop\n+\t\t\t}\n+\t\tcase <-time.After(time.Second):\n+\t\t\tbreak loop\n+\t\t}\n+\t}\n+\tfor _, tt := range futexsleepTests {\n+\t\truntime.Futexwakeup(&tt.mtx, 1)\n \t}\n }\n```
*   **並列実行:** `futexsleepTests`の各テストケースは、それぞれ新しいゴルーチンで実行されます。これにより、複数の`futexsleep`呼び出しを同時にテストできます。
*   **チャネルによる結果通知:** 各ゴルーチンは`futexsleep`が終了すると、対応する`futexsleepTest`構造体をチャネル`tt.ch`に送信します。
*   **`select`文による監視:** メインのテスト関数は`select`文を使用して、以下のいずれかのイベントを待ちます。
    *   `beforeY2038`テストケースが早期に終了した場合。
    *   `afterY2038`テストケースが早期に終了した場合。
    *   `time.After(time.Second)`による1秒のタイムアウト。
*   **FreeBSD/386の特殊処理:** `afterY2038`テストケースが早期に終了し、かつ`runtime.GOOS == "freebsd" && runtime.GOARCH == "386"`である場合、`t.Log`で警告メッセージを出力し、テストを失敗としないようにしています。これは、この特定の環境での既知の挙動を許容するためです。それ以外の環境で早期終了した場合は、エラーとして報告されます。
*   **`Futexwakeup`の呼び出し:** `select`ループが終了した後、すべての`futexsleepTests`に対して`runtime.Futexwakeup`が呼び出されます。これは、もし`futexsleep`がまだスリープ状態であった場合に、それらをウェイクアップしてリソースを解放するためです。

この変更により、テストはより堅牢になり、特定の環境での既知の制限を考慮しつつ、`futexsleep`の正しい動作をより広範なシナリオで検証できるようになりました。

## 関連リンク

*   Go Issue #7194: [https://golang.org/issue/7194](https://golang.org/issue/7194)
*   Go CL 72310043: [https://golang.org/cl/72310043](https://golang.org/cl/72310043)

## 参考にした情報源リンク

*   [The Futex synchronization primitive](https://lwn.net/Articles/178217/)
*   [Year 2038 problem - Wikipedia](https://en.wikipedia.org/wiki/Year_2038_problem)
*   [Go runtime package documentation](https://pkg.go.dev/runtime)
*   [Go environment variables (GOOS, GOARCH)](https://go.dev/doc/install/source#environment)
*   [FreeBSD 10 Release Notes (relevant changes to timedwait semantics might be mentioned here, though specific details would require deeper dive into FreeBSD kernel documentation)](https://www.freebsd.org/releases/10.0R/relnotes/) (General reference, specific details not directly found in a quick search)
*   [Go source code for futex_test.go](https://github.com/golang/go/blob/master/src/runtime/futex_test.go) (for context before and after the commit)
*   [Go source code for futex.go](https://github.com/golang/go/blob/master/src/runtime/futex.go) (for understanding `Futexsleep` and `Futexwakeup` implementation)