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()

我們?nèi)フ?now 這個函數(shù)的代碼,會發(fā)現(xiàn)在 package time 所屬的代碼里只有一個聲明:

// $GOROOT/src/time/time.go, line 1078
func now() (sec int64, nsec int32, mono int64)

在整個 $GOROOT/src 里也搜索不到它的定義,你可能一臉懵逼。以 golang 源碼的尿性,通常會出現(xiàn)這種情況:

//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 中用匯編實現(xiàn)的,先不管它了(手動狗頭)。后者是在非 windows 中的實現(xiàn),分別調(diào)用了 walltimenanotime。

1.1. walltime

其中,walltime 函數(shù)在不同平臺和系統(tǒng)下有分別的定義,這里以 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 匯編,我表示壓力很大。獲取系統(tǒng)時間終究需要調(diào)用操作系統(tǒng)的 API,操作系統(tǒng) API 終究是 C 語言的天下,而 Golang 與 C 的函數(shù)調(diào)用在對寄存器和棧的使用上有著很大的差別,不可能直接調(diào)用 C 函數(shù)。要么使用 cgo,但對于獲取時間這種常用 API,cgo 的性能是不能接受的,所以對于這種情況,通常都需要使用匯編來弭平語言之間的鴻溝。
如果看不懂匯編沒關(guān)系,這段代碼的主要邏輯等價于如下的代碼:

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 開頭的函數(shù)說明來自 Linux vdso,至于這是個啥麻煩自己去查。__vdso_clock_gettime 的精度是納秒,CLOCK_REALTIME 說明獲取的是真實世界中的上的掛鐘時間,也是你在桌面的某個角落會看到的時間,即所謂 walltime。而 fallback 情況下,__vdso_gettimeofday 的精度是微秒。當(dāng)然 walltime 函數(shù)的兩個返回值分別是 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,掛鐘時間可以在操作系統(tǒng)的設(shè)置中被手動更改,或者被線上的時間同步服務(wù)更改,可以時光倒流非單調(diào),而 CLOCK_MONOTONIC 表示單調(diào)時間,即從開機(jī)到當(dāng)下的時間間隔,這個間隔是單獨計數(shù)的,不受掛鐘時間更改的影響,所以是單調(diào)遞增的。但奇怪的是,在 fallback 的情況下,調(diào)用 __vdso_gettimeofday 拿到的是掛鐘時間而非單調(diào)時間,這個后面再講。

綜上,正常情況下,now 函數(shù)的三個返回值分別為:當(dāng)前掛鐘時間的 unix 時間戳的秒、納秒部分,以及以納秒為單位的單調(diào)時間。例如,當(dāng)前 unix 時間戳為 1577777777.666666666 秒,開機(jī)了 88 秒,則三個返回值分別為 1577777777、66666666688000000000。

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 就是剛才提到的匯編實現(xiàn)的 nanotime。正常情況下,用進(jìn)程初始化時的單調(diào)時間,去減當(dāng)前的單調(diào)時間,得到從進(jìn)程初始化到當(dāng)前的時間間隔。而在 fallback 的情況下,就解答了剛才的疑點,兩個掛鐘時間相減仍然能得到一個時間間隔,只是會受到掛鐘時間設(shè)置的影響。

第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年之間的秒數(shù),也就是時間戳的起始時間從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 結(jié)構(gòu)的定義:

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
}

注釋講得很清楚了。當(dāng) sec 用 33 位 hold 不住的時候,wall 字段的最高位為 0,只使用低 30 位記錄 nsec,ext 字段記錄從西漢平帝元年開始的時間戳的秒部分,在 2157 年的某一秒開始進(jìn)入這種姿勢。這種情況下,Time 結(jié)構(gòu)只包含掛鐘時間,不包含單調(diào)時間。
否則,wall 字段的最高位為 1,從高到低第 2 到第 34 位記錄從自♂由♀神像落成那一年開始的時間戳的秒部分,ext 字段記錄單調(diào)時間 nano。

3. 時間使用

現(xiàn)在知道了,time.Now 給我們的可能同時包含掛鐘時間和單調(diào)時間,也可能只包含掛鐘時間,當(dāng)然我們基本上活不到那個時候,甚至 golang 也不一定能活到那一天。
兩個時間有所分工,給人類看時間用的相關(guān)操作,用掛鐘時間;測量時長用的相關(guān)操作,用單調(diào)時間,如果沒有再使用掛鐘時間。測量時長可以不受系統(tǒng)時間更改的影響,比如想要一個進(jìn)程運行一段時間后開始收費……

Licensed under CC BY-SA 4.0

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容