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

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

このコミットは、Goランタイムがシステム時刻の変動に堅牢に対応できるよう、タイマー処理にモノトニッククロックを使用するように変更するものです。特にLinux/386およびLinux/amd64アーキテクチャに焦点を当てています。

コミット

commit 86c976ffd094c0326c9ba2e3d47d9cc6d73084cf
Author: Jay Weisskopf <jay@jayschwa.net>
Date:   Mon Feb 24 10:57:46 2014 -0500

    runtime: use monotonic clock for timers (linux/386, linux/amd64)
    
    This lays the groundwork for making Go robust when the system's
    calendar time jumps around. All input values to the runtimeTimer
    struct now use the runtime clock as a common reference point.
    This affects net.Conn.Set[Read|Write]Deadline(), time.Sleep(),
    time.Timer, etc. Under normal conditions, behavior is unchanged.
    
    Each platform and architecture's implementation of runtime·nanotime()
    should be modified to use a monotonic system clock when possible.
    
    Platforms/architectures modified and tested with monotonic clock:
      linux/x86     - clock_gettime(CLOCK_MONOTONIC)
    
    Update #6007
    
    LGTM=dvyukov, rsc
    R=golang-codereviews, dvyukov, alex.brainman, stephen.gutekanst, dave, rsc, mikioh.mikioh
    CC=golang-codereviews
    https://golang.org/cl/53010043

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

https://github.com/golang/go/commit/86c976ffd094c0326c9ba2e3d47d9cc6d73084cf

元コミット内容

このコミットの目的は、Goランタイムのタイマーがシステム時刻の急激な変更(NTP同期や手動での時刻変更など)によって影響を受けないようにすることです。具体的には、net.Conn.Set[Read|Write]Deadline()time.Sleep()time.Timerといった時間に関連するGoの機能が、システム時刻の変動に左右されない「モノトニッククロック」を基準とするように変更されています。これにより、システム時刻が前後しても、これらの機能の動作が予測可能かつ堅牢になります。

コミットメッセージでは、runtime·nanotime()関数が可能な限りモノトニックシステムクロックを使用するように各プラットフォームとアーキテクチャで修正されるべきであると述べられています。このコミットでは、特にlinux/x86(386)とlinux/amd64clock_gettime(CLOCK_MONOTONIC)を使用するように変更され、テストされています。

変更の背景

Goランタイムのタイマーやデッドライン処理は、以前はシステムが提供する「壁時計(wall clock)」、すなわちCLOCK_REALTIMEに依存していました。壁時計は、人間が認識する現在時刻(年、月、日、時、分、秒)を提供するもので、NTP(Network Time Protocol)による時刻同期や、システム管理者が手動で時刻を変更することによって、前後にジャンプする可能性があります。

このような時刻のジャンプは、時間間隔の測定やタイマーの動作に深刻な問題を引き起こします。例えば、ある処理の開始時刻と終了時刻を壁時計で記録し、その差分を計算して処理時間を求めようとした場合、途中でシステム時刻が巻き戻されると、計算される処理時間が負の値になったり、実際よりも大幅に短くなったりする可能性があります。同様に、特定の時間後にイベントを発生させるタイマーが、時刻の巻き戻しによって予期せぬ遅延を起こしたり、逆に早まったりする可能性がありました。

この問題を解決するため、Goランタイムは「モノトニッククロック(monotonic clock)」を使用するように変更されました。モノトニッククロックは、システム起動時からの経過時間を測定するもので、システム時刻の変更に影響されず、常に単調増加(monotonic)することが保証されています。これにより、時間間隔の正確な測定や、タイマーの信頼性の高い動作が実現されます。

この変更は、Goのネットワーク処理におけるデッドライン設定や、time.Sleeptime.Timerといった基本的な時間操作の堅牢性を向上させるために不可欠でした。

前提知識の解説

クロックの種類: 壁時計 (Wall Clock) と モノトニッククロック (Monotonic Clock)

Linuxシステムでは、clock_gettime()システムコールを通じて複数の種類のクロックにアクセスできます。このコミットで特に重要なのは、以下の2つのクロックです。

  1. 壁時計 (Wall Clock) - CLOCK_REALTIME:

    • これは、人間が日常的に使用する「現在の時刻」を表します。エポック(1970年1月1日00:00:00 UTC)からの経過時間を返します。
    • NTPによる時刻同期や、システム管理者の手動操作によって、時刻が前後にジャンプする可能性があります。
    • ログ記録や、ユーザーに現在時刻を表示するような、人間が理解できる時刻が必要な場合に適しています。
    • 時間間隔の測定には不向きです。時刻がジャンプすると、計算される時間差が不正確になるためです。
  2. モノトニッククロック (Monotonic Clock) - CLOCK_MONOTONIC:

    • これは、システムが起動してからの経過時間を表します。特定のエポックからの時間ではなく、システム起動時を基準とした相対的な時間です。
    • CLOCK_REALTIMEとは異なり、NTP同期や手動での時刻変更によって値がジャンプすることはありません。常に単調に増加することが保証されています。
    • 時間間隔(例: 処理の実行時間、タイムアウトまでの残り時間)を正確に測定するのに適しています。
    • システムがサスペンド状態になると、その間の時間はカウントされません。サスペンド時間を含めて単調増加するクロックが必要な場合は、CLOCK_BOOTTIME(Linux固有)が使用されることもあります。
    • Goランタイムのタイマーやデッドライン処理のように、正確な時間間隔の測定が求められる場面で不可欠です。

