源码分析go调度器四: 非main goroutine的调度循环
本节的重点主要有:
非main goroutine如何返回到goexit的
mcall如何切换到g0继续执行的
调度循环
有如下代码
package main import "time" func hello() { println("msg") } func main() { go hello() time.Sleep(time.Hour * 1) } 复制代码
编译go build -gcflags "-N -l" -ldflags=-compressdwarf=false main.go
使用gdb调试:gdb main
(gdb) b main.hello Breakpoint 1 at 0x1057240: file /Users/zixuan.xu/workspace/study/learn-go-plan9/goroutine/main.go, line 5. (gdb) r Starting program: /Users/zixuan.xu/workspace/study/learn-go-plan9/goroutine/main [New Thread 0x1a03 of process 88861] [New Thread 0x1b03 of process 88861] warning: unhandled dyld version (17) [New Thread 0x1d03 of process 88861] [New Thread 0x1f07 of process 88861] [New Thread 0x2307 of process 88861] [New Thread 0x5403 of process 88861] [Switching to Thread 0x2307 of process 88861] Thread 5 hit Breakpoint 1, main.hello () at /Users/zixuan.xu/workspace/study/learn-go-plan9/goroutine/main.go:5 5 func hello() { (gdb) disas Dump of assembler code for function main.hello: => 0x0000000001057240 <+0>: cmp 0x10(%r14),%rsp 0x0000000001057244 <+4>: jbe 0x1057279 <main.hello+57> 0x0000000001057246 <+6>: sub $0x18,%rsp 0x000000000105724a <+10>: mov %rbp,0x10(%rsp) 0x000000000105724f <+15>: lea 0x10(%rsp),%rbp 0x0000000001057254 <+20>: call 0x102d2e0 <runtime.printlock> 0x0000000001057259 <+25>: lea 0xcc55(%rip),%rax # 0x1063eb5 <go.string.*+309> 0x0000000001057260 <+32>: mov $0x4,%ebx 0x0000000001057265 <+37>: call 0x102dc00 <runtime.printstring> 0x000000000105726a <+42>: call 0x102d360 <runtime.printunlock> 0x000000000105726f <+47>: mov 0x10(%rsp),%rbp 0x0000000001057274 <+52>: add $0x18,%rsp 0x0000000001057278 <+56>: ret 0x0000000001057279 <+57>: call 0x1052da0 <runtime.morestack_noctxt> 0x000000000105727e <+62>: xchg %ax,%ax 0x0000000001057280 <+64>: jmp 0x1057240 <main.hello> End of assembler dump. (gdb) b *0x0000000001057278 Breakpoint 2 at 0x1057278: file /Users/zixuan.xu/workspace/study/learn-go-plan9/goroutine/main.go, line 7. (gdb) c Continuing. msg Thread 5 hit Breakpoint 2, 0x0000000001057278 in main.hello () at /Users/zixuan.xu/workspace/study/learn-go-plan9/goroutine/main.go:7 7 } (gdb) disas Dump of assembler code for function main.hello: 0x0000000001057240 <+0>: cmp 0x10(%r14),%rsp 0x0000000001057244 <+4>: jbe 0x1057279 <main.hello+57> 0x0000000001057246 <+6>: sub $0x18,%rsp 0x000000000105724a <+10>: mov %rbp,0x10(%rsp) 0x000000000105724f <+15>: lea 0x10(%rsp),%rbp 0x0000000001057254 <+20>: call 0x102d2e0 <runtime.printlock> 0x0000000001057259 <+25>: lea 0xcc55(%rip),%rax # 0x1063eb5 <go.string.*+309> 0x0000000001057260 <+32>: mov $0x4,%ebx 0x0000000001057265 <+37>: call 0x102dc00 <runtime.printstring> 0x000000000105726a <+42>: call 0x102d360 <runtime.printunlock> 0x000000000105726f <+47>: mov 0x10(%rsp),%rbp 0x0000000001057274 <+52>: add $0x18,%rsp => 0x0000000001057278 <+56>: ret 0x0000000001057279 <+57>: call 0x1052da0 <runtime.morestack_noctxt> 0x000000000105727e <+62>: xchg %ax,%ax 0x0000000001057280 <+64>: jmp 0x1057240 <main.hello> End of assembler dump. (gdb) si runtime.goexit () at /Users/zixuan.xu/.gvm/gos/go1.17/src/runtime/asm_amd64.s:1582 1582 CALL runtime·goexit1(SB) // does not return (gdb) disas Dump of assembler code for function runtime.goexit: 0x0000000001053060 <+0>: nop => 0x0000000001053061 <+1>: call 0x10551e0 <runtime.goexit1> 0x0000000001053066 <+6>: nop End of assembler dump. 复制代码
在main.hello下断点,并执行到这里
在ret处下断点,并执行到ret位置
使用si进入ret的调用,能看到进入的是goexit这个函数
asm_amd64.s:1580
// The top-most function running on a goroutine // returns to goexit+PCQuantum. TEXT runtime·goexit(SB),NOSPLIT|TOPFRAME,$0-0 BYTE $0x90 // NOP CALL runtime·goexit1(SB) // does not return // traceback from goexit1 must hit code range of goexit BYTE $0x90 // NOP 复制代码
其实si调用完,指向的指令就是goexit的第二行指令,就是上一节中newg.sched.sp
字段。
现在也可以证明非girouitne执行完并不会退出
proc.go:3622
接下来调用goexit1
// Finishes execution of the current goroutine. func goexit1() { if raceenabled { racegoend() } if trace.enabled { traceGoEnd() } mcall(goexit0) } 复制代码
这里通过mcall
从goroutine
的栈切换到g0
的栈
asm_amd64.s:282
// func mcall(fn func(*g)) // Switch to m->g0's stack, call fn(g). // Fn must never return. It should gogo(&g->sched) // to keep running g. #ifdef GOEXPERIMENT_regabiargs TEXT runtime·mcall<ABIInternal>(SB), NOSPLIT, $0-8 MOVQ AX, DX // DX = fn // save state in g->sched MOVQ 0(SP), BX // caller's PC MOVQ BX, (g_sched+gobuf_pc)(R14) // newg.sched.pc=return addr LEAQ fn+0(FP), BX // caller's SP // BX=&fn MOVQ BX, (g_sched+gobuf_sp)(R14) //newg.sched.sp=&fn MOVQ BP, (g_sched+gobuf_bp)(R14) //newg.sched.bp = BP // switch to m->g0 & its stack, call fn MOVQ g_m(R14), BX // BX=newg.m MOVQ m_g0(BX), SI // SI = g.m.g0 CMPQ SI, R14 // if g == m->g0 call badmcall JNE goodm JMP runtime·badmcall(SB) goodm: MOVQ R14, AX // AX (and arg 0) = g MOVQ SI, R14 // g = g.m.g0 get_tls(CX) // Set G in TLS MOVQ R14, g(CX) MOVQ (g_sched+gobuf_sp)(R14), SP // sp = g0.sched.sp PUSHQ AX // open up space for fn's arg spill slot MOVQ 0(DX), R12 CALL R12 // fn(g) POPQ AX JMP runtime·badmcall2(SB) RET #else 复制代码
通过MOVQ (g_sched+gobuf_sp)(R14), SP
就能看出,sp切换到g0.sched.sp,所以每次栈每次都会切换到同样的位置
proc.go:3633
// goexit continuation on g0. func goexit0(gp *g) { _g_ := getg() casgstatus(gp, _Grunning, _Gdead) if isSystemGoroutine(gp, false) { atomic.Xadd(&sched.ngsys, -1) } gp.m = nil locked := gp.lockedm != 0 gp.lockedm = 0 _g_.m.lockedg = 0 gp.preemptStop = false gp.paniconfault = false gp._defer = nil // should be true already but just in case. gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data. gp.writebuf = nil gp.waitreason = 0 gp.param = nil gp.labels = nil gp.timer = nil if gcBlackenEnabled != 0 && gp.gcAssistBytes > 0 { // Flush assist credit to the global pool. This gives // better information to pacing if the application is // rapidly creating an exiting goroutines. assistWorkPerByte := float64frombits(atomic.Load64(&gcController.assistWorkPerByte)) scanCredit := int64(assistWorkPerByte * float64(gp.gcAssistBytes)) atomic.Xaddint64(&gcController.bgScanCredit, scanCredit) gp.gcAssistBytes = 0 } dropg() if GOARCH == "wasm" { // no threads yet on wasm gfput(_g_.m.p.ptr(), gp) schedule() // never returns } if _g_.m.lockedInt != 0 { print("invalid m->lockedInt = ", _g_.m.lockedInt, "\n") throw("internal lockOSThread error") } gfput(_g_.m.p.ptr(), gp) if locked { // The goroutine may have locked this thread because // it put it in an unusual kernel state. Kill it // rather than returning it to the thread pool. // Return to mstart, which will release the P and exit // the thread. if GOOS != "plan9" { // See golang.org/issue/22227. gogo(&_g_.m.g0.sched) } else { // Clear lockedExt on plan9 since we may end up re-using // this thread. _g_.m.lockedExt = 0 } } schedule() } 复制代码
清理g的信息
将g放入gfree
继续调用schedule
总结下流程:
作者:zxx
链接:https://juejin.cn/post/7016529318654246943