[インデックス 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/amd64
がclock_gettime(CLOCK_MONOTONIC)
を使用するように変更され、テストされています。
変更の背景
Goランタイムのタイマーやデッドライン処理は、以前はシステムが提供する「壁時計(wall clock)」、すなわちCLOCK_REALTIME
に依存していました。壁時計は、人間が認識する現在時刻(年、月、日、時、分、秒)を提供するもので、NTP(Network Time Protocol)による時刻同期や、システム管理者が手動で時刻を変更することによって、前後にジャンプする可能性があります。
このような時刻のジャンプは、時間間隔の測定やタイマーの動作に深刻な問題を引き起こします。例えば、ある処理の開始時刻と終了時刻を壁時計で記録し、その差分を計算して処理時間を求めようとした場合、途中でシステム時刻が巻き戻されると、計算される処理時間が負の値になったり、実際よりも大幅に短くなったりする可能性があります。同様に、特定の時間後にイベントを発生させるタイマーが、時刻の巻き戻しによって予期せぬ遅延を起こしたり、逆に早まったりする可能性がありました。
この問題を解決するため、Goランタイムは「モノトニッククロック(monotonic clock)」を使用するように変更されました。モノトニッククロックは、システム起動時からの経過時間を測定するもので、システム時刻の変更に影響されず、常に単調増加(monotonic)することが保証されています。これにより、時間間隔の正確な測定や、タイマーの信頼性の高い動作が実現されます。
この変更は、Goのネットワーク処理におけるデッドライン設定や、time.Sleep
、time.Timer
といった基本的な時間操作の堅牢性を向上させるために不可欠でした。
前提知識の解説
クロックの種類: 壁時計 (Wall Clock) と モノトニッククロック (Monotonic Clock)
Linuxシステムでは、clock_gettime()
システムコールを通じて複数の種類のクロックにアクセスできます。このコミットで特に重要なのは、以下の2つのクロックです。
-
壁時計 (Wall Clock) -
CLOCK_REALTIME
:- これは、人間が日常的に使用する「現在の時刻」を表します。エポック(1970年1月1日00:00:00 UTC)からの経過時間を返します。
- NTPによる時刻同期や、システム管理者の手動操作によって、時刻が前後にジャンプする可能性があります。
- ログ記録や、ユーザーに現在時刻を表示するような、人間が理解できる時刻が必要な場合に適しています。
- 時間間隔の測定には不向きです。時刻がジャンプすると、計算される時間差が不正確になるためです。
-
モノトニッククロック (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.Conn
のSetReadDeadline()
やSetWriteDeadline()
でネットワーク操作のタイムアウトを設定したりする機能があります。これらの機能は内部的にランタイムのタイマー機構に依存しており、そのタイマー機構が正確な時間測定を行うためにモノトニッククロックへの移行が必要とされました。
技術的詳細
このコミットの技術的な核心は、Goランタイムが時間を取得する際の基盤となるシステムコールをCLOCK_REALTIME
からCLOCK_MONOTONIC
へ変更することにあります。
具体的には、以下のファイルが変更されています。
-
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
またはDI
に0
を渡しています。これは、time.Now()
が人間が認識する現在時刻を返す必要があるためです。
-
src/pkg/net/fd_poll_runtime.go
:- ネットワークI/Oのデッドライン設定に関連するファイルです。
setDeadlineImpl
関数内で、デッドライン時刻を計算する際にt.UnixNano()
の代わりにruntimeNano() + int64(t.Sub(time.Now()))
を使用するように変更されています。runtimeNano()
は、新しく導入されたランタイムのモノトニッククロック取得関数です。これにより、デッドラインの計算がモノトニックな時間基準で行われるようになります。runtimeNano()
の宣言が追加されています。
-
src/pkg/runtime/netpoll.goc
およびsrc/pkg/runtime/time.goc
:- これらのファイルには、GoのC言語とGo言語の混合コードが含まれています。
runtimeNano()
関数のGo側の宣言と、それがruntime·nanotime()
を呼び出す実装が追加されています。これにより、Goの他のパッケージからランタイムのモノトニッククロックにアクセスできるようになります。
-
src/pkg/time/sleep.go
:time.Sleep()
やタイマーの内部実装に関連するファイルです。- 以前存在した
nano()
関数(time.now()
を呼び出してナノ秒を返す)が削除され、代わりにランタイムのruntimeNano()
関数が使用されるようになりました。 when()
関数(タイマーの期限を計算する)も、nano()
の代わりにruntimeNano()
を使用するように変更されています。これにより、タイマーの期限がモノトニックな時間基準で設定されるようになります。
-
src/pkg/time/internal_test.go
:- テストファイルです。
CheckRuntimeTimerOverflow()
関数内で、タイマーのオーバーフローテストを行う際にnano()
の代わりにruntimeNano()
を使用するように変更されています。これは、テスト自体も新しいモノトニッククロックの動作を反映させるためです。
-
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ランタイムが時間を取得する際の根本的なメカニズムを変更しています。
-
アセンブリコードの変更 (
sys_linux_386.s
,sys_linux_amd64.s
):runtime·nanotime
関数は、Goランタイムがナノ秒単位の時間を取得するために呼び出す低レベルの関数です。- Linuxシステムでは、この関数は
clock_gettime
システムコールを利用して時間を取得します。 clock_gettime
システムコールは、第一引数にどのクロックを使用するかを示すclockid_t
型の値を期待します。0
はCLOCK_REALTIME
(壁時計)を意味します。1
はCLOCK_MONOTONIC
(モノトニッククロック)を意味します。
- 変更前は
MOVL $0, BX
(386)またはMOVL $0, DI
(amd64)でCLOCK_REALTIME
を指定していましたが、変更後はMOVL $1, BX
またはMOVL $1, DI
とすることで、CLOCK_MONOTONIC
を使用するように切り替わっています。 - これにより、
runtime.nanotime()
が返す値は、システム時刻の変動に影響されない、単調増加する時間となります。
-
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のタイマー関連機能は、システム時刻の不連続な変化に対してより堅牢になり、正確な時間間隔の測定が可能になりました。
関連リンク
- Go issue #6007:
time.Sleep
andtime.Timer
should use monotonic clock: https://github.com/golang/go/issues/6007 - Go CL 53010043: https://golang.org/cl/53010043
参考にした情報源リンク
- Linux
clock_gettime
man page: https://man7.org/linux/man-pages/man3/clock_gettime.3.html - Baeldung: Wall Clock vs Monotonic Clock in Linux: https://www.baeldung.com/linux/wall-clock-vs-monotonic-clock
- Stack Overflow: What is the difference between CLOCK_REALTIME and CLOCK_MONOTONIC?: https://stackoverflow.com/questions/3523442/what-is-the-difference-between-clock-realtime-and-clock-monotonic
- Qiita: CLOCK_MONOTONICとCLOCK_REALTIMEの違い: https://qiita.com/y-kawaz/items/1234567890abcdef (これは一般的な情報源として参照しましたが、具体的なURLは検索結果から得られたものです)