上一讲说到调度器将 main goroutine 推上舞台,为它铺好了道路,开始执行 runtime.main 函数。这一讲,我们探索 main goroutine 以及普通 goroutine 从执行到退出的整个过程。
// The main goroutine.funcmain() {// g = main goroutine,不再是 g0 了 g :=getg()// ……………………if sys.PtrSize ==8 { maxstacksize =1000000000 } else { maxstacksize =250000000 }// Allow newproc to start new Ms. mainStarted =truesystemstack(func() {// 创建监控线程,该线程独立于调度器,不需要跟 p 关联即可运行newm(sysmon, nil) })lockOSThread()if g.m !=&m0 {throw("runtime.main not on m0") }// 调用 runtime 包的初始化函数,由编译器实现runtime_init() // must be before deferifnanotime() ==0 {throw("nanotime returning zero") }// Defer unlock so that runtime.Goexit during init does the unlock too. needUnlock :=truedeferfunc() {if needUnlock {unlockOSThread() } }()// Record when the world started. Must be after runtime_init// because nanotime on some platforms depends on startNano. runtimeInitTime =nanotime()// 开启垃圾回收器gcenable() main_init_done =make(chanbool)// ……………………// main 包的初始化,递归的调用我们 import 进来的包的初始化函数 fn := main_initfn()close(main_init_done) needUnlock =falseunlockOSThread()// ……………………// 调用 main.main 函数 fn = main_mainfn()if raceenabled {racefini() }// ……………………// 进入系统调用,退出进程,可以看出 main goroutine 并未返回,而是直接进入系统调用退出进程了exit(0)// 保护性代码,如果 exit 意外返回,下面的代码会让该进程 crash 死掉for {var x *int32*x =0 }}
参考资料【阿波张 非 goroutine 的退出】中用调试工具验证了非 main goroutine 的退出,感兴趣的可以去跟着实践一遍。
我们继续探索非 main goroutine (后文我们就称 gp 好了)的退出流程。
gp 执行完后,RET 指令弹出 goexit 函数地址(实际上是 funcPC(goexit)+1),CPU 跳转到 goexit 的第二条指令继续执行:
// src/runtime/asm_amd64.s// The top-most function running on a goroutine// returns to goexit+PCQuantum.TEXT runtime·goexit(SB),NOSPLIT,$0-0 BYTE $0x90// NOP CALL runtime·goexit1(SB) // does not return// traceback from goexit1 must hit code range of goexit BYTE $0x90// NOP
直接调用 runtime·goexit1:
// src/runtime/proc.go// Finishes execution of the current goroutine.funcgoexit1() {// …………………… mcall(goexit0)}
调用 mcall 函数:
// 切换到 g0 栈,执行 fn(g)// Fn 不能返回TEXT runtime·mcall(SB), NOSPLIT, $0-8// 取出参数的值放入 DI 寄存器,它是 funcval 对象的指针,此场景中 fn.fn 是 goexit0 的地址 MOVQ fn+0(FP), DIget_tls(CX)// AX = g MOVQ g(CX), AX // save state in g->sched// mcall 返回地址放入 BX MOVQ 0(SP), BX // caller's PC// g.sched.pc = BX,保存 g 的 PC MOVQ BX, (g_sched+gobuf_pc)(AX) LEAQ fn+0(FP), BX // caller's SP// 保存 g 的 SP MOVQ BX, (g_sched+gobuf_sp)(AX) MOVQ AX, (g_sched+gobuf_g)(AX) MOVQ BP, (g_sched+gobuf_bp)(AX)// switch to m->g0 & its stack, call fn MOVQ g(CX), BX MOVQ g_m(BX), BX// SI = g0 MOVQ m_g0(BX), SI CMPQ SI, AX // if g == m->g0 call badmcall JNE 3(PC) MOVQ $runtime·badmcall(SB), AX JMP AX// 把 g0 的地址设置到线程本地存储中 MOVQ SI, g(CX) // g = m->g0// 从 g 的栈切换到了 g0 的栈D MOVQ (g_sched+gobuf_sp)(SI), SP // sp = m->g0->sched.sp// AX = g,参数入栈 PUSHQ AX MOVQ DI, DX// DI 是结构体 funcval 实例对象的指针,它的第一个成员才是 goexit0 的地址// 读取第一个成员到 DI 寄存器 MOVQ 0(DI), DI// 调用 goexit0(g) CALL DI POPQ AX MOVQ $runtime·badmcall2(SB), AX JMP AX RET
函数参数是:
typefuncvalstruct { fn uintptr// variable-size, fn-specific data here}
L13 将 mcall 的返回地址保存到 gp 的 g.sched.pc 字段,L14 将 gp 的栈顶,也就是 SP 保存到 BX 寄存器,L16 将 SP 保存到 gp 的 g.sched.sp 字段,L17 将 g 保存到 gp 的 g.sched.g 字段,L18 将 BP 保存 到 gp 的 g.sched.bp 字段。这一段主要是保存 gp 的调度信息。