墙上时钟 vs 单调时钟
<br>
但对计算机而言,这个时间不一定是单调递增的。因为人觉得当前机器的时间不准,可以随意拨慢或调快。
也称 CLOCK_REALTIME
,壁钟时间
本质是个相对时间,一般以时间戳形式存在(即从 1970.01.01 00:00:00 到现在的时间)。
相关函数拿得的机器的系统时间,如果修改了系统时间,会改变获取到的值。
<br>
Monotonic Clock
<br>
Monotonic 即单调的
也称 CLOCK_MONOTONIC
,或 逻辑时钟
是个绝对时间。表示系统(或程序)启动后流逝的时间,更改系统的时间对它没有影响。每次系统(或程序)启动时,该值都归 0
<br>
操作系统会提供这两套时间,分别对应墙上时钟和单调时钟,其中墙上时钟 因为不支持闰秒, 且可人为更改,另外这个时间是通过石英钟等来实现的,会由于温度等不可控因素导致时间发生偏移,往往会通过网络时间协议 NTP 来同步修正
所以墙上时钟存在较大误差,对于某些需要精准时间计算的场景是不够的。而单调时钟保证时间一定是单调递增的,不存在时间往回拨,在这类场景中用的更多
<br>
<br>
Go 中两种时间的实现
<br>
通过 time.Now 拿到的是 墙上时钟,如
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
fmt.Println("当前墙上时间为:", start.String())
}
复制代码
输出: 当前墙上时间为: 2022-07-17 17:04:48.734903 +0800 CST m=+0.000060084
而如果修改系统时间后再执行:
输出: 当前墙上时间为: 2008-08-08 20:08:03.598384 +0800 CST m=+0.000205376
<br>
修改上面代码:
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
fmt.Println("start:", start.Unix())
fmt.Println("当前墙上时间为:", start.String())
time.Sleep(10e9)
fmt.Println("在此期间修改系统时间")
end := time.Now()
fmt.Println("end:", end.Unix())
fmt.Println("修改系统时间后的墙上时间为:", end.String())
elapsed := end.Sub(start)
fmt.Println(elapsed)
}
复制代码
发现修改系统时间后,第二次打印当前时间戳,确实“回到了过去”。但奇怪的是end.Sub(start)
的结果居然是正确的,而不是负数
看一下具体实现:
<br>
Time 结构体
<br>
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 和 ext 对 wall time seconds、wall time nanoseconds 和可选的以 nanoseconds 为单位的单调时钟读数进行编码。 从高位到低位位置,wall 编码一个 1 位标志(hasMonotonic)、一个 33 位秒字段和一个 30 位壁时间纳秒字段。纳秒字段在 [0, 999999999] 范围内。
// 如果 hasMonotonic 位为 0,则 33 位字段必须为零,并且自第 1 年 1 月 1 日以来的完整有符号 64 位墙秒存储在 ext。 如果 hasMonotonic 位为 1,则 33 位字段保存自 1885 年 1 月 1 日以来的 33 位无符号壁秒,而 ext 保存自进程开始以来的有符号 64 位单调时钟读数,纳秒。
wall uint64
ext int64
// loc specifies the Location that should be used to
// determine the minute, hour, month, day, and year
// that correspond to this Time.
// The nil location means UTC.
// All UTC times are represented with loc==nil, never loc==&utcLoc.
loc *Location // 时区,在此先不讨论
}
复制代码
对于 wall 字段这个无符号的 64 位整型,
如果单调时钟标识位为 0,则这 33 位都是 0;ext 字段是距离Jan 1 year 1
的秒数;
如果单调时钟标识位为 1,则这 33 位是距离Jan 1 year 1885
的秒数,ext 是自从程序启动的纳秒数
<br>
为什么是 1885 年 1 月 1 号?是因为 33 位的精度正好到这个点么?
并不是..详细可参考 Go中的“魔数”
1885 年和「回到未来3」&希尔山谷,1977-5-25 和「星球大战」…
还有更出名的2006-01-02 15:04:05
,Go 团队对时间模块“魔数”的引入,各种天马行空和意想不到的彩蛋。“颇具浪漫主义气息”
有意思!Go 源代码中的那些秘密:为什么 time.minWall 是 1885?
<br>
第 1 年 1 月 1 日,第一感觉:为什么不是第 0 年 1 月 1 日?
我们现在通用的这套公元纪年法,没有公元0年,公元后从 1 年开始。…所以 2000 年严格来说其实并不是新千禧年的开始,2001 年才是。所以很多人建议将 2001 而不是 2000 作为新世纪的开始
</font>
<br>
<br>
time.Now
<br>
而后看下time.Now
的实现,
// Monotonic times are reported as offsets from startNano.
// We initialize startNano to runtimeNano() - 1 so that on systems where
// monotonic time resolution is fairly low (e.g. Windows 2008
// which appears to have a default resolution of 15ms),
// we avoid ever reporting a monotonic time of 0.
// (Callers may want to use 0 as "time not set".)
// 单调时间报告为与 startNano 的偏移量。我们将 startNano 初始化为 runtimeNano() - 1,以便在单调时间分辨率相当低的系统上(例如,Windows 2008 的默认分辨率似乎为 15 毫秒),我们避免报告单调时间 时间为 0。(调用者可能希望使用 0 作为“未设置时间”。)
var startNano int64 = runtimeNano() - 1
// Now returns the current local time.
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}
}
复制代码
其中 now()
:
// Provided by package runtime.
func now() (sec int64, nsec int32, mono int64)
复制代码
和go:linkname中提到的func Sleep(d Duration)
一样,具体实现在 runtime 包中的runtime/timestub.go文件中:
package runtime
import _ "unsafe" // for go:linkname
//go:linkname time_now time.now
func time_now() (sec int64, nsec int32, mono int64) {
sec, nsec = walltime()
return sec, nsec, nanotime()
}
复制代码
即调用walltime()
,nanotime()
,返回当前的秒,纳秒和程序运行开始至今的单调时间(纳秒)
runtime/time_nofake.go:
//go:nosplit
func nanotime() int64 {
return nanotime1()
}
func walltime() (sec int64, nsec int32) {
return walltime1()
}
复制代码
系统调用,对于 Mac OS,runtime/sys_darwin.go:
//go:nosplit
//go:cgo_unsafe_args
func nanotime1() int64 {
var r struct {
t int64 // raw timer
numer, denom uint32 // conversion factors. nanoseconds = t * numer / denom.
}
libcCall(unsafe.Pointer(funcPC(nanotime_trampoline)), unsafe.Pointer(&r))
// Note: Apple seems unconcerned about overflow here. See
// https://developer.apple.com/library/content/qa/qa1398/_index.html
// Note also, numer == denom == 1 is common.
t := r.t
if r.numer != 1 {
t *= int64(r.numer)
}
if r.denom != 1 {
t /= int64(r.denom)
}
return t
}
func nanotime_trampoline()
//go:nosplit
//go:cgo_unsafe_args
func walltime1() (int64, int32) {
var t timespec
libcCall(unsafe.Pointer(funcPC(walltime_trampoline)), unsafe.Pointer(&t))
return t.tv_sec, int32(t.tv_nsec)
}
func walltime_trampoline()
复制代码
之后是汇编代码,从操作系统中获取时间
runtime/asm_arm64.s:
以 Linux amd64 架构来说,runtime/sys_linux_amd64.s
// func walltime1() (sec int64, nsec int32)
// non-zero frame-size means bp is saved and restored
TEXT runtime·walltime1(SB),NOSPLIT,$16-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, R12 // Save old SP; R12 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.
// Save the old values on stack and restore them on exit,
// so this function is reentrant.
MOVQ m_vdsoPC(BX), CX
MOVQ m_vdsoSP(BX), DX
MOVQ CX, 0(SP)
MOVQ DX, 8(SP)
LEAQ sec+0(FP), DX
MOVQ -8(DX), CX
MOVQ CX, m_vdsoPC(BX)
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
MOVL $0, DI // CLOCK_REALTIME
LEAQ 0(SP), SI
MOVQ runtime·vdsoClockgettimeSym(SB), AX
CMPQ AX, $0
JEQ fallback
CALL AX
ret:
MOVQ 0(SP), AX // sec
MOVQ 8(SP), DX // nsec
MOVQ R12, SP // Restore real SP
// Restore vdsoPC, vdsoSP
// We don't worry about being signaled between the two stores.
// If we are not in a signal handler, we'll restore vdsoSP to 0,
// and no one will care about vdsoPC. If we are in a signal handler,
// we cannot receive another signal.
MOVQ 8(SP), CX
MOVQ CX, m_vdsoSP(BX)
MOVQ 0(SP), CX
MOVQ CX, m_vdsoPC(BX)
MOVQ AX, sec+0(FP)
MOVL DX, nsec+8(FP)
RET
fallback:
MOVQ $SYS_clock_gettime, AX
SYSCALL
JMP ret
// func nanotime1() int64
TEXT runtime·nanotime1(SB),NOSPLIT,$16-8
// Switch to g0 stack. See comment above in runtime·walltime.
MOVQ SP, R12 // Save old SP; R12 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.
// Save the old values on stack and restore them on exit,
// so this function is reentrant.
MOVQ m_vdsoPC(BX), CX
MOVQ m_vdsoSP(BX), DX
MOVQ CX, 0(SP)
MOVQ DX, 8(SP)
LEAQ ret+0(FP), DX
MOVQ -8(DX), CX
MOVQ CX, m_vdsoPC(BX)
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
MOVL $1, DI // CLOCK_MONOTONIC
LEAQ 0(SP), SI
MOVQ runtime·vdsoClockgettimeSym(SB), AX
CMPQ AX, $0
JEQ fallback
CALL AX
ret:
MOVQ 0(SP), AX // sec
MOVQ 8(SP), DX // nsec
MOVQ R12, SP // Restore real SP
// Restore vdsoPC, vdsoSP
// We don't worry about being signaled between the two stores.
// If we are not in a signal handler, we'll restore vdsoSP to 0,
// and no one will care about vdsoPC. If we are in a signal handler,
// we cannot receive another signal.
MOVQ 8(SP), CX
MOVQ CX, m_vdsoSP(BX)
MOVQ 0(SP), CX
MOVQ CX, m_vdsoPC(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:
MOVQ $SYS_clock_gettime, AX
SYSCALL
JMP ret
复制代码
runtime/vdso_linux_amd64.go:
var vdsoLinuxVersion = vdsoVersionKey{"LINUX_2.6", 0x3ae75f6}
var vdsoSymbolKeys = []vdsoSymbolKey{
{"__vdso_gettimeofday", 0x315ca59, 0xb01bca00, &vdsoGettimeofdaySym},
{"__vdso_clock_gettime", 0xd35ec75, 0x6e43a318, &vdsoClockgettimeSym},
}
var (
vdsoGettimeofdaySym uintptr
vdsoClockgettimeSym uintptr
)
复制代码
先通过 runtime·vdsoClockgettimeSym 拿物理时间,如果拿不到再通过 runtime·vdsoGettimeofdaySym 拿
而 runtime·vdsoClockgettimeSym 对应的系统调用是 gettimeofday
runtime·vdsoGettimeofdaySym 对应的系统调用是 clock_gettime
关于二者区别,可参考 ❲深入理解❳如何精确测量一段代码的执行时间
gettimeofday 获取到的时间精度是微秒(us,10^-6s)。这个函数获得的系统时间是使用墙上时间 xtime 和 jiffies 处理得到的。在 Linux x86_64 系统中,gettimeofday 的实现采用了“同时映射一块内存到用户态和内核态,数据由内核态维护,用户态拥有读权限”的方式使得该函数调用不需要陷入内核去获取数据,即 Linux x86_64 位系统中,这个函数的调用成本和普通的用户态函数基本一致(小于 1ms)
clock_gettime 是 ns(纳秒,10^-9)级别精度的时间获取函数,但需要进入内核态获取
<br>
<br>
time.Unix
<br>
标准库中提供了几个由其他类型转为 time.Time 类型的方法,如下:
package main
import (
"fmt"
"time"
)
func main() {
t0 := time.Unix(0, 0)
fmt.Println(t0)
t1 := time.Date(2022, 07, 17, 19, 11, 23, 45, time.Local)
fmt.Println(t1)
st, err := time.Parse("2006-01-02 15:04:05", "2008-08-08 20:08:00")
if err != nil {
panic(err)
}
fmt.Println(st)
}
复制代码
输出:
1970-01-01 08:00:00 +0800 CST
2022-07-17 19:11:23.000000045 +0800 CST
2008-08-08 20:08:00 +0000 UTC
复制代码
其中,time.Parse
会在一系列复杂处理后最终再调用time.Date
,算是一类
在此暂只分析time.Unix
// Unix returns the local Time corresponding to the given Unix time,
// sec seconds and nsec nanoseconds since January 1, 1970 UTC.
// It is valid to pass nsec outside the range [0, 999999999].
// Not all sec values have a corresponding time value. One such
// value is 1<<63-1 (the largest int64 value).
// Unix 返回与给定 Unix 时间相对应的本地时间,自 1970 年 1 月 1 日 UTC 以来的秒秒和纳秒纳秒。
// 在 [0, 999999999] 范围之外传递 nsec 是有效的。
// 并非所有秒值都有对应的时间值。 一个这样的值是 1<<63-1(最大的 int64 值)。
func Unix(sec int64, nsec int64) Time {
if nsec < 0 || nsec >= 1e9 {
n := nsec / 1e9
sec += n
nsec -= n * 1e9
if nsec < 0 {
nsec += 1e9
sec--
}
}
return unixTime(sec, int32(nsec))
}
func unixTime(sec int64, nsec int32) Time {
return Time{uint64(nsec), sec + unixToInternal, Local}
}
var unixToInternal int64 = (1969*365 + 1969/4 - 1969/100 + 1969/400) * secondsPerDay
// 即公元1年1月1日的秒数;可参考https://www.zhihu.com/question/320347209;
// 关于计算两个日期之间的天数,可参考如何计算两个日期之间的天数: https://dashen.tech/2022/03/17/%E5%A6%82%E4%BD%95%E8%AE%A1%E7%AE%97%E4%B8%A4%E4%B8%AA%E6%97%A5%E6%9C%9F%E4%B9%8B%E9%97%B4%E7%9A%84%E5%A4%A9%E6%95%B0/
复制代码
在func unixTime
中因为入参的 nsec 是 int32 类型,转为 uint64 后,第一个 bit 位显然为 0。
所以之后的 33 个 bit 位 为零,并且 ext 存储的是 从公元第 1 年 1 月 1 日以来的,完整有符号的 64 位(墙上时钟)秒 。
即对于 time.Unix 和 time.Date/time.Parse 来的 time 是没 nanotime()的,因为 ext 字段没有存
所以如果将最初代码改为:
package main
import (
"fmt"
"time"
)
func main() {
start := time.Unix(time.Now().Unix(), 0)
fmt.Println("start:", start.Unix())
fmt.Println("当前墙上时间为:", start.String())
time.Sleep(10e9)
fmt.Println("在此期间修改系统时间")
end := time.Unix(time.Now().Unix(), 0)
fmt.Println("end:", end.Unix())
fmt.Println("修改系统时间后的墙上时间为:", end.String())
elapsed := end.Sub(start)
fmt.Println(elapsed)
}
复制代码
<br>
time.Sub
<br>
const hasMonotonic = 1 << 63
const (
minDuration Duration = -1 << 63
maxDuration Duration = 1<<63 - 1
)
// Sub returns the duration t-u. If the result exceeds the maximum (or minimum)
// value that can be stored in a Duration, the maximum (or minimum) duration
// will be returned.
// To compute t-d for a duration d, use t.Add(-d).
func (t Time) Sub(u Time) Duration {
if t.wall&u.wall&hasMonotonic != 0 {
te := t.ext
ue := u.ext
d := Duration(te - ue)
if d < 0 && te > ue {
return maxDuration // t - u is positive out of range
}
if d > 0 && te < ue {
return minDuration // t - u is negative out of range
}
return d
}
d := Duration(t.sec()-u.sec())*Second + Duration(t.nsec()-u.nsec())
// Check for overflow or underflow.
switch {
case u.Add(d).Equal(t):
return d // d is correct
case t.Before(u):
return minDuration // t - u is negative out of range
default:
return maxDuration // t - u is positive out of range
}
}
复制代码
而改回最早 time.Now 的代码,再次 debug
显然进到了 if 的逻辑块,最后直接 return d
<br>
<br>
时间的比较
<br>
package main
import (
"fmt"
"time"
)
func main() {
t1 := time.Now()
t2 := time.Now()
a := t1 == t2
fmt.Println(a)
}
复制代码
输出为 false
看下汇编:
go tool compile -S walltime.go | grep walltime.go:1 | grep -v PCDATA
还需要结合分析,隐约知道是先比较 wall 字段,再比较 ext 字段
<br>
参考自 Go是如何使用逻辑时钟的?
扩展阅读:
一次 Golang 的 time.Now 优化之旅
Linux内核高精度定时器
聊一个可能有惊喜的System GC知识点 (所以说,Go 的玫 2 分钟的强制 GC,是从软件启动后的 2 分钟,还是墙上的两分钟?比如第 0 分钟 0 秒,2 分钟 0 秒,4 分钟 0 秒?)
<br>
<br>
Rust 中的单调时间
<br>
Go 中为方便开发者,time.Now()将单调时钟和墙上时钟融在了一起,性能和 C 等比差了不少。
看看也以性能著称的 Rust,如何处理单调时间
Rust时间和日期
use std::time::{Duration, Instant};
use std::thread::sleep;
fn main() {
let now = Instant::now();
sleep(Duration::new(2, 0));
println!("{}", now.elapsed().as_secs());
}
复制代码
详见下篇。
评论