go version go1.16.4 darwin/amd64
平时阅读go源码的时候,总能发现某处调用了mcall,这个函数的作用是从当前g的栈切换到g0的栈,并调用函数fn(g),并将BP/PC/SP保存到g.sched,因此g后续被调度到的时候能够恢复到g的栈空间,fn不可以有返回值,因为fn都会调用schedule开始新一轮的调度循环,让m运行其他的g,而当前的g由于调用了mcall而放弃运行.
随便找一个调用mcall的地方看下:
func asyncPreempt2() {
gp := getg()
gp.asyncSafePoint = true
if gp.preemptStop {
mcall(preemptPark)
} else {
mcall(gopreempt_m)
}
gp.asyncSafePoint = false
}
当前的asyncPreempt2栈空间如下:
分析下
mcall函数:
TEXT runtime·mcall(SB), NOSPLIT, $0-8 //0代表mcall的栈空间为0,8代表有一个参数大小为8
MOVQ fn+0(FP), DI //DI=&fn
get_tls(CX) //CX=线程本地存储
MOVQ g(CX), AX // save state in g->sched AX=g
MOVQ 0(SP), BX // caller's PC //BX=PC=调用mcall时候,下一跳指令的地址
MOVQ BX, (g_sched+gobuf_pc)(AX) //g.sched.gobuf.pc = BX=调用mcall时候,下一跳指令的地址
LEAQ fn+0(FP), BX // caller's SP // 这里因为mcall的栈大小为0,所以FP也代表栈顶
MOVQ BX, (g_sched+gobuf_sp)(AX) // g.sched.gobuf.sp = caller's SP
MOVQ BP, (g_sched+gobuf_bp)(AX) // g.sched.gobuf.bp = BP
// switch to m->g0 & its stack, call fn //切换到g0栈,并调用fn
MOVQ g(CX), BX // BX=g
MOVQ g_m(BX), BX //BX=m
MOVQ m_g0(BX), SI //SI=g0
CMPQ SI, AX // if g == m->g0 call badmcall //g不能等于g0
JNE 3(PC) //g!=g0,跳转到下面
MOVQ $runtime·badmcall(SB), AX
JMP AX
// 跳转到这里
MOVQ SI, g(CX) // g = m->g0
MOVQ SI, R14 // set the g register
MOVQ (g_sched+gobuf_sp)(SI), SP // sp = m->g0->sched.sp
PUSHQ AX //将g放到栈上,作为参数
MOVQ DI, DX // DX=&fn
MOVQ 0(DI), DI //DI=fn
CALL DI //调用fn
POPQ AX //fn永远不会返回,所以这里根本走不到,如果走到了,下面会报错的
MOVQ $runtime·badmcall2(SB), AX
JMP AX
RET
当调用了mcall时候,栈空间又如下:
这样,就完成了
g到g0的栈空间切换,接下来执行gopreempt_m
func gopreempt_m(gp *g) {
if trace.enabled {
traceGoPreempt()
}
goschedImpl(gp)
}
func goschedImpl(gp *g) {
status := readgstatus(gp)
if status&^_Gscan != _Grunning {
dumpgstatus(gp)
throw("bad g status")
}
casgstatus(gp, _Grunning, _Grunnable)
dropg()
lock(&sched.lock)
globrunqput(gp)
unlock(&sched.lock)
schedule()
}
func schedule() {
...
gp = 从队列里获取
execute(gp, inheritTime)
}
func execute(gp *g, inheritTime bool) {
...
gogo(&gp.sched)
}
// func gogo(buf *gobuf)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $0-8
MOVQ buf+0(FP), BX // BX=g.sched
MOVQ gobuf_g(BX), DX // DX=g.sched.g
MOVQ 0(DX), CX // make sure g != nil
JMP gogo<>(SB)
TEXT gogo<>(SB), NOSPLIT, $0
get_tls(CX)
MOVQ DX, g(CX) //把g放入线程本地存储中,
MOVQ DX, R14 // set the g register
// 下面几个指令是恢复寄存器的值
MOVQ gobuf_sp(BX), SP // SP=g.sched.sp
MOVQ gobuf_ret(BX), AX // AX=g.sched.ret
MOVQ gobuf_ctxt(BX), DX // DX=g.sched.ctxt
MOVQ gobuf_bp(BX), BP // BP=g.sched.bp
// 因为.gsched的值都已经恢复到寄存器里了,后面用不到了,所以下面几个指令是清空g.sched的值,减少gc工作量
MOVQ $0, gobuf_sp(BX)
MOVQ $0, gobuf_ret(BX)
MOVQ $0, gobuf_ctxt(BX)
MOVQ $0, gobuf_bp(BX)
MOVQ gobuf_pc(BX), BX // BX=g.sched.PC = 调用mcall时候的下一条指令
JMP BX // 跳转到BX
此时,
asyncPreempt2继续往下运行.
下图是本文的全局流程分析: