源码分析go的mcall&gogo

781 阅读1分钟

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栈空间如下: image.png 分析下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时候,栈空间又如下:

image.png 这样,就完成了gg0的栈空间切换,接下来执行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

image.png 此时,asyncPreempt2继续往下运行. 下图是本文的全局流程分析:

image.png