Starting with Go 1.14, the Go language implements true “preemption” during scheduling and GC through the use of signals.
The preemption process sends SIGURG signals from the preemption originating thread to the preempt thread.
When the preempt thread receives the signal, it enters the SIGURG process and forces the asyncPreempt call into the user’s currently executing code location.
This section provides a detailed analysis of this process.
Preempt the timing of initiation
Preemption occurs when:
- During the period of STW
- During execution of the Safe Point function on P
- Sysmon background monitoring
- Gc Pacer allocates a new dedicated worker during the session
- During a Panic crash
With the exception of stack scans, all triggered preemption ends up executing the preemptone function. The stack scanning process is special:
From these processes, let’s pick out three.
STW preemption
Above is a GC flow diagram for the current Go language, where both STW phases require stopping goroutine running on the executing thread.
func stopTheWorldWithSema() { ..... preemptall() ..... If {for {// Wait for 100us, then try to re-preempt in case of any races // Wait for 100us, If notetsleep(&sched.stopnote, 100*1000) {noteclear(&sched.stopnote) break} preemptall()}}Copy the code
The GC stack scanning
The goroutine stack is the root of the GC scan, and all markroots need to stop the user’s Goroutine, mainly in the running state:
func markroot(gcw *gcWork, i uint32) {
// Note: if you add a case here, please also update heapdump.go:dumproots.
switch {
......
default:
// the rest is scanning goroutine stacks
var gp *g
......
// scanstack must be done on the system stack in case
// we're trying to scan our own stack.
systemstack(func() {
stopped := suspendG(gp)
scanstack(gp, gcw)
resumeG(stopped)
})
}
}
Copy the code
SuspendG calls preemptM -> signalM to send preemption signals to the thread where the goroutine is executing.
Sysmon background monitoring
func sysmon() { idle := 0 // how many cycles in succession we had not wokeup somebody for { ...... // retake P's blocked in syscalls // and preempt long running G's if retake(now) ! = 0 { idle = 0 } else { idle++ } } }Copy the code
If syscall is executed for too long, P needs to be removed from M. Running user code for too long requires preemption to stop the goroutine execution. Here we only look at preempting goroutine:
const forcePreemptNS = 10 * 1000 * 1000 // 10ms func retake(now int64) uint32 { ...... for i := 0; i < len(allp); i++ { _p_ := allp[i] s := _p_.status if s == _Prunning || s == _Psyscall { // Preempt G if it's running for too long. t := int64(_p_.schedtick) if int64(pd.schedtick) ! = t { pd.schedtick = uint32(t) pd.schedwhen = now } else if pd.schedwhen+forcePreemptNS <= now { preemptone(_p_) } } . }... }Copy the code
Principle of cooperative preemption
The key to cooperative preemption is cooperative. The timing of preemption does not vary much between versions. Let’s focus on the cooperative process.
Function header, function tail insertion stack expansion check
When a function is called in Go, if the function framesize > 0, goroutine stack expansion may occur when the function is called. In this case, a sink code is inserted in the function header and the function tail respectively:
package main
func main() {
add(1, 2)
}
//go:noinline
func add(x, y int) (int, bool) {
var z = x + y
println(z)
return x + y, true
}
Copy the code
The add function generates the following result after using the go tool compile-s:
// After go1.17, the calling convention of the function has changed // 1.17 will also be different from the assembly code in previous versions of the header, Add STEXT size=103 args=0x20 locals=0x18 0x0000 00000 (add.go:8) TEXT "".add(SB), ABIInternal, $24-32 0x0000 00000 (add.go:8) MOVQ (TLS), CX 0x0009 00009 (add.go:8) CMPQ SP, 16(CX) 0x000d 00013 (add.go:8) JLS 96 ...... func body 0x005f 00095 (add.go:11) RET 0x0060 00096 (add.go:11) NOP 0x0060 00096 (add.go:8) CALL runtime.morestack_noctxt(SB) 0x0065 00101 (add.go:8) JMP 0Copy the code
TLS stores a pointer to G, and an offset of 16 bytes is stackGuard0 in the structure of G. The goroutine stack is also growing from high to low, so the current SP < stackGuard0 indicates that the stack needs to be expanded.
Scheduling logic in MoreStack
/ / morestack_noctxt is a simple method of assembly / / jump straight to the morestack TEXT, runtime morestack_noctxt (SB), NOSPLIT | NOFRAME, $0-0 MOV ZERO, CTXT JMP runtime· moreStack (SB) TEXT Runtime ·morestack(SB),NOSPLIT,$0-0...... // Call newstack on m->g0's stack. MOVQ m_g0(BX), BX MOVQ BX, G (CX) MOVQ (g_sched+ GOBUF_sp)(BX), SP CALL Runtime · newStack (SB)...... RETCopy the code
Morestack saves the live goroutine in the gobuf of the current Goroutine, switches the execution stack to G0, and then executes runtime.newstack on g0.
Before signal preemption is implemented, the goroutine responsible for GC stack scanning does not know when the user’s G will stop, so scanStack can only set the flag bit of preemptscan, and finally the stack scanning must be matched by newStack. The following newStack is an implementation of Go 1.13:
func newstack() { thisg := getg() gp := thisg.m.curg preempt := atomic.Loaduintptr(&gp.stackguard0) == stackPreempt if preempt { if thisg.m.locks ! = 0 || thisg.m.mallocing ! = 0 || thisg.m.preemptoff ! = "" || thisg.m.p.ptr().status ! = _Prunning {gp.stackGuard0 = gp.stack.lo + _StackGuard gogo(&gp.sched) // never return}} if preempt {// to scang // Older versions of newStack and GC Scan procedures have heavier coupling casgStatus (gp, _Grunning, _Gwaiting) if gp.preemptscan {for! castogscanstatus(gp, _Gwaiting, _Gscanwaiting) { } if ! Gp.gcscandone {GCW := &gp.m.p.ptr().gcw Scanstack (gp, gcw) gp.gcscandone = true } gp.preemptscan = false gp.preempt = false casfrom_Gscanstatus(gp, _Gscanwaiting, _Gwaiting) casgstatus(gp, _Gwaiting, _Grunning) gp.stackguard0 = gp.stack.lo + _StackGuard gogo(&gp.sched) // never return } casgstatus(gp, _Gwaiting, _Grunning) gopreempt_m(gp) // never return } ...... }Copy the code
After preemption, the current goroutine is placed in the global queue:
func gopreempt_m(gp *g) { goschedImpl(gp) } func goschedImpl(gp *g) { status := readgstatus(gp) ...... casgstatus(gp, _Grunning, _Grunnable) dropg() lock(&sched.lock) globrunqput(gp) // Puts the current goroutine into the global queue unlock(&sched.lock) schedule() // The current thread re-enters the scheduling loop}Copy the code
Newstack after signal preemption
With signal preemption, there is some expectation of when the user’s goroutine will terminate, so newStack no longer needs to couple scanstack’s logic. A new version of NewStack is implemented as follows:
func newstack() { thisg := getg() gp := thisg.m.curg preempt := atomic.Loaduintptr(&gp.stackguard0) == stackPreempt if preempt { if ! CanPreemptM (thisg.m) {// let goroutine continue // next time preempt it gp.stackGuard0 = gp.stack.lo + _StackGuard gogo(&gp.sched) // never Return}} if preempt {// If GC needs to initiate a goroutine stack scan // preemptStop is set to true Gp.preemptstop {preemptPark(gp) // never returns Gopreempt_m (gp) // never return}...... Normal stack extension logic}Copy the code
CanPreemptM is used in NewStack to determine which scenarios are suitable for preemption and which are not. If the current goroutine is running (status == RUNNING) and any of the following is true:
- Hold lock (mainly write lock, read lock actually can not tell);
- Memory allocation in progress
- Preemptoff is not empty
It should not preempt and will be judged the next time it enters NewStack.
Non-cooperative preemption
Non-cooperative preemption is achieved through signal processing. So let’s just focus on the SIGURG process.
Signal processing initialization
When m0(the first thread when the program starts) initializes, initialization of signal processing occurs:
// mstartm0 implements part of mstart1 that only runs on the m0. func mstartm0() { initsig(false) } // Initialize signals. func initsig(preinit bool) { for i := uint32(0); i < _NSIG; i++ { setsig(i, funcPC(sighandler)) } } var sigtable = [...] sigTabT{ ...... /* 23 */ {_SigNotify + _SigIgn, "SIGURG: urgent condition on socket"}, ...... }Copy the code
Finally, we execute sigAction:
MOVQ sig+0(FP), DI MOVQ new+8(FP), SI MOVQ old+16(FP), DX MOVQ size+24(FP), R10 MOVL $SYS_rt_sigaction, AX SYSCALL MOVL AX, ret+32(FP) RETCopy the code
Not much different from regular Syscall.
The process of signal processing initialization is simple, which is to attach sighandler to all known signals that need processing.
Send a signal
Func preempTONE (_p_ *p) bool {mp := _p_.m.ptr() gp := mp.curg gp.preempt = true gP.stackGuard0 = stackPreempt // Send the packet to the thread SIGURG if preemptMSupported && debug. asyncPreemptoff == 0 {_p_. Preempt = true preemptM(mp)} return true}Copy the code
PreemptM’s flow is linear:
func preemptM(mp *m) {
if atomic.Cas(&mp.signalPending, 0, 1) {
signalM(mp, sigPreempt)
}
}
func signalM(mp *m, sig int) {
tgkill(getpid(), int(mp.procid), sig)
}
Copy the code
Finally, use the tgkill syscall to send the signal to the thread with the specified ID:
TEXT ·tgkill(SB),NOSPLIT,$0
MOVQ tgid+0(FP), DI
MOVQ tid+8(FP), SI
MOVQ sig+16(FP), DX
MOVL $SYS_tgkill, AX
SYSCALL
RET
Copy the code
Processing of received signals
When thread M receives the signal, it switches from user stack G to GSignal to execute the signal processing logic, which is known as sighandler flow:
func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {
_g_ := getg()
c := &sigctxt{info, ctxt}
......
if sig == sigPreempt && debug.asyncpreemptoff == 0 {
doSigPreempt(gp, c)
}
......
}
Copy the code
If a preempt signal is received, the doSigPreempt logic is executed:
Func doSigPreempt(gp *g, CTXT *sigctxt) {if wantAsyncPreempt(gp) {if ok, newpc := isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()); ok { // Adjust the PC and inject a call to asyncPreempt. ctxt.pushCall(funcPC(asyncPreempt), newpc) } } ...... }Copy the code
IsAsyncSafePoint filters out some scenarios that should not be preempted, including:
- The current code executes in a function written in assembly
- The code is executed in the Runtime, Runtime /internal, or Reflect package
PushCall in the doSigPreempt code is a key step:
func (c *sigctxt) pushCall(targetPC, resumePC uintptr) {
// Make it look like we called target at resumePC.
sp := uintptr(c.rsp())
sp -= sys.PtrSize
*(*uintptr)(unsafe.Pointer(sp)) = resumePC
c.set_rsp(uint64(sp))
c.set_rip(uint64(targetPC))
}
Copy the code
PushCall pushes the address of the next line of code that the user will execute directly onto the stack, and JMP executes the code at the specified target address:
before
----- PC = 0x123
local var 1
-----
local var 2
----- <---- SP
Copy the code
after
----- PC = targetPC
local var 1
-----
local var 2
-----
prev PC = 0x123
----- <---- SP
Copy the code
The target in this case is asyncPreempt.
AsyncPreempt performs process analysis
AsyncPreempt is divided into an upper part and a lower part, separated by asyncPreempt2. The top half is responsible for saving all the registers in the current execution scene of Goroutine to the current run stack.
The lower half is responsible for restoring these scenes after asyncPreempt2 returns.
TEXT, asyncPreempt < ABIInternal > (SB), NOSPLIT | NOFRAME, $0-0 PUSHQ BP MOVQ SP, BP... Save 1 MOVQ AX, 0(SP) MOVQ CX, 8(SP) MOVQ DX, 16(SP) MOVQ BX, 24(SP) MOVQ SI, 32(SP)...... Save site 2 MOVQ R15, 104(SP) MOVUPS X0, 112(SP) MOVUPS X1, 128(SP)...... MOVUPS X15, 352(SP) CALL ·asyncPreempt2(SB) MOVUPS 352(SP), X15...... Recovery scene 2 MOVUPS 112(SP), X0 MOVQ 104(SP), R15...... Recovery site 1 MOVQ 8(SP), CX MOVQ 0(SP), AX...... RETCopy the code
AsyncPreempt2 has two branches:
Func asyncPreempt2() {gp := getg() gp.asyncSafePoint = true if gp.preemptStop {// This preemptStop is set to true only during GC stack scans McAll (gopreempt_m)} gp.asyncSafePoint = false} McAll (gopreempt_m)} gp.asyncSafePoint = falseCopy the code
GC stack scans the if branch and else branch in all cases except stack scan.
Stack scan preemption process
SuspendG -> preemptM -> signalM sends signals.
Sighandler -> asyncPreempt -> Save execution scene -> asyncPreempt2 -> preemptPark sighandler -> asyncPreempt -> Save execution scene -> asyncPreempt2 -> preemptPark
PreemptPark, like Gopark, suspends the currently executing Goroutine so that the threads previously bound to the goroutine can continue the scheduling loop.
After executing scanstack:
ResumeG -> ready -> runqput puts the previously stopped Goroutine into the current P queue or global queue.
Other process
Preemptone -> preemptM – signalM Indicates the signal.
Sighandler -> asyncPreempt -> Save execution scene -> asyncPreempt2 -> gopreempt_m sighandler -> asyncPreempt -> save execution scene -> asyncPreempt2 -> gopreempt_m
Gopreempt_m puts the preempted Goroutine directly into the global queue.
When a Goroutine program is scheduled, whether for a stack scan or any other process, it starts execution from the next instruction in the assembly called ·asyncPreempt2(SB), the lower half of the asyncPreempt assembly function.
This will completely restore the goroutine scene as if the preemption never happened.
Welcome to follow my official account: TechPaper