runtime·nanotime() 関数

Goランタイム内部で使用されるnanotime()関数は、ナノ秒単位で現在の時間を取得するための関数です。このコミット以前は、この関数が壁時計(CLOCK_REALTIME)を基に時間を返していたため、システム時刻の変動の影響を受けていました。このコミットの主要な変更点は、このnanotime()関数がモノトニッククロック(CLOCK_MONOTONIC)を使用するように修正されたことです。

Goのタイマーとデッドライン

Go言語では、time.Sleep()でゴルーチンを一定時間スリープさせたり、time.Timerで指定時間後にイベントを発生させたり、net.ConnSetReadDeadline()SetWriteDeadline()でネットワーク操作のタイムアウトを設定したりする機能があります。これらの機能は内部的にランタイムのタイマー機構に依存しており、そのタイマー機構が正確な時間測定を行うためにモノトニッククロックへの移行が必要とされました。

技術的詳細

このコミットの技術的な核心は、Goランタイムが時間を取得する際の基盤となるシステムコールをCLOCK_REALTIMEからCLOCK_MONOTONICへ変更することにあります。

具体的には、以下のファイルが変更されています。

  1. src/pkg/runtime/sys_linux_386.s および src/pkg/runtime/sys_linux_amd64.s:

    • これらのファイルは、Linux/386およびLinux/amd64アーキテクチャにおけるGoランタイムの低レベルなアセンブリコードを含んでいます。
    • runtime·nanotime関数(Goのruntime.nanotime()に対応)の実装が変更されています。
    • 以前はclock_gettimeシステムコールを呼び出す際に、CLOCK_REALTIMEを示す0をレジスタBX(386)またはDI(amd64)に渡していました。
    • 変更後、CLOCK_MONOTONICを示す1をこれらのレジスタに渡すように修正されています。これにより、runtime.nanotime()がモノトニッククロックの値を返すようになります。
    • 一方で、time·now関数(Goのtime.now()に対応)は引き続きCLOCK_REALTIMEを使用するように、BXまたはDI0を渡しています。これは、time.Now()が人間が認識する現在時刻を返す必要があるためです。
  2. src/pkg/net/fd_poll_runtime.go:

    • ネットワークI/Oのデッドライン設定に関連するファイルです。
    • setDeadlineImpl関数内で、デッドライン時刻を計算する際にt.UnixNano()の代わりにruntimeNano() + int64(t.Sub(time.Now()))を使用するように変更されています。
    • runtimeNano()は、新しく導入されたランタイムのモノトニッククロック取得関数です。これにより、デッドラインの計算がモノトニックな時間基準で行われるようになります。
    • runtimeNano()の宣言が追加されています。
  3. src/pkg/runtime/netpoll.goc および src/pkg/runtime/time.goc:

    • これらのファイルには、GoのC言語とGo言語の混合コードが含まれています。
    • runtimeNano()関数のGo側の宣言と、それがruntime·nanotime()を呼び出す実装が追加されています。これにより、Goの他のパッケージからランタイムのモノトニッククロックにアクセスできるようになります。
  4. src/pkg/time/sleep.go:

    • time.Sleep()やタイマーの内部実装に関連するファイルです。
    • 以前存在したnano()関数(time.now()を呼び出してナノ秒を返す)が削除され、代わりにランタイムのruntimeNano()関数が使用されるようになりました。
    • when()関数(タイマーの期限を計算する)も、nano()の代わりにruntimeNano()を使用するように変更されています。これにより、タイマーの期限がモノトニックな時間基準で設定されるようになります。
  5. src/pkg/time/internal_test.go:

    • テストファイルです。
    • CheckRuntimeTimerOverflow()関数内で、タイマーのオーバーフローテストを行う際にnano()の代わりにruntimeNano()を使用するように変更されています。これは、テスト自体も新しいモノトニッククロックの動作を反映させるためです。
  6. src/pkg/time/tick.go:

    • time.Tickerの実装に関連するファイルです。
    • NewTicker関数内で、タイマーのwhenフィールドを設定する際にnano() + int64(d)の代わりにwhen(d)を使用するように変更されています。when(d)は既にruntimeNano()を使用するように変更されているため、これによりTickerもモノトニッククロックに準拠します。
    • sendTime関数内で、チャネルに時刻を送信する際にUnix(0, now)の代わりにNow()を使用するように変更されています。これは、time.Now()が壁時計を返すため、Tickerが壁時計ベースの時刻を送信し続けることを意味します。これは、Tickerが「現在の時刻」を定期的に通知するというユースケースに合致するため、適切な変更です。

これらの変更により、Goランタイムの内部的な時間管理が、システム時刻の変動に影響されない堅牢なモノトニッククロックを基盤とするようになり、Goアプリケーションの信頼性と予測可能性が向上しました。

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

このコミットの最も重要な変更は、Goランタイムがナノ秒単位の時間を取得するアセンブリコードにあります。

src/pkg/runtime/sys_linux_386.s (抜粋)

--- a/src/pkg/runtime/sys_linux_386.s
+++ b/src/pkg/runtime/sys_linux_386.s
@@ -123,7 +123,7 @@ TEXT time·now(SB), NOSPLIT, $32
 // void nanotime(int64 *nsec)
 TEXT runtime·nanotime(SB), NOSPLIT, $32
 	MOVL	$265, AX			// syscall - clock_gettime
-	MOVL	$0, BX
+	MOVL	$1, BX		// CLOCK_MONOTONIC
 	LEAL	8(SP), CX
 	MOVL	$0, DX
 	CALL	*runtime·_vdso(SB)

src/pkg/runtime/sys_linux_amd64.s (抜粋)

--- a/src/pkg/runtime/sys_linux_amd64.s
+++ b/src/pkg/runtime/sys_linux_amd64.s
@@ -136,7 +136,7 @@ TEXT runtime·nanotime(SB),NOSPLIT,$16
 	MOVQ	runtime·__vdso_clock_gettime_sym(SB), AX
 	CMPQ	AX, $0
 	JEQ	fallback_gtod_nt
-	MOVL	$0, DI // CLOCK_REALTIME
+	MOVL	$1, DI // CLOCK_MONOTONIC
 	LEAQ	0(SP), SI
 	CALL	AX
 	MOVQ	0(SP), AX	// sec

src/pkg/time/sleep.go (抜粋)

--- a/src/pkg/time/sleep.go
+++ b/src/pkg/time/sleep.go
@@ -8,10 +8,8 @@ package time
 // A negative or zero duration causes Sleep to return immediately.
 func Sleep(d Duration)
 
-func nano() int64 {
-	sec, nsec := now()
-	return sec*1e9 + int64(nsec)
-}
+// runtimeNano returns the current value of the runtime clock in nanoseconds.
+func runtimeNano() int64
 
 // Interface to timers implemented in package runtime.
 // Must be in sync with ../runtime/runtime.h:/^struct.Timer$
@@ -29,9 +27,9 @@ type runtimeTimer struct {
 // zero because of an overflow, MaxInt64 is returned.
 func when(d Duration) int64 {
 	if d <= 0 {
-		return nano()
+		return runtimeNano()
 	}
-	t := nano() + int64(d)
+	t := runtimeNano() + int64(d)
 	if t < 0 {
 		t = 1<<63 - 1 // math.MaxInt64
 	}

コアとなるコードの解説

上記のコード変更は、Goランタイムが時間を取得する際の根本的なメカニズムを変更しています。

  1. アセンブリコードの変更 (sys_linux_386.s, sys_linux_amd64.s):

    • runtime·nanotime関数は、Goランタイムがナノ秒単位の時間を取得するために呼び出す低レベルの関数です。
    • Linuxシステムでは、この関数はclock_gettimeシステムコールを利用して時間を取得します。
    • clock_gettimeシステムコールは、第一引数にどのクロックを使用するかを示すclockid_t型の値を期待します。
      • 0CLOCK_REALTIME(壁時計)を意味します。
      • 1CLOCK_MONOTONIC(モノトニッククロック)を意味します。
    • 変更前はMOVL $0, BX(386)またはMOVL $0, DI(amd64)でCLOCK_REALTIMEを指定していましたが、変更後はMOVL $1, BXまたはMOVL $1, DIとすることで、CLOCK_MONOTONICを使用するように切り替わっています。
    • これにより、runtime.nanotime()が返す値は、システム時刻の変動に影響されない、単調増加する時間となります。
  2. src/pkg/time/sleep.go の変更:

    • 以前はnano()というヘルパー関数が存在し、これがtime.now()(壁時計ベース)を呼び出してナノ秒を取得していました。
    • このコミットでは、nano()関数が削除され、代わりにランタイムからエクスポートされたruntimeNano()関数が使用されるようになりました。
    • runtimeNano()は、前述のアセンブリコードで変更されたruntime·nanotime()を呼び出すため、モノトニッククロックの値を返します。
    • when(d Duration)関数は、タイマーの期限(runtimeTimer構造体のwhenフィールド)を計算するために使用されます。この関数もnano()の代わりにruntimeNano()を使用するように変更されたため、タイマーの期限はモノトニックな時間基準で設定されるようになります。
    • 例えば、time.Sleep(d)が呼び出されると、内部的にはruntime.tsleep(runtimeNano() + int64(d), ...)のような形で、現在のモノトニック時間からdだけ先の時間を期限として設定します。これにより、システム時刻が変更されても、スリープの期間が正確に保たれます。

これらの変更により、Goのタイマー関連機能は、システム時刻の不連続な変化に対してより堅牢になり、正確な時間間隔の測定が可能になりました。

関連リンク

参考にした情報源リンク