前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Wall Clock与Monotonic Clock(以Go time为例)

Wall Clock与Monotonic Clock(以Go time为例)

作者头像
fliter
发布2024-02-06 11:31:14
1860
发布2024-02-06 11:31:14
举报
文章被收录于专栏:旅途散记旅途散记

墙上时钟 vs 单调时钟

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

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

也称 CLOCK_REALTIME壁钟时间

本质是个相对时间,一般以时间戳形式存在(即从1970.01.01 00:00:00到现在的时间)。

相关函数拿得的机器的系统时间,如果修改了系统时间,会改变获取到的值。

Monotonic Clock

Monotonic即单调的

也称 CLOCK_MONOTONIC,或 逻辑时钟

是个绝对时间。表示系统(或程序)启动后流逝的时间,更改系统的时间对它没有影响。每次系统(或程序)启动时,该值都归0

操作系统会提供这两套时间,分别对应墙上时钟单调时钟,其中墙上时钟 因为不支持闰秒, 且可人为更改,另外这个时间是通过石英钟等来实现的,会由于温度等不可控因素导致时间发生偏移,往往会通过网络时间协议NTP来同步修正

所以墙上时钟存在较大误差,对于某些需要精准时间计算的场景是不够的。而单调时钟保证时间一定是单调递增的,不存在时间往回拨,在这类场景中用的更多


Go中两种时间的实现

通过time.Now拿到的是 墙上时钟,如

代码语言:javascript
复制
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

修改上面代码:

代码语言:javascript
复制
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)的结果居然是正确的,而不是负数

看一下具体实现[1]

Time结构体

代码语言:javascript
复制
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位:精确到的纳秒
为什么是1885年1月1号?是因为33位的精度正好到这个点么?

并不是..详细可参考 Go中的“魔数”[2]

1885年和「回到未来3」[3]&希尔山谷,1977-5-25和「星球大战」[4]

还有更出名的2006-01-02 15:04:05,Go团队对时间模块“魔数”的引入,各种天马行空和意想不到的彩蛋。“颇具浪漫主义气息”

有意思!Go 源代码中的那些秘密:为什么 time.minWall 是 1885?[5]

第1年1月1日,第一感觉:为什么不是第0年1月1日?

我们现在通用的这套公元纪年法[6],没有公元0年[7],公元后从1年开始。…所以2000年严格来说其实并不是新千禧年的开始,2001年才是。所以很多人建议将2001而不是2000作为新世纪的开始

time.Now

而后看下time.Now的实现,

代码语言:javascript
复制
// 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():

代码语言:javascript
复制
// Provided by package runtime.
func now() (sec int64, nsec int32, mono int64)

go:linkname[8]中提到的func Sleep(d Duration)一样,具体实现在runtime包中的runtime/timestub.go[9]文件中:

代码语言:javascript
复制
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[10]:

代码语言:javascript
复制
//go:nosplit
func nanotime() int64 {
 return nanotime1()
}

func walltime() (sec int64, nsec int32) {
 return walltime1()
}

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

代码语言:javascript
复制

//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[12]:

以Linux amd64架构来说,runtime/sys_linux_amd64.s[13]

代码语言:javascript
复制
// 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[14]:

代码语言:javascript
复制
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)级别精度的时间获取函数,但需要进入内核态获取

time.Unix

标准库中提供了几个由其他类型转为time.Time类型的方法,如下:

代码语言:javascript
复制
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)
}

输出:

代码语言:javascript
复制
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

代码语言:javascript
复制
// 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字段没有存

所以如果将最初代码改为:

代码语言:javascript
复制
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)

}

time.Sub

代码语言:javascript
复制


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


时间的比较

代码语言:javascript
复制
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字段

参考自 Go是如何使用逻辑时钟的?

扩展阅读:

一次 Golang 的 time.Now 优化之旅[15]

Linux内核高精度定时器

聊一个可能有惊喜的System GC知识点 (所以说,Go的玫2分钟的强制GC,是从软件启动后的2分钟,还是墙上的两分钟?比如第0分钟0秒,2分钟0秒,4分钟0秒?)


Rust中的单调时间

Go中为方便开发者,time.Now()将单调时钟和墙上时钟融在了一起,性能和C等比差了不少。

看看也以性能著称的Rust,如何处理单调时间

Rust时间和日期[16]

代码语言:javascript
复制
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());
}

详见下篇。

参考资料

[1]

具体实现: https://pkg.go.dev/time@go1.18.4

[2]

Go中的“魔数”: https://dashen.tech/2021/08/16/Go%E4%B8%AD%E7%9A%84%E2%80%9C%E9%AD%94%E6%95%B0%E2%80%9D/

[3]

「回到未来3」: https://movie.douban.com/subject/1296666/

[4]

「星球大战」: https://movie.douban.com/subject/1293838/

[5]

有意思!Go 源代码中的那些秘密:为什么 time.minWall 是 1885?: https://jishuin.proginn.com/p/763bfbd2fb66

[6]

公元纪年法: https://zh.m.wikipedia.org/zh-hans/%E5%85%AC%E5%85%83

[7]

公元0年: https://zh.m.wikipedia.org/zh-hans/0%E5%B9%B4

[8]

go:linkname: https://dashen.tech/2021/05/23/go-linkname/

[9]

runtime/timestub.go: https://github.com/golang/go/blob/master/src/runtime/timestub.go

[10]

runtime/time_nofake.go: https://github.com/golang/go/blob/master/src/runtime/time_nofake.go

[11]

runtime/sys_darwin.go: https://github.com/golang/go/blob/master/src/runtime/sys_darwin.go#L332

[12]

runtime/asm_arm64.s: https://github.com/golang/go/blob/master/src/runtime/asm_arm64.s#L959

[13]

runtime/sys_linux_amd64.s: https://github.com/golang/go/blob/master/src/runtime/sys_linux_amd64.s

[14]

runtime/vdso_linux_amd64.go: https://github.com/golang/go/blob/master/src/runtime/vdso_linux_amd64.go

[15]

一次 Golang 的 time.Now 优化之旅: https://www.purewhite.io/2021/04/29/golang-time-now-optimize/

[16]

Rust时间和日期: https://dashen.tech/2022/01/08/Rust%E6%97%B6%E9%97%B4%E5%92%8C%E6%97%A5%E6%9C%9F/

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-02-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 旅途散记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 墙上时钟 vs 单调时钟
    • Monotonic Clock
    • Go中两种时间的实现
      • Time结构体
        • 为什么是1885年1月1号?是因为33位的精度正好到这个点么?
        • 第1年1月1日,第一感觉:为什么不是第0年1月1日?
      • time.Now
        • time.Unix
          • time.Sub
            • 时间的比较
            • Rust中的单调时间
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档