基于 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. 時(shí)間獲取
第1行:
sec, nsec, mono := now()
我們?nèi)フ?now
這個(gè)函數(shù)的代碼,會(huì)發(fā)現(xiàn)在 package time
所屬的代碼里只有一個(gè)聲明:
// $GOROOT/src/time/time.go, line 1078
func now() (sec int64, nsec int32, mono int64)
在整個(gè) $GOROOT/src
里也搜索不到它的定義蓄氧,你可能一臉懵逼函似。以 golang 源碼的尿性,通常會(huì)出現(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 中用匯編實(shí)現(xiàn)的堂氯,先不管它了(手動(dòng)狗頭)蔑担。后者是在非 windows 中的實(shí)現(xiàn),分別調(diào)用了 walltime
和 nanotime
咽白。
1.1. walltime
其中啤握,walltime
函數(shù)在不同平臺(tái)和系統(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)時(shí)間終究需要調(diào)用操作系統(tǒng)的 API,操作系統(tǒng) API 終究是 C 語(yǔ)言的天下三妈,而 Golang 與 C 的函數(shù)調(diào)用在對(duì)寄存器和棧的使用上有著很大的差別畜埋,不可能直接調(diào)用 C 函數(shù)。要么使用 cgo畴蒲,但對(duì)于獲取時(shí)間這種常用 API悠鞍,cgo 的性能是不能接受的,所以對(duì)于這種情況模燥,通常都需要使用匯編來弭平語(yǔ)言之間的鴻溝咖祭。
如果看不懂匯編沒關(guān)系,這段代碼的主要邏輯等價(jià)于如下的代碼:
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 := ×pec{}
__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蔫骂,至于這是個(gè)啥麻煩自己去查么翰。__vdso_clock_gettime
的精度是納秒,CLOCK_REALTIME
說明獲取的是真實(shí)世界中的墻上的掛鐘時(shí)間辽旋,也是你在桌面的某個(gè)角落會(huì)看到的時(shí)間浩嫌,即所謂 walltime。而 fallback 情況下补胚,__vdso_gettimeofday
的精度是微秒码耐。當(dāng)然 walltime
函數(shù)的兩個(gè)返回值分別是 unix 時(shí)間戳的秒和納秒部分。
1.2. nanotime
簡(jiǎn)單地來考慮溶其,好像拿到 walltime 就萬(wàn)事大吉了骚腥,然而事情并不簡(jiǎn)單。同樣的套路瓶逃,匯編來了:
// $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
主要邏輯等價(jià)于:
func nanotime() (mono int64) {
if __vdso_clock_gettime != nil {
t := ×pec{}
__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
束铭,掛鐘時(shí)間可以在操作系統(tǒng)的設(shè)置中被手動(dòng)更改廓块,或者被線上的時(shí)間同步服務(wù)更改,可以時(shí)光倒流非單調(diào)契沫,而 CLOCK_MONOTONIC
表示單調(diào)時(shí)間带猴,即從開機(jī)到當(dāng)下的時(shí)間間隔,這個(gè)間隔是單獨(dú)計(jì)數(shù)的埠褪,不受掛鐘時(shí)間更改的影響浓利,所以是單調(diào)遞增的。但奇怪的是钞速,在 fallback 的情況下贷掖,調(diào)用 __vdso_gettimeofday
拿到的是掛鐘時(shí)間而非單調(diào)時(shí)間,這個(gè)后面再講渴语。
綜上苹威,正常情況下,now
函數(shù)的三個(gè)返回值分別為:當(dāng)前掛鐘時(shí)間的 unix 時(shí)間戳的秒驾凶、納秒部分牙甫,以及以納秒為單位的單調(diào)時(shí)間。例如调违,當(dāng)前 unix 時(shí)間戳為 1577777777.666666666
秒窟哺,開機(jī)了 88
秒,則三個(gè)返回值分別為 1577777777
技肩、666666666
且轨、88000000000
。
2. 時(shí)間處理
第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
顯然虚婿,這個(gè) runtimeNano
就是剛才提到的匯編實(shí)現(xiàn)的 nanotime
旋奢。正常情況下,用進(jìn)程初始化時(shí)的單調(diào)時(shí)間然痊,去減當(dāng)前的單調(diào)時(shí)間至朗,得到從進(jìn)程初始化到當(dāng)前的時(shí)間間隔。而在 fallback 的情況下剧浸,就解答了剛才的疑點(diǎn)锹引,兩個(gè)掛鐘時(shí)間相減仍然能得到一個(gè)時(shí)間間隔,只是會(huì)受到掛鐘時(shí)間設(shè)置的影響辛蚊。
第3行:
sec += unixToInternal - minWall
unixToInternal
和 minWall
的定義:
// $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
這里粤蝎,時(shí)間戳的秒部分 sec
加上了從1885年到1970年之間的秒數(shù),也就是時(shí)間戳的起始時(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 不住的時(shí)候,wall
字段的最高位為 0
桑谍,只使用低 30 位記錄 nsec
延柠,ext
字段記錄從西漢平帝元年開始的時(shí)間戳的秒部分,在 2157 年的某一秒開始進(jìn)入這種姿勢(shì)锣披。這種情況下贞间,Time
結(jié)構(gòu)只包含掛鐘時(shí)間,不包含單調(diào)時(shí)間雹仿。
否則增热,wall
字段的最高位為 1
,從高到低第 2 到第 34 位記錄從自♂由♀神像落成那一年開始的時(shí)間戳的秒部分胧辽,ext
字段記錄單調(diào)時(shí)間 nano
峻仇。
3. 時(shí)間使用
現(xiàn)在知道了,time.Now
給我們的可能同時(shí)包含掛鐘時(shí)間和單調(diào)時(shí)間邑商,也可能只包含掛鐘時(shí)間摄咆,當(dāng)然我們基本上活不到那個(gè)時(shí)候,甚至 golang 也不一定能活到那一天人断。
兩個(gè)時(shí)間有所分工吭从,給人類看時(shí)間用的相關(guān)操作,用掛鐘時(shí)間恶迈;測(cè)量時(shí)長(zhǎng)用的相關(guān)操作涩金,用單調(diào)時(shí)間,如果沒有再使用掛鐘時(shí)間蝉绷。測(cè)量時(shí)長(zhǎng)可以不受系統(tǒng)時(shí)間更改的影響鸭廷,比如想要一個(gè)進(jìn)程運(yùn)行一段時(shí)間后開始收費(fèi)……
Licensed under CC BY-SA 4.0