<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          『每周譯Go』Go 如何知道 time.Now?

          共 13944字,需瀏覽 28分鐘

           ·

          2021-04-13 08:26

          原文地址:https://tpaschalis.github.io/golang-time-now/

          原文作者:Paschalis

          本文永久鏈接:https://github.com/gocn/translator/blob/master/2021/w14_how_does_go_know_time_now.md

          譯者:cvley


          幾天前,我在睡前想過(guò)這個(gè)問(wèn)題,而答案比我想象的還要有意思!

          這篇博客可能比之前的稍長(zhǎng)一些,所以拿起你的咖啡、你的茶,找一個(gè)安靜的地方,一起來(lái)深入看看我們可以發(fā)現(xiàn)什么。

          所有的代碼片段都有完整的參考信息;文中的參考是release-branch.go1.16(https://github.com/golang/go/tree/release-branch.go1.16)。

          關(guān)于 time.Time

          首先,理解 Go 中_如何_嵌入時(shí)間非常有用。

          time.Time 結(jié)構(gòu)體可以表示納秒精度的時(shí)間度量。為了更可信的描述用于對(duì)比、加減的耗時(shí),time.Time 也會(huì)包含一個(gè)可選的、納秒精度的讀取_當(dāng)前進(jìn)程_單調(diào)時(shí)鐘的操作。這么做是為了避免表達(dá)錯(cuò)誤的時(shí)段,比如,夏令時(shí)(Daylight Saving time,DST)。

          type Time struct {
           wall uint64
           ext  int64
           loc *Location
          }

          Τime 結(jié)構(gòu)體在2017年早期就是當(dāng)前這個(gè)形式;你可以瀏覽 Russ Cox 提出的相關(guān)issue, 提案和實(shí)現(xiàn)。

          因此,首先有一個(gè) wall 值用于提供直接讀取的 “時(shí)鐘”時(shí)間, ext 提供了這種單調(diào)時(shí)鐘形式下的_額外_信息。

          分解 wall 參數(shù),它在最高位包含 1 比特的 hasMonotonic 標(biāo)志;接下來(lái)是表示秒的 33 比特;最后 30 個(gè)比特用于表示納秒,范圍在 [0, 999999999] 之間。

          mSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn
          ^                   ^                   ^
          hasMonotonic        seconds             nanoseconds

          在 Go >= 1.9 的版本中,hasMonotonic 標(biāo)志都是開啟的,日期是在 1885 到 2157 之間,但由于兼容性考慮和一些極端情況,Go 可以保證這些時(shí)間內(nèi)的值都可以被正確處理。

          更準(zhǔn)確的來(lái)說(shuō),下面是具體的行為差異:

          如果 hasMonotonic 比特是 1,那么 33 比特的位置存儲(chǔ)的就是從 1885 年 1 月 1 日開始的無(wú)符號(hào)的秒表示的時(shí)間,ext 表示的是從進(jìn)程開始時(shí)的 64 比特單調(diào)時(shí)鐘的納秒精度的值。在代碼中,大多數(shù)是這種情況。

          如果 hasMonotonic 比特是 0,那么 33 比特的位置是 0,從公元 1 月開始的完整 64 比特的有符號(hào)時(shí)鐘的秒值存在 ext 中,直到其單調(diào)性改變。

          最后,每個(gè) Time 的值都包含一個(gè) Location,用于計(jì)算_表示形式_的時(shí)間;位置的改變僅改變這個(gè)表示,即打印的值,它不會(huì)影響存儲(chǔ)的實(shí)際時(shí)間。nil 位置(默認(rèn)情況)表示的是 “UTC”。

          為了表述更加清楚,再重申一遍;一般 報(bào)時(shí)的操作用的是讀取的時(shí)鐘時(shí)間,但衡量時(shí)間的操作,特別是比較和減法,使用的是單調(diào)時(shí)鐘時(shí)間。

          很棒,但_當(dāng)前_時(shí)間是如何計(jì)算的?

          下面是 Go 代碼中如何定義 time.Now()startNano

          // Monotonic times are reported as offsets from startNano.
          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}
          }

          如果我們了解了一些常量后,代碼就非常明確易懂

          hasMonotonic         = 1 << 63
          unixToInternal int64 = (1969*365 + 1969/4 - 1969/100 + 1969/400) * secondsPerDay
          wallToInternal int64 = (1884*365 + 1884/4 - 1884/100 + 1884/400) * secondsPerDay
          minWall              = wallToInternal               // year 1885
          nsecShift            = 30

          if 分支檢查秒的值是否可以存儲(chǔ)在 33 比特內(nèi),否則就需要設(shè)置 hasMonotonic=off。因?yàn)閱握{(diào)的粗略計(jì)算, 2^33 秒是 272 年,所以我們可以通過(guò)確定是否在 (1885+272=) 2157 年之后就可以高效快速得到結(jié)果。

          否則,我們按上面描述的方法設(shè)置 hasMonotonic=on 的情況。

          哎呀信息有些多!

          我當(dāng)然同意!但即使有了這些信息,還有兩個(gè)未知的情況;

          定義的未引出的 now()runtimeNano() 在哪里? 以及

          Local 又是從何而來(lái)?

          下面就越來(lái)越意思了!

          第一個(gè)未解之謎

          我們先來(lái)看第一個(gè)問(wèn)題。按約定的邏輯,我們應(yīng)該在相同的包內(nèi)查看,但可能什么也找不到!

          這兩個(gè)函數(shù)是從 runtime 包中通過(guò)鏈接名字(https://tpaschalis.github.io/golang-linknames/)的方式獲取的。

          // Provided by package runtime.
          func now() (sec int64, nsec int32, mono int64)

          // runtimeNano returns the current value of the runtime clock in nanoseconds.
          //go:linkname runtimeNano runtime.nanotime
          func runtimeNano() int64

          正如鏈接名字所示,要找到 runtimeNano() ,就必須找到 runtime.nanotime(),而我們會(huì)發(fā)現(xiàn)它出現(xiàn)了兩次。

          相似的,如果我們繼續(xù)在 runtime 包中尋找,我們將會(huì)遇到 timestub.go 中包含 time.Now() 定義的鏈接名字使用了 walltime()


          // Declarations for operating systems implementing time.now
          // indirectly, in terms of walltime and nanotime assembly.

          // +build !windows
          ...
          //go:linkname time_now time.now
          func time_now() (sec int64, nsec int32, mono int64) {
           sec, nsec = walltime()
           return sec, nsec, nanotime()
          }

          啊哈!現(xiàn)在我們有了一些進(jìn)展!

          walltime()nanotime() 表示的是一個(gè) ‘虛擬’ 實(shí)現(xiàn),主要用于在 Go playground 中使用,而‘真正’ 的實(shí)現(xiàn),調(diào)用的是 walltime1nanotime1

          //go:nosplit
          func nanotime() int64 {
           return nanotime1()
          }

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

          對(duì)應(yīng)的, nanotime1walltime1 按幾種不同的平臺(tái)和架構(gòu)進(jìn)行了定義。

          更加深入

          我先為任何錯(cuò)誤的表達(dá)道歉;在遇到匯編語(yǔ)言時(shí),我有時(shí)就像一只在車燈前的小鹿一樣迷茫,但我們可以嘗試?yán)斫庠?amd64 Linux 下是如何計(jì)算 walltime。

          發(fā)現(xiàn)問(wèn)題請(qǐng)一定要評(píng)論來(lái)修改,不要猶豫!

          // 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

          從我的理解來(lái)看,這個(gè)計(jì)算過(guò)程如下。

          1. 因?yàn)槲覀儾恢来a需要多少的棧空間,所以需要切換至g0,它是每個(gè)系統(tǒng)線程創(chuàng)建的第一個(gè) goroutine ,用于調(diào)度其他的 goroutines。我們保持追蹤這個(gè)線程的本地存儲(chǔ),使用 get_tls 將它載入到 CX 寄存器,當(dāng)前的 goroutine 使用了幾次 MOVQ 語(yǔ)句。

          2. 接下來(lái)代碼存儲(chǔ) vdsoPCvdsoSP (程序計(jì)數(shù)器和棧指針 ) 的值,用于在退出前存儲(chǔ)它們,這樣程序就可以 重新進(jìn)入。

          3. 代碼檢測(cè)它是否已經(jīng)在 g0,是的話就跳轉(zhuǎn)到 noswitch,否則使用下面的代碼切換至 g0

          MOVQ m_g0(BX), DX
          MOVQ (g_sched+gobuf_sp)(DX), SP // Set SP to g0 stack
          1. 接下來(lái),嘗試載入 runtime·vdsoClockgettimeSym 進(jìn) AX 寄存器;如果它非零就調(diào)用并跳轉(zhuǎn)到 ret 代碼塊,并獲取秒和納秒的值,并存儲(chǔ)真實(shí)的棧指針和 vDSO 程序計(jì)數(shù)器和棧指針并返回
          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
          1. 另外,如果 runtime·vdsoClockgettimeSym 的地址為零,那么就會(huì)跳轉(zhuǎn)到 fallback標(biāo)簽,嘗試使用不同的方法來(lái)獲取系統(tǒng)時(shí)間,即 $SYS_clock_gettime
          MOVQ runtime·vdsoClockgettimeSym(SB), AX
           CMPQ AX, $0
           JEQ fallback
          ...
          ...
          fallback:
           MOVQ $SYS_clock_gettime, AX
           SYSCALL
           JMP ret

          同樣的文件定義了$SYS_clock_gettime

          #define SYS_clock_gettime 228

          它實(shí)際對(duì)應(yīng)的是 __x64_sys_clock_gettime syscall ,在 Linux 源碼中的系統(tǒng)調(diào)用表中可以找到。

          兩個(gè)不同的選項(xiàng)有何不同?

          “優(yōu)選”的 vdsoClockgettimeSym 模式定義在 vdsoSymbolKeys

          var vdsoSymbolKeys = []vdsoSymbolKey{
           {"__vdso_gettimeofday", 0x315ca59, 0xb01bca00, &vdsoGettimeofdaySym},
           {"__vdso_clock_gettime", 0xd35ec75, 0x6e43a318, &vdsoClockgettimeSym},
          }

          與從 文檔 中找到 vDSO 符號(hào)匹配。

          為什么選擇 __vdso_clock_gettime 而不是 __x64_sys_clock_gettime,它們有什么不同?

          vDSO 表示的是 虛擬動(dòng)態(tài)共享對(duì)象 ,它是一種將內(nèi)核空間的子集暴漏到用戶空間應(yīng)用的一種內(nèi)核機(jī)制,這樣內(nèi)核空間就可以在進(jìn)程中調(diào)用,而無(wú)需從用戶態(tài)切換至內(nèi)核態(tài)而產(chǎn)生性能損耗。

          vDSO 文檔 包含了 gettimeofday 的相關(guān)例子,解釋了使用它的好處。

          引用文檔

          有些內(nèi)核提供的系統(tǒng)調(diào)動(dòng),在用戶空間頻繁使用時(shí),會(huì)遇到這些調(diào)用主導(dǎo)整體性能的情況。這不僅是頻繁的系統(tǒng)調(diào)用導(dǎo)致,還是從用戶空間退出并進(jìn)入內(nèi)核的上下文切換的結(jié)果。

          系統(tǒng)調(diào)用會(huì)比較慢,但觸發(fā)一次軟件中斷來(lái)告訴內(nèi)核你希望進(jìn)行系統(tǒng)調(diào)用的性能開銷也很大,因?yàn)樗灤┨幚砥鞯奈⒋a和內(nèi)核的整個(gè)終端處理的路徑。

          一個(gè)頻繁使用的系統(tǒng)調(diào)用是 gettimeofday(2)。這個(gè)系統(tǒng)調(diào)用是直接由用戶空間的應(yīng)用調(diào)用。這個(gè)信息也不是秘密——任何在任意權(quán)限模式下(root或其他非特權(quán)用戶)應(yīng)用將得到相同的結(jié)果。因此內(nèi)核將這個(gè)問(wèn)題需要的信息放在了進(jìn)程可以獲取的內(nèi)存中。現(xiàn)在調(diào)用 gettimeofday(2) 從一個(gè)系統(tǒng)調(diào)用變?yōu)橐粋€(gè)正常的有幾次內(nèi)存訪問(wèn)的函數(shù)調(diào)用。

          因此, vDSO 調(diào)用被優(yōu)先選擇作為獲取時(shí)鐘信息的方法,是因?yàn)樗恍枰灤﹥?nèi)核的中斷處理路徑,但可以更快的調(diào)用。

          將它們封裝起來(lái),Linux AMD64 的當(dāng)前時(shí)間最后要么從 __vdso_clock_gettime__x64_sys_clock_gettime 系統(tǒng)調(diào)用獲取。為了“愚弄” time.Now() 你不得不修改其中一個(gè)方法。

          Windows 的奇怪之處

          有觀察力的讀者可能會(huì)問(wèn), 在 timestub.go 中,我們使用了 // +build !windows。. 這有什么用?

          這是因?yàn)椋琖indows 直接在匯編里實(shí)現(xiàn)了 time.Now() ,結(jié)果是 timeasm.go 文件中的鏈接名字。

          我們可以在 sys_windows_amd64.s 中看到相關(guān)的匯編代碼。

          據(jù)我所知,這里的代碼路徑和 Linux 下的有些相似。time·now 匯編首先做的也是檢查是否使用 QPC 來(lái)獲取 nowQPC 函數(shù)的時(shí)間。

          CMPB runtime·useQPCTime(SB), $0
           JNE useQPC

          useQPC:
           JMP runtime·nowQPC(SB)
           RET

          如果不是這種情況,代碼將會(huì)嘗試使用下面KUSER_SHARED_DATA 結(jié)構(gòu)體中的兩個(gè)地址,也叫做SharedUserData。這個(gè)結(jié)構(gòu)體保存了一些內(nèi)核信息,與用戶態(tài)共享,是為了避免向內(nèi)核多次傳輸,和 vDSO 類似。

          #define _INTERRUPT_TIME 0x7ffe0008
          #define _SYSTEM_TIME 0x7ffe0014

          KSYSTEM_TIME InterruptTime;
          KSYSTEM_TIME SystemTime;

          使用這兩個(gè)地址的部分如下所示。獲取的信息存在 KSYSTEM_TIME 結(jié)構(gòu)體中。


           CMPB runtime·useQPCTime(SB), $0
           JNE useQPC
           MOVQ $_INTERRUPT_TIME, DI
          loop:
           MOVL time_hi1(DI), AX
           MOVL time_lo(DI), BX
           MOVL time_hi2(DI), CX
           CMPL AX, CX
           JNE loop
           SHLQ $32, AX
           ORQ BX, AX
           IMULQ $100, AX
           MOVQ AX, mono+16(FP)

           MOVQ $_SYSTEM_TIME, DI

          _SYSTEM_TIME 的問(wèn)題是更低的解析度,更新周期為 100 納秒;這也可能是優(yōu)先選擇 QPC 的原因。

          在 Windows 部分我花費(fèi)了很長(zhǎng)的時(shí)間,若 你 感興趣,這里 有一些 更詳細(xì)的 信息

          第 2 個(gè)未解之謎

          這個(gè)問(wèn)題是什么來(lái)著?噢,我們還沒(méi)弄清楚 _ Local 從何而來(lái)?_

          導(dǎo)出的 Local *Location 符號(hào)首先指向了 localLoc 的地址。

          var Local *Location = &localLoc

          如果這個(gè)地址是 nil,那么就如我們所說(shuō),返回的是 UTC 位置。否則,代碼會(huì)在需要位置信息的第一次調(diào)用時(shí),通過(guò)使用 sync.Once 語(yǔ)句來(lái)設(shè)置包級(jí)別的localLoc變量。

          // localLoc is separate so that initLocal can initialize
          // it even if a client has changed Local.
          var localLoc Location
          var localOnce sync.Once

          func (l *Location) get() *Location {
           if l == nil {
            return &utcLoc
           }
           if l == &localLoc {
            localOnce.Do(initLocal)
           }
           return l
          }

          initLocal() 函數(shù)使用 $TZ 的內(nèi)容來(lái)找到使用的時(shí)區(qū)。

          如果 $TZ 變量沒(méi)有設(shè)置,Go 會(huì)使用系統(tǒng)默認(rèn)的文件如 /etc/localtime 來(lái)載入時(shí)區(qū)。如果設(shè)置但為空,Go 將使用 UTC 時(shí)區(qū),而當(dāng)它為無(wú)效的時(shí)區(qū)時(shí),它會(huì)從系統(tǒng)時(shí)區(qū)文件夾中找同名的文件。默認(rèn)的搜索路徑是

          var zoneSources = []string{
           "/usr/share/zoneinfo/",
           "/usr/share/lib/zoneinfo/",
           "/usr/lib/locale/TZ/",
           runtime.GOROOT() + "/lib/time/zoneinfo.zip",
          }

          平臺(tái)相關(guān)的 zoneinfo_XYZ.go 文件使用相似的邏輯來(lái)尋找默認(rèn)的時(shí)區(qū),比如Windows 或 WASM。過(guò)去,當(dāng)我在類 Unix 系統(tǒng)下,需要在定制的容器鏡像中使用時(shí)區(qū)時(shí),只需要在 Dockerfile 中添加下面的命令。

          COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

          另外,在無(wú)法控制構(gòu)建環(huán)境的情況下, tzdata 包提供了一個(gè) 嵌入復(fù)制 的時(shí)區(qū)數(shù)據(jù)庫(kù)。若這個(gè)包在任意位置引入或我們使用 -tags timetzdata 構(gòu)建標(biāo)簽,程序文件大小將會(huì)增加約 ~450KB,但將可以在 Go 無(wú)法在宿主系統(tǒng)中無(wú)法找到 tzdata 文件時(shí),提供一個(gè)備用的方式。

          最后,我們也可以在代碼中使用LoadLocation 函數(shù)手動(dòng)設(shè)置時(shí)區(qū),比如在測(cè)試的情況下。

          結(jié)尾

          今天就這么多!我希望你們都可以學(xué)到一些新知識(shí),或者了解一些有趣的知識(shí)點(diǎn),并更加有信心去閱讀 Go 的源碼庫(kù)!

          歡迎通過(guò)郵件或者 Twitter 聯(lián)系、提出修改建議。

          再見(jiàn),保重!

          獎(jiǎng)勵(lì):Go 中的 funcname1是什么?

          在 Go 的代碼庫(kù)中,你將會(huì)見(jiàn)到很多 funcname1()funcname2() 的引用,尤其是當(dāng)你看底層的代碼時(shí)。

          據(jù)我理解,它們有兩個(gè)目的;它們有助于保持 Go 的兼容性保證,可以更加輕松的切換未導(dǎo)出函數(shù)的內(nèi)部實(shí)現(xiàn),通過(guò)也可以將相似功能“組合”和/或鏈接起來(lái)。

          有些人可能嘲笑這種方式,但我認(rèn)為它是保持代碼可讀性和維護(hù)性的一種簡(jiǎn)單有效的方法。

          瀏覽 75
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  高清无码成人在线观看 | 国产高清无码内射视频在线观看 | 日本A V在线视频 | 日本色情A片网址 | 天天干天天操天天透 |