This is a creation in Article, where the information may have evolved or changed.
This paper mainly discusses the management of the timer heap in go and the question of how time is acquired by the runtime, which leads to the conclusion that we can rely on the accuracy of the timer.
First, let's look at how go gets time, find time.Now
out, and find that the final call is the following assembly function.
//Func Now (sec Int64, nsec Int32) TEXT time Now (SB), nosplit,$16//Be careful. We ' re calling a function with GCC calling convention here. We ' re guaranteed bytes on entry, and we ' ve taken, and the//call uses another 8. That's leaves 104 for the gettime code to use. Hope that ' s enough! There is only guaranteed to call the GetTime function when there are 104 bytes to the function as a stack//because the return value uses 16 bytes, the function itself will use 8 bytes, to call gettime only 104 bytes left to use. MOVQ runtime __vdso_clock_gettime_sym (SB), ax CMPQ Ax, $ JEQ fallback movl $, DI//Clock_realtime Leaq 0 (SP), SI call ax Movq 0 (SP), Ax//sec Movq 8 (SP), DX//Nsec movq AX, sec+0 (FP ) Movl DX, Nsec+8 (FP) RETFALLBACK:LEAQ 0 (SP), DI movq $, SI movq runtime __vdso_gettimeofday_ Sym (SB), ax call ax Movq 0 (SP), Ax/sec movl 8 (SP), DX//USEC IMULQ $1000, DX movq AX, Sec+0 (FP) movl DX, Nsec+8 (FP) RET
The first line TEXT time·now(SB),NOSPLIT,$16
, which represents the address of the time·now(SB)
function, the now
NOSPLIT
flag does not depend on the parameter, $16
indicates that the returned content is 16 bytes, and the assembly syntax of Go can refer to the Go ASM document [1].
The first is to __vdso_clock_gettime_sym(SB)
take out (this is a symbol, pointing to is actually the Clock_gettime function, man clock_gettime can see the definition of glibc) address, if the symbol is not empty, the address of the top of the stack to calculate the incoming Si (LEA command). Di and Si are system call
the first two parameters of the register, which corresponds to the call clock_gettime(0,&ret)
. The fallback branch is a function that is called back when the corresponding symbol is not initialized and then calls gettimeofday
(man gettimeofday can see the definition of libc).
The function call of go ensures that there will be at least 128 bytes of stack (note not the stack of goroutine), can be referenced runtime/stack.go
_StackSmall
, but after entering the corresponding C function, the stack growth can not be controlled by go, so the remaining 104 bytes to ensure that the call will not stack overflow . (but generally not, because these two get-time functions are not complex).
First of all, the function of the symbol, consult the relevant data can be seen, VDSO is virtual Dynamic Shared Object, is the core provided by the dummy. So, the. So file is not on disk, but in the kernel, mapped to user space. The details can be seen in an answer to so [2]. There is some description in one of the links [3].
One-to-obtaining a fast gettimeofday () is by writing the current time in a fixed place, on a page mapped into the memo Ry of all applications, and updating this is on each clock interrupt.
These applications could then read the fixed location with a single instruction-no system call required.
Concerning Gettimeofday (), a vsyscall version for the x86-64 was already part of the vanilla kernel.
Patches for i386 exist. (An example of the kind of timing differences:john Stultz reports in an experiment where he measures gettimeofday () and F Inds 1.67 us for the int 0x80 to, 1.24 us for the sysenter, and 0.88 us for the Vsyscall.)
In short, it is a mechanism plus compatibility mode that accelerates system calls. A gettimeofday
function like this is that if there are too many context switches like normal system calls, especially some programs that get time frequently, it is generally gettimeofday
called through this mechanism. A single address is mapped in user space, with some system calls from the kernel exposed, This may be syscall or int 80 or systenter, the kernel determines the faster call mode, to prevent the glibc version and kernel version incompatibility issues. (Vdso is an upgraded version of Vsyscall that avoids some security issues and the mappings are no longer statically pinned).
It can be seen from the kernel that the fetch system call is updated by the time interrupt, and its call stack is as follows [5].
Hardware timer interrupt (generated by the Programmable interrupt Timer-pit)
Tick_periodic ();
Do_timer (1);
Update_wall_time ();
-Timekeeping_update (TK, false);
-Update_vsyscall (TK);
update_wall_time
The time of clock source is used, and the accuracy can reach the NS level.
However, the general Linux kernel time interrupt is 100HZ, High also has 1000HZ, that is to say this time is usually in 10ms or 1ms interrupt processing update once.
From the operating system point of view the time granularity is probably the MS level. However, this is just a benchmark value. The clock source is still available for each acquisition of time
Time (the clock source has many kinds of [9], may be the hardware counter, may also be the interrupt jiffy, generally can reach NS). Gets the precision of the time or can be between us and hundreds of NS.
Because the system call itself takes time, the theoretically more precise time is not obtained through this system call, but rather requires a direct use of the assembly instruction "RDTSC" to read the CPU cycle to get accurate time.
Next, the process of acquiring the function symbol of time involves some content of elf, in fact it is the process of dynamic linking, the address of the function symbol in. So is parsed into the function pointer, for example __vdso_clock_gettime_sym
, the content of the link can read the "Programmer self-cultivation" book [4].
Other functions, for example TEXT runtime·nanotime(SB),NOSPLIT,$16
, are similar processes. This function can get the time.
Read the process of time acquisition, and then look at the part of the go runtime, and see timer heap
how it is managed.
// Package time knows the layout of this structure.// If this struct changes, adjust ../time/sleep.go:/runtimeTimer.// For GOOS=nacl, package syscall knows the layout of this structure.// If this struct changes, adjust ../syscall/net_nacl.go:/runtimeTimer.type timer struct { i int // heap index // Timer wakes up at when, and then at when+period, ... (period > 0 only) // each time calling f(now, arg) in the timer goroutine, so f must be // a well-behaved function and not block. when int64 period int64 f func(interface{}, uintptr) arg interface{} seq uintptr }
The timer is managed in the form of heap, which is a fully binary tree, which can be saved with an array, and I is the index of the heap.
When is the time when the Goroutine is awakened, the period is the wake-up gap, the next wake-up time is when+period, and so on.
Call f(now, arg)
the function, now is the timestamp.
var timers struct { lock mutex gp *g created bool sleeping bool rescheduling bool waitnote note t []*timer }
The entire timer heap timers
is managed by. GP points to a g
pointer, the G structure in the scheduler, a goroutine state-maintained structure that points to a single goroutine of the time manager, This is not a user but a goroutine that starts at run time. (Of course, only using a timer will start, or there will be no such goroutine). lock
Ensure thread safety for timers. Waitnote is a conditional variable.
Look at the addtimer
function, he is the beginning of the entire timer entrance, of course, it just added a lock.
func addtimer(t *timer) { lock(&timers.lock) addtimerLocked(t) unlock(&timers.lock) }
Then addtimerLocked
you can see that it is the process of loading into the heap.
ADD a timer to the heap and start or kick the timer proc.//If The new timer was earlier than any of the others.//timer S is locked. Func addtimerlocked (t *timer) {//When must never is negative; otherwise timerproc would overflow//during Its delta calculation and never expire other runtime timers. If T.when < 0 {t.when = 1<<63-1} t.i = Len (TIMERS.T) TIMERS.T = append (timers.t, T) Siftuptimer (T.I) If t.i = = 0 {//SIF Tup moved to Top:new earliest deadline. If timers.sleeping {timers.sleeping = False Notewakeup (&tim Ers.waitnote)} if timers.rescheduling {timers.rescheduling = False Goready (TIMERS.GP, 0)}} if!timers.created {timers.Created = True Go Timerproc ()}}
Start with the following if!timers.created
branch, and if timers does not create a timerproc for go, Timeproc is defined as follows.
Timerproc runs the Time-driven events.//It sleeps until the next event in the Timers heap.//If AddTimer Inserts a new Earlier event, Addtimer1 wakes Timerproc early.func Timerproc () {timers.gp = GETG () for {L Ock (&timers.lock) timers.sleeping = false Now: = Nanotime () Delta: = INT6 4 ( -1) for {if Len (timers.t) = = 0 {Delta =-1 Break} t: = Timers.t[0] Del TA = t.when-now If delta > 0 {break} If T.period > 0 {//leave in heap and adjust next time to fire T.when + = T.period * (1 +-delta/t.period) Siftdowntimer (0) } ELSE {//Remove from heap Last: = Len (timers.t)-1 If last > 0 {timers.t[0] = Timers.t[last] timers.t[0].i = 0} Timers.t[last] = Nil TIMERS.T = Timers.t[:last] If last > 0 { Siftdowntimer (0)} T.I = 1// Mark as removed} f: = t.f arg: = T.arg Seq: = T.seq Unlock (&timers.lock) if raceenabled { Raceacquire (unsafe. Pointer (t))} f (ARG, seq) lock (&tiMers.lock)} If Delta < 0 | | Faketime > 0 {//No timers Left-put goroutine to sleep. Timers.rescheduling = TRUE//If there is no timers remaining, let G enter the sleep state. Goparkunlock's role is to unlock G and M connections, let Goroutine sleep and M//look for the next G to execute. Goparkunlock (&timers.lock, "Timer goroutine (Idle)", Traceevgoblock, 1) continue }//At least one timer pending. Sleep until then. Timers.sleeping = TRUE//Clear 0 waitnote. Noteclear (&timers.waitnote) Unlock (&timers.lock)//Call M structure corresponding os-dependent, os-thread signal volume let m enter s Leep status. It will only be awakened when the time-out or the condition variable waitnote triggered. NOTETSLEEPG (&timers.waitnote, Delta)}}
The main body of Timeproc is to remove the timer from the minimum heap and then the callback function, if the period is greater than 0 to change the time of the timer and save it back to the heap and adjust, if less than 0 is directly deleted (corresponding to the standard library of Ticker and timer), Enter the OS semaphore to wait for the next processing of sleep, (of course, can be awakened by waitnote variable), so that the loop, the white is by the semaphore timeout mechanism to achieve the runtime sleep[8]. And then when there's no timer left at all, The G-structure indicates that the goroutine goes to sleep, and the os-thread represented by the m structure that hosts Goroutine will look for other operational goroutine executions. Details of the runtime schedule can be found in the notes of the rain-mark Uncle [7].
After reading this case, go back addtimerLocked
, when adding a new timer
, will be checked, that is, the newest insert timer
is on the heap top words, will wake up the sleep of the timergorountine began to check the heap on the timer
outdated and execute. Here the wake and previous sleep are the two corresponding states that timers.sleeping
are entering the M OS semaphore sleep.
timers.rescheduling
is to enter the g of the scheduled sleep, and M does not sleep, let G back into the operational state. Time-outs and the addition of new timer, both combine to become the timer
driving force of the runtime.
After reading the implementation of time, and then back to answer the original question of the article "How accurate the timer can be", in fact, and two reasons, one is the operating system itself, the time granularity, usually the US level, the time benchmark update is the MS level, the time accuracy can reach the US level, the other is timer
Its own goroutine scheduling problem, if the runtime load is too large or the operating system itself is too large, will cause timer
its own goroutine response is not timely, causing the timer trigger is not in time, May result in a 20ms timer and a 30ms timer between the same as concurrency (this is also the problem that I encountered, especially some of the cgroup restricted container environment, CPU time distribution under the condition of a few), So sometimes we can't trust the timing of the timer to keep the program running properly. NewTimer
The note also highlights "Newtimer creates a new Timer that would send the current time on its channel after at least duration D.", no Some people can guarantee that the timer executes by point, of course, if your interval is ridiculously large, you can ignore the impact:)
```
Reference Links:
- Go Assembly https://golang.org/doc/asm
- http://stackoverflow.com/questions/19938324/ What-are-vdso-and-vsyscall
- http://www.win.tue.nl/~aeb/linux/lk/lk-4.html
- /http book.douban.com/subject/3652388/
- http://linuxmogeb.blogspot.com/2013/10/ how-does-clockgettime-work.html
- Clock source http://blog.csdn.net/droidphone/article/details/7975694
- Go Source Anatomy HTTPS://GITHUB.COM/QYUHEN/BOOK/BLOB/MASTE/GO%201.5%20%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90.PDFR
- Implementation of the semaphore with timeout http://lxr.free-electrons.com/source/kernel/locking/semaphore.c#L204 the clock source of the
- kernel http// blog.csdn.net/droidphone/article/details/7975694