写点什么

Wall Clock 与 Monotonic Clock

作者:fliter
  • 2024-02-04
    上海
  • 本文字数:7955 字

    阅读完需:约 26 分钟

墙上时钟 vs 单调时钟

<br>


  • Wall Clock: 挂钟时间,即现实世界里我们感知到的时间,如 2008-08-08 20:08:00。


但对计算机而言,这个时间不一定是单调递增的。因为人觉得当前机器的时间不准,可以随意拨慢或调快。


也称 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 或 1,表征是否有单调时钟(如果是 time.Now 等生成的有该属性,而若是通过字符串转换过来的则没有)

  • 中间的 33 位:


如果单调时钟标识位为 0,则这 33 位都是 0;ext 字段是距离Jan 1 year 1秒数


如果单调时钟标识位为 1,则这 33 位是距离Jan 1 year 1885的秒数,ext 是自从程序启动的纳秒数


  • 最后 30 位:精确到的纳秒


<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.nowfunc time_now() (sec int64, nsec int32, mono int64) { sec, nsec = walltime() return sec, nsec, nanotime()}
复制代码


即调用walltime(),nanotime(),返回当前的秒,纳秒和程序运行开始至今的单调时间(纳秒)


runtime/time_nofake.go:


//go:nosplitfunc nanotime() int64 {  return nanotime1()}
func walltime() (sec int64, nsec int32) { return walltime1()}
复制代码


系统调用,对于 Mac OS,runtime/sys_darwin.go:



//go:nosplit//go:cgo_unsafe_argsfunc 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_argsfunc 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 restoredTEXT 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 AXret: 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) RETfallback: MOVQ $SYS_clock_gettime, AX SYSCALL JMP ret
// func nanotime1() int64TEXT 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 AXret: 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) RETfallback: 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 CST2022-07-17 19:11:23.000000045 +0800 CST2008-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());}
复制代码


详见下篇。

用户头像

fliter

关注

www.dashen.tech 2018-06-21 加入

Software Engineer. Focus on Micro Service,Containerization

评论

发布
暂无评论
Wall Clock与Monotonic Clock_fliter_InfoQ写作社区