Go如何知道时间。
本文直接采用谷歌翻译翻译,方便学习查看使用
大约时间
首先,它是非常有用的了解。
该time.Time
结构可以表示具有纳秒精度的时间瞬间。为了更可靠地测量经过的时间以进行比较,加法和减法,time.Time
还可以包含当前过程单调时钟的可选的,纳秒级精度的读数。这是为了避免报告错误的持续时间,例如。如果是DST。
type Time struct {
wall uint64
ext int64
loc *Location
}
imeime结构在2017年初采用了这种形式。您可以浏览Russ Cox自己浏览的相关问题,建议和实施。
因此,首先要wall
提供一个简单的“挂钟”读数值,并以单调时钟的形式ext
提供此扩展信息。
分解时,wall
它hasMonotonic
的最高位包含一个1位标志。然后是33位用于跟踪秒数;最后是30位,用于跟踪纳秒,范围为[0,999999999]。
mSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn
^ ^ ^
hasMonotonic seconds nanoseconds
对于Go> = 1.9,该hasMonotonic
标志始终处于启用状态,其日期介于1885年至2157年之间,但是由于兼容性承诺以及极端情况,Go还要确保正确处理这些时间值。
更确切地说,这是行为上的不同之处:
如果该hasMonotonic
位为1,则33位字段存储自1885年1月1日以来的无符号的墙壁秒数,而ext保留有符号的64位单调时钟读数,距离过程开始的时间为纳秒。这是大多数代码中通常发生的情况。
如果该hasMonotonic
位为0,则33位字段为零,并且ext
像单调更改之前一样,存储自1年1月1日以来的完整带符号的64位墙秒。
最后,每个Time
值都包含一个Location,用于计算其表示形式;更改位置只会更改表示形式,例如。打印值时,它不会影响存储时间。无位置(默认)表示“ UTC”。
再次重申,使事情变得清晰;通常的计时操作使用挂钟读数,但是时间测量操作(特别是比较和减法)使用单调时钟读数。。
很好,但是当前时间如何计算?
这是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-branch检查是否可以将秒值适合33位字段中,还是需要使用hasMonotonic=off
。由于单调草案提到,2 ^33秒是272年,所以我们实际上看我们是否是后一年(1885 + 272 =)2157提前返回。
否则,我们将hasMonotonic=on
遇到如上所述的通常情况。
哎呀,真是太好了!
我必须同意!但是即使有了这些信息,仍然存在两个谜团。
*未导出now()
和runtimeNano()
定义在哪里?*和
Local来自哪里?
这就是有趣的地方!
一号谜
让我们从第一个问题开始。常规逻辑会说我们要研究同一个程序包,但是我们可能什么也找不到!
这两个函数是从运行时包中链接命名的。
// 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
正如linkname指令通知我们的那样,要找到runtimeNano()
我们必须在runtime.nanotime()
其中找到两个匹配项的位置进行搜索。
同样,如果继续查找runtime
包,我们会发现 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()
}
哈哈!现在我们到了某个地方!
双方walltime()
并nanotime()
配有“假”的实施意味着要用于去游乐场,还有“真实”之一,它调用walltime1
和nanotime1
。
//go:nosplit
func nanotime() int64 {
return nanotime1()
}
func walltime() (sec int64, nsec int32) {
return walltime1()
}
反过来,两者nanotime1
和walltime1
分别为几种 不同的 平台 和体系结构定义 。
潜水更深
如有任何错误声明,我深表歉意。我有时就像当装配面对夹在头灯鹿,但让我们试着去了解walltime是如何计算的AMD64的Linux这里。
请随时与我们联系,以提出评论和更正!
// 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
据我了解,这是该过程的过程。
-
由于我们不知道代码需要多少堆栈空间,因此我们切换到
g0
哪个是为每个OS线程创建的第一个goroutine,负责调度其他goroutine。我们使用两个语句get_tls
来跟踪线程本地存储(用于将其加载到CX
寄存器中)和当前的goroutine中MOVQ
。 -
然后,该代码存储值
vdsoPC
和vdsoSP
(程序计数器和堆栈指针)退出,这样可把功能之前恢复他们折返。 -
该代码检查它是否已经打开
g0
,跳转到的位置
noswitch
,否则
g0
用以下几行 更改为
MOVQ m_g0(BX), DX MOVQ (g_sched+gobuf_sp)(DX), SP // Set SP to g0 stack
-
接下来,它将尝试将的地址加载
runtime·vdsoClockgettimeSym
到
AX
寄存器中。如果它不为零,则将其调用并移至该
ret
块,在该块中检索第二和纳秒的值,还原实际的堆栈指针,还原vDSO程序计数器和堆栈指针,最后返回
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
-
另一方面,如果地址
runtime·vdsoClockgettimeSym
为零,那么它将跳转到
fallback
标签,在标签中尝试使用其他方法来获取系统时间,即
$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
从Linux源代码中查找syscall表时,它实际上对应于syscall!__x64_sys_clock_gettime
这两个不同的选项是什么?
“首选”vdsoClockgettimeSym
模式在vdsoSymbolKeys
var vdsoSymbolKeys = []vdsoSymbolKey{
{"__vdso_gettimeofday", 0x315ca59, 0xb01bca00, &vdsoGettimeofdaySym},
{"__vdso_clock_gettime", 0xd35ec75, 0x6e43a318, &vdsoClockgettimeSym},
}
与在文档中找到的导出的vDSO符号匹配。
为什么__vdso_clock_gettime
优先选择而不是__x64_sys_clock_gettime
,它们之间有什么区别?
vDSO代表虚拟动态共享对象,是一种内核机制,用于将内核空间例程的子集导出到用户空间应用程序,以便可以在进程内调用这些内核空间例程,而不会造成从用户模式切换到内核模式的性能损失。 。
该VDSO文档包含了相关的例子gettimeofday
解释它的好处。
引用文档
内核提供了一些系统调用,这些调用提供了最终使用户空间代码频繁使用的地步,以至于此类调用可以支配整体性能。这是由于调用频率以及退出用户空间并进入内核而导致的上下文切换开销所致。
进行系统调用可能很慢,但是触发软件中断以告知内核您希望进行系统调用的代价很高,因为它要经过处理器微代码以及内核中完整的中断处理路径。
一个经常使用的系统调用是gettimeofday(2)。用户空间应用程序直接调用此系统调用。此信息也不是秘密的-任何特权模式下的任何应用程序(root或任何非特权用户)都将获得相同的答案。因此,内核将回答这个问题所需的信息安排在进程可以访问的内存中。现在,对gettimeofday(2)的调用从系统调用变为正常的函数调用以及一些内存访问。
因此,首选vDSO调用作为获取时钟信息的方法,因为它不必通过内核的中断处理路径,但可以更快地调用。
总结一下,Linux AMD64中的当前时间最终来自__vdso_clock_gettime
或__x64_sys_clock_gettime
syscall。要“愚弄”time.Now()
您必须篡改这两种情况。
Windows怪异
细心的读者可能会在timetub.go中询问我们是否使用// +build !windows
。那是怎么回事?
好吧,Windowstime.Now()
直接在程序集中实现,结果是从timeasm.go
文件中命名的。
我们可以在中看到相关的汇编代码sys_windows_amd64.s
。
据我了解,这里的代码路径与Linux情况有些相似。time·now
程序集要做的第一件事是使用功能检查它是否可以使用QPC来获取时间nowQPC
。
CMPB runtime·useQPCTime(SB), $0
JNE useQPC
useQPC:
JMP runtime·nowQPC(SB)
RET
如果不是这种情况,代码将尝试使用结构中的以下两个地址KUSER_SHARED_DATA
,也称为SharedUserData
。该结构保留了一些与用户模式共享的内核信息,以避免类似于vDSO的多次转换到内核。
#define _INTERRUPT_TIME 0x7ffe0008
#define _SYSTEM_TIME 0x7ffe0014
KSYSTEM_TIME InterruptTime;
KSYSTEM_TIME SystemTime;
下面介绍了使用这两个地址的部分。信息以KSYSTEM_TIME
结构形式获取。
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
在于它的分辨率较低,更新周期为100纳秒;这可能就是为什么选择QPC时间的原因。
自从我使用Windows以来已经很久了,但是如果您有兴趣的话, 这里 还有 更多 资源 。
神秘之二
你刚说什么?哦,我们还不知道Local来自何处?
导出的Local *Location
符号首先指向localLoc
地址。
var Local *Location = &localLoc
如前所述,如果该地址为nil,则返回UTC位置。否则,代码localLoc
将sync.Once
在第一次需要位置信息时尝试通过使用原语来设置包级变量。
// 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()
函数查找的内容$TZ
以找到要使用的时区。
如果$TZ
未设置该变量,则Go使用系统默认文件(例如/etc/localtime
,加载时区)。如果已设置但为空,则使用UTC;如果它包含无效的时区,则尝试在系统时区目录中查找具有相同名称的文件。将搜索的默认源是
var zoneSources = []string{
"/usr/share/zoneinfo/",
"/usr/share/lib/zoneinfo/",
"/usr/lib/locale/TZ/",
runtime.GOROOT() + "/lib/time/zoneinfo.zip",
}
有特定zoneinfo_XYZ.go
于平台的文件可使用类似的逻辑来查找默认时区,例如。对于Windows或WASM。过去,当我想在精简后的容器映像中使用时区时,我要做的就是从类似Unix的系统进行构建时,将以下行添加到Dockerfile中。
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
另一方面,在无法控制构建环境的情况下,有一个tzdata
软件包提供了时区数据库的嵌入式副本。如果将此软件包导入任何地方或使用-tags timetzdata
标记进行构建,则程序的大小将增加约450KB,但在Go不能tzdata
在主机系统上找到文件的情况下,也会提供后备功能。
最后,我们可以使用LoadLocation
函数从代码中手动设置位置。用于测试目的。
奖励:funcname1
Go中有什么功能?
在整个Go代码库中,您会看到许多对funcname1()
或的引用funcname2()
,尤其是在获得底层代码时。
据我了解,它们有两个目的:通过更轻松地更改未导出功能的内部,以及将相似和/或链接的功能“组合”在一起,它们有助于跟上Go的兼容性承诺。
尽管有人对此表示嘲笑,但我认为保持代码的可读性和可维护性是一个简单而绝妙的主意