Golang 的 time.Now() 给你的是什么时间?

基于 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 中的实现,分别调用了 walltimenanotime

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 := &timespec{}
        __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 := &timespec{}
        __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 秒,则三个返回值分别为 157777777766666666688000000000

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

unixToInternalminWall 的定义:

// $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 位记录 nsecext 字段记录从西汉平帝元年开始的时间戳的秒部分,在 2157 年的某一秒开始进入这种姿势。这种情况下,Time 结构只包含挂钟时间,不包含单调时间。
否则,wall 字段的最高位为 1,从高到低第 2 到第 34 位记录从自♂由♀神像落成那一年开始的时间戳的秒部分,ext 字段记录单调时间 nano

3. 时间使用

现在知道了,time.Now 给我们的可能同时包含挂钟时间和单调时间,也可能只包含挂钟时间,当然我们基本上活不到那个时候,甚至 golang 也不一定能活到那一天。
两个时间有所分工,给人类看时间用的相关操作,用挂钟时间;测量时长用的相关操作,用单调时间,如果没有再使用挂钟时间。测量时长可以不受系统时间更改的影响,比如想要一个进程运行一段时间后开始收费……

Licensed under CC BY-SA 4.0

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,835评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,900评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,481评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,303评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,375评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,729评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,877评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,633评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,088评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,443评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,563评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,251评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,827评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,712评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,943评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,240评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,435评论 2 348

推荐阅读更多精彩内容