基于 go1.13.4,上源码:
// $GOROOT/src/time/time.go, line 1093
func Now() Time {
sec, nsec, mono := now()
mono -= startNano
sec += unixToInternal - minWall
if uint64(sec)>>33 != 0 {
return Time{uint64(nsec), sec + minWall, Local}
}
return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local}
}
1. 时间获取
第1行:
sec, nsec, mono := now()
我们去找 now
这个函数的代码,会发现在 package time
所属的代码里只有一个声明:
// $GOROOT/src/time/time.go, line 1078
func now() (sec int64, nsec int32, mono int64)
在整个 $GOROOT/src
里也搜索不到它的定义,你可能一脸懵逼。以 golang 源码的尿性,通常会出现这种情况:
//go:linkname time_now time.now
这表示把 time.now
重定向到 time_now
。这样去搜,果不其然:
// $GOROOT/src/runtime/timeasm.go, line 13
//go:linkname time_now time.now
func time_now() (sec int64, nsec int32, mono int64)
// $GOROOT/src/runtime/timestub.go, line 14
//go:linkname time_now time.now
func time_now() (sec int64, nsec int32, mono int64) {
sec, nsec = walltime()
return sec, nsec, nanotime()
}
这才是它的真身。前者是在 windows 中用汇编实现的,先不管它了(手动狗头)。后者是在非 windows 中的实现,分别调用了 walltime
和 nanotime
。
1.1. walltime
其中,walltime
函数在不同平台和系统下有分别的定义,这里以 amd64/linux 为例:
// $GOROOT/src/runtime/sys_linux_amd64.s, line 178
// func walltime() (sec int64, nsec int32)
TEXT runtime·walltime(SB),NOSPLIT,$0-12
// We don't know how much stack space the VDSO code will need,
// so switch to g0.
// In particular, a kernel configured with CONFIG_OPTIMIZE_INLINING=n
// and hardening can use a full page of stack space in gettime_sym
// due to stack probes inserted to avoid stack/heap collisions.
// See issue #20427.
MOVQ SP, BP // Save old SP; BP unchanged by C code.
get_tls(CX)
MOVQ g(CX), AX
MOVQ g_m(AX), BX // BX unchanged by C code.
// Set vdsoPC and vdsoSP for SIGPROF traceback.
MOVQ 0(SP), DX
MOVQ DX, m_vdsoPC(BX)
LEAQ sec+0(SP), DX
MOVQ DX, m_vdsoSP(BX)
CMPQ AX, m_curg(BX) // Only switch if on curg.
JNE noswitch
MOVQ m_g0(BX), DX
MOVQ (g_sched+gobuf_sp)(DX), SP // Set SP to g0 stack
noswitch:
SUBQ $16, SP // Space for results
ANDQ $~15, SP // Align for C code
MOVQ runtime·vdsoClockgettimeSym(SB), AX
CMPQ AX, $0
JEQ fallback
MOVL $0, DI // CLOCK_REALTIME
LEAQ 0(SP), SI
CALL AX
MOVQ 0(SP), AX // sec
MOVQ 8(SP), DX // nsec
MOVQ BP, SP // Restore real SP
MOVQ $0, m_vdsoSP(BX)
MOVQ AX, sec+0(FP)
MOVL DX, nsec+8(FP)
RET
fallback:
LEAQ 0(SP), DI
MOVQ $0, SI
MOVQ runtime·vdsoGettimeofdaySym(SB), AX
CALL AX
MOVQ 0(SP), AX // sec
MOVL 8(SP), DX // usec
IMULQ $1000, DX
MOVQ BP, SP // Restore real SP
MOVQ $0, m_vdsoSP(BX)
MOVQ AX, sec+0(FP)
MOVL DX, nsec+8(FP)
RET
看来还是逃不过 Plan9 汇编,我表示压力很大。获取系统时间终究需要调用操作系统的 API,操作系统 API 终究是 C 语言的天下,而 Golang 与 C 的函数调用在对寄存器和栈的使用上有着很大的差别,不可能直接调用 C 函数。要么使用 cgo,但对于获取时间这种常用 API,cgo 的性能是不能接受的,所以对于这种情况,通常都需要使用汇编来弭平语言之间的鸿沟。
如果看不懂汇编没关系,这段代码的主要逻辑等价于如下的代码:
type timespec struct {
sec int64
nsec int64
}
type timeval struct {
sec int64
usec int64
}
func walltime() (sec int64, nsec int32) {
if __vdso_clock_gettime != nil {
t := ×pec{}
__vdso_clock_gettime(CLOCK_REALTIME, t)
return t.sec, int32(t.nsec)
}
t := &timeval{}
__vdso_gettimeofday(t, nil)
return t.sec, int32(t.usec * 1000)
}
其中 __vdso
开头的函数说明来自 Linux vdso,至于这是个啥麻烦自己去查。__vdso_clock_gettime
的精度是纳秒,CLOCK_REALTIME
说明获取的是真实世界中的墙上的挂钟时间,也是你在桌面的某个角落会看到的时间,即所谓 walltime。而 fallback 情况下,__vdso_gettimeofday
的精度是微秒。当然 walltime
函数的两个返回值分别是 unix 时间戳的秒和纳秒部分。
1.2. nanotime
简单地来考虑,好像拿到 walltime 就万事大吉了,然而事情并不简单。同样的套路,汇编来了:
// $GOROOT/src/runtime/sys_linux_amd64.s, line 236
TEXT runtime·nanotime(SB),NOSPLIT,$0-8
// Switch to g0 stack. See comment above in runtime·walltime.
MOVQ SP, BP // Save old SP; BP unchanged by C code.
get_tls(CX)
MOVQ g(CX), AX
MOVQ g_m(AX), BX // BX unchanged by C code.
// Set vdsoPC and vdsoSP for SIGPROF traceback.
MOVQ 0(SP), DX
MOVQ DX, m_vdsoPC(BX)
LEAQ ret+0(SP), DX
MOVQ DX, m_vdsoSP(BX)
CMPQ AX, m_curg(BX) // Only switch if on curg.
JNE noswitch
MOVQ m_g0(BX), DX
MOVQ (g_sched+gobuf_sp)(DX), SP // Set SP to g0 stack
noswitch:
SUBQ $16, SP // Space for results
ANDQ $~15, SP // Align for C code
MOVQ runtime·vdsoClockgettimeSym(SB), AX
CMPQ AX, $0
JEQ fallback
MOVL $1, DI // CLOCK_MONOTONIC
LEAQ 0(SP), SI
CALL AX
MOVQ 0(SP), AX // sec
MOVQ 8(SP), DX // nsec
MOVQ BP, SP // Restore real SP
MOVQ $0, m_vdsoSP(BX)
// sec is in AX, nsec in DX
// return nsec in AX
IMULQ $1000000000, AX
ADDQ DX, AX
MOVQ AX, ret+0(FP)
RET
fallback:
LEAQ 0(SP), DI
MOVQ $0, SI
MOVQ runtime·vdsoGettimeofdaySym(SB), AX
CALL AX
MOVQ 0(SP), AX // sec
MOVL 8(SP), DX // usec
MOVQ BP, SP // Restore real SP
MOVQ $0, m_vdsoSP(BX)
IMULQ $1000, DX
// sec is in AX, nsec in DX
// return nsec in AX
IMULQ $1000000000, AX
ADDQ DX, AX
MOVQ AX, ret+0(FP)
RET
主要逻辑等价于:
func nanotime() (mono int64) {
if __vdso_clock_gettime != nil {
t := ×pec{}
__vdso_clock_gettime(CLOCK_MONOTONIC, t)
return t.sec * 1000000000 + t.nsec
}
t := &timeval{}
__vdso_gettimeofday(t, nil)
return t.sec * 1000000000 + t.usec * 1000
}
同样的 __vdso_clock_gettime
,挂钟时间可以在操作系统的设置中被手动更改,或者被线上的时间同步服务更改,可以时光倒流非单调,而 CLOCK_MONOTONIC
表示单调时间,即从开机到当下的时间间隔,这个间隔是单独计数的,不受挂钟时间更改的影响,所以是单调递增的。但奇怪的是,在 fallback 的情况下,调用 __vdso_gettimeofday
拿到的是挂钟时间而非单调时间,这个后面再讲。
综上,正常情况下,now
函数的三个返回值分别为:当前挂钟时间的 unix 时间戳的秒、纳秒部分,以及以纳秒为单位的单调时间。例如,当前 unix 时间戳为 1577777777.666666666
秒,开机了 88
秒,则三个返回值分别为 1577777777
、666666666
、88000000000
。
2. 时间处理
第2行:
mono -= startNano
startNano
的定义如下:
// $GOROOT/src/time/time.go, line 1090
var startNano int64 = runtimeNano() - 1
// $GOROOT/src/time/time.go, line 1081
//go:linkname runtimeNano runtime.nanotime
func runtimeNano() int64
显然,这个 runtimeNano
就是刚才提到的汇编实现的 nanotime
。正常情况下,用进程初始化时的单调时间,去减当前的单调时间,得到从进程初始化到当前的时间间隔。而在 fallback 的情况下,就解答了刚才的疑点,两个挂钟时间相减仍然能得到一个时间间隔,只是会受到挂钟时间设置的影响。
第3行:
sec += unixToInternal - minWall
unixToInternal
和 minWall
的定义:
// $GOROOT/src/time/time.go, line 440
unixToInternal int64 = (1969*365 + 1969/4 - 1969/100 + 1969/400) * secondsPerDay
// $GOROOT/src/time/time.go, line 153
minWall = wallToInternal
// $GOROOT/src/time/time.go, line 443
wallToInternal int64 = (1884*365 + 1884/4 - 1884/100 + 1884/400) * secondsPerDay
这里,时间戳的秒部分 sec
加上了从1885年到1970年之间的秒数,也就是时间戳的起始时间从1970年提前到了1885年,注意要考虑闰年。为什么选择1885年呢?查了一下,这一年有自♂由♀神像落成……
第4~7行:
if uint64(sec)>>33 != 0 {
return Time{uint64(nsec), sec + minWall, Local}
}
return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local}
需要看一看 Time
结构的定义:
type Time struct {
// wall and ext encode the wall time seconds, wall time nanoseconds,
// and optional monotonic clock reading in nanoseconds.
//
// From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
// a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
// The nanoseconds field is in the range [0, 999999999].
// If the hasMonotonic bit is 0, then the 33-bit field must be zero
// and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
// If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
// unsigned wall seconds since Jan 1 year 1885, and ext holds a
// signed 64-bit monotonic clock reading, nanoseconds since process start.
wall uint64
ext int64
loc *Location
}
注释讲得很清楚了。当 sec
用 33 位 hold 不住的时候,wall
字段的最高位为 0
,只使用低 30 位记录 nsec
,ext
字段记录从西汉平帝元年开始的时间戳的秒部分,在 2157 年的某一秒开始进入这种姿势。这种情况下,Time
结构只包含挂钟时间,不包含单调时间。
否则,wall
字段的最高位为 1
,从高到低第 2 到第 34 位记录从自♂由♀神像落成那一年开始的时间戳的秒部分,ext
字段记录单调时间 nano
。
3. 时间使用
现在知道了,time.Now
给我们的可能同时包含挂钟时间和单调时间,也可能只包含挂钟时间,当然我们基本上活不到那个时候,甚至 golang 也不一定能活到那一天。
两个时间有所分工,给人类看时间用的相关操作,用挂钟时间;测量时长用的相关操作,用单调时间,如果没有再使用挂钟时间。测量时长可以不受系统时间更改的影响,比如想要一个进程运行一段时间后开始收费……
tbc.
有疑问加站长微信联系(非本文作者)