本文内容主要分为三部分:函数
- main goroutine 的调度运行
- 非 main goroutine 的退出流程
- 工做线程的执行流程与调度循环。
main goroutine 的调度运行
runtime·rt0_go中在调用完runtime.newproc建立main goroutine后,就调用了runtime.mstart。让咱们来分析一下这个函数。ui
mstart
mstart没什么太多工做,而后就调用了mstart1。atom
func mstart() { _g_ := getg() // 在启动阶段,_g_.stack早就完成了初始化,因此osStack是false,下面被省略的也不会执行。 osStack := _g_.stack.lo == 0 ...... _g_.stackguard0 = _g_.stack.lo + _StackGuard _g_.stackguard1 = _g_.stackguard0 mstart1() ...... mexit(osStack) }
mstart1
- 调用save保存g0的状态
- 处理信号相关
- 调用 schedule 开始调度
func mstart1() { _g_ := getg() if _g_ != _g_.m.g0 { throw("bad runtime·mstart") } save(getcallerpc(), getcallersp()) // 保存调用mstart1的函数(mstart)的 pc 和 sp。 asminit() // 空函数 minit() // 信号相关 if _g_.m == &m0 { // 初始化时会执行这里,也是信号相关 mstartm0() } if fn := _g_.m.mstartfn; fn != nil { // 初始化时 fn = nil,不会执行这里 fn() } if _g_.m != &m0 { // 不是m0的话,没有p。绑定一个p acquirep(_g_.m.nextp.ptr()) _g_.m.nextp = 0 } schedule() }
save(pc, sp uintptr) 保存调度信息
保存当前g(初始化时为g0)的状态到sched字段中。线程
func save(pc, sp uintptr) { _g_ := getg() _g_.sched.pc = pc _g_.sched.sp = sp _g_.sched.lr = 0 _g_.sched.ret = 0 _g_.sched.g = guintptr(unsafe.Pointer(_g_)) if _g_.sched.ctxt != nil { badctxt() } }
schedule 开始调度
调用globrunqget、runqget、findrunnable获取一个可执行的gdebug
func schedule() { _g_ := getg() // g0 ...... var gp *g // 初始化时,通过下面一系列查找,会找到main goroutine,由于目前为止整个运行时只有这一个g(除了g0)。 var inheritTime bool ...... if gp == nil { // 该p上每进行61次就从全局队列中获取一个g if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 { lock(&sched.lock) gp = globrunqget(_g_.m.p.ptr(), 1) unlock(&sched.lock) } } if gp == nil { // 从p的runq中获取一个g gp, inheritTime = runqget(_g_.m.p.ptr()) // We can see gp != nil here even if the M is spinning, // if checkTimers added a local goroutine via goready. } if gp == nil { // 寻找可执行的g,会尝试从本地,全局运行对列获取,若是没有,从其余p那里偷取。 gp, inheritTime = findrunnable() // blocks until work is available } ...... execute(gp, inheritTime) }
execute:安排g在当前m上运行
- 被调度的 g 与 m 相互绑定
- 更改g的状态为 _Grunning
- 调用 gogo 切换到被调度的g上
func execute(gp *g, inheritTime bool) { _g_ := getg() // g0 _g_.m.curg = gp // 与下面一行是 gp 和 m 相互绑定。gp 其实就是 main goroutine gp.m = _g_.m casgstatus(gp, _Grunnable, _Grunning) // 更改状态 gp.waitsince = 0 gp.preempt = false gp.stackguard0 = gp.stack.lo + _StackGuard if !inheritTime { _g_.m.p.ptr().schedtick++ } ...... gogo(&gp.sched) }
gogo(buf *gobuf)
在本方法下面的讲解中将使用newg代指被调度的g。3d
gogo函数是用汇编实现的。其做用是:加载newg的上下文,跳转到gobuf.pc指向的函数。指针
// go/src/runtime/asm_amd64.s TEXT runtime·gogo(SB), NOSPLIT, $16-8 MOVQ buf+0(FP), BX // bx = &gp.sched MOVQ gobuf_g(BX), DX // dx = gp.sched.g ,也就是存储的 newg 指针 MOVQ 0(DX), CX // make sure g != nil get_tls(CX) MOVQ DX, g(CX) // newg指针设置到tls MOVQ gobuf_sp(BX), SP // 下面四条是加载上下文到cpu寄存器。 MOVQ gobuf_ret(BX), AX MOVQ gobuf_ctxt(BX), DX MOVQ gobuf_bp(BX), BP MOVQ $0, gobuf_sp(BX) // 下面四条是清零,减小gc的工做量。 MOVQ $0, gobuf_ret(BX) MOVQ $0, gobuf_ctxt(BX) MOVQ $0, gobuf_bp(BX) MOVQ gobuf_pc(BX), BX // gobuf.pc 存储的是要执行的函数指针,初始化时此函数为runtime.main JMP BX // 跳转到要执行的函数
runtime.main:main函数的执行
在上面gogo执行最后的JMP指令,其实就是跳转到了runtime.main。调试
func main() { g := getg() // 获取当前g,已经不是g0了,咱们暂且称为maing if sys.PtrSize == 8 { // 64位系统,栈最大为1GB maxstacksize = 1000000000 } else { maxstacksize = 250000000 } mainStarted = true // 启动监控进程,抢占调度就是在这里实现的 if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon systemstack(func() { newm(sysmon, nil) }) } ...... doInit(&runtime_inittask) // 调用runtime的初始化函数 ...... runtimeInitTime = nanotime() // 记录世界开始时间 gcenable() // 开启gc ...... doInit(&main_inittask) // 调用main的初始化函数 ...... fn := main_main // 调用main.main,也就是咱们常常写hello world的main。 fn() ...... exit(0) // 退出 }
runtime.main主要作了如下的工做:code
- 启动监控进程。
- 调用runtime的初始化函数。
- 开启gc。
- 调用main的初始化函数。
- 调用main.main,执行完后退出。
非 main goroutine 的退出流程
首先明确一点,不管是main goroutine仍是非main goroutine的都是调用newproc建立的,因此在调度上基本是一致的。blog
以前的文章中说过,在gostartcall函数中,会将goroutine要执行的函数fn伪形成是被goexit调用的。可是,当fn是runtime.main的时候是没有用的,由于在runtime.main末尾会调用exit(0)退出程序。因此,这只对非main goroutine起做用。让咱们简单验证一下。
先给出一个简单的例子:
package main import "fmt" func main() { ch := make(chan int) go foo(ch) fmt.Println(<-ch) } func foo(ch chan int) { ch <- 1 }
dlv调试一波:
root@xiamin:~/study# dlv debug foo.go (dlv) b main.foo // 打个断点 Breakpoint 1 set at 0x4ad86f for main.foo() ./foo.go:11 (dlv) c > main.foo() ./foo.go:11 (hits goroutine(6):1 total:1) (PC: 0x4ad86f) 6: ch := make(chan int) 7: go foo(ch) 8: fmt.Println(<-ch) 9: } 10: => 11: func foo(ch chan int) { 12: ch <- 1 13: } (dlv) bt // 能够看到调用栈中确实存在goexit 0 0x00000000004ad86f in main.foo at ./foo.go:11 1 0x0000000000463df1 in runtime.goexit at /root/go/src/runtime/asm_amd64.s:1373 // 此处执行三次 s,获得如下结果,确实是回到了goexit。 > runtime.goexit() /root/go/src/runtime/asm_amd64.s:1374 (PC: 0x463df1) 1370: // The top-most function running on a goroutine 1371: // returns to goexit+PCQuantum. 1372: TEXT runtime·goexit(SB),NOSPLIT,$0-0 1373: BYTE $0x90 // NOP =>1374: CALL runtime·goexit1(SB) // does not return 1375: // traceback from goexit1 must hit code range of goexit 1376: BYTE $0x90 // NOP
咱们暂且将关联foo的g称之为foog,接下来咱们看一下它的退出流程。
goexit
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
goexit1
func goexit1() { if raceenabled { racegoend() } if trace.enabled { traceGoEnd() } mcall(goexit0) }
goexit和goexit1没什么可说的,看一下mcall
mcall(fn func(*g))
mcall的参数是个函数fn,而fn有个参数是*g,此处fn是goexit0。
mcall是由汇编编写的:
TEXT runtime·mcall(SB), NOSPLIT, $0-8 MOVQ fn+0(FP), DI // 此处 di 存储的是 funcval 结构体指针,funcval.fn 指向的是 goexit0。 get_tls(CX) MOVQ g(CX), AX // 此处 ax 中存储的是foog // 保存foog的上下文 MOVQ 0(SP), BX // caller's PC。mcall的返回地址,此处就是 goexit1 调用 mcall 时的pc MOVQ BX, (g_sched+gobuf_pc)(AX) // foog.sched.pc = caller's PC LEAQ fn+0(FP), BX // caller's SP。 MOVQ BX, (g_sched+gobuf_sp)(AX) // foog.sched.sp = caller's SP MOVQ AX, (g_sched+gobuf_g)(AX) // foog.sched.g = foog MOVQ BP, (g_sched+gobuf_bp)(AX) // foog.sched.bp = bp // 切换到m.g0和它的栈,调用fn。 MOVQ g(CX), BX // 此处 bx 中存储的是foog MOVQ g_m(BX), BX // bx = foog.m MOVQ m_g0(BX), SI // si = m.g0 CMPQ SI, AX // if g == m->g0 call badmcall JNE 3(PC) // 上面的结果不相等就跳转到下面第三行。 MOVQ $runtime·badmcall(SB), AX JMP AX MOVQ SI, g(CX) // g = m->g0。m.g0设置到tls MOVQ (g_sched+gobuf_sp)(SI), SP // sp = m->g0->sched.sp。设置g0栈. PUSHQ AX // fn的参数压栈,ax = foog MOVQ DI, DX MOVQ 0(DI), DI // 读取 funcval 结构的第一个成员,也就是 funcval.fn,此处是goexit0。 CALL DI // 调用 goexit0(foog)。 POPQ AX MOVQ $runtime·badmcall2(SB), AX JMP AX RET
在此场景下,mcall作了如下工做:保存foog的上下文。切换到g0及其栈,调用传入的方法,并将foog做为参数。
能够看到mcall与gogo的做用正好相反:
- gogo实现了从g0切换到某个goroutine,执行关联函数。
- mcall实现了保存某个goroutine,切换到g0及其栈,并调用fn函数,其参数就是被保存的goroutine指针。
goexit0
func goexit0(gp *g) { _g_ := getg() // g0 casgstatus(gp, _Grunning, _Gdead) // 更改gp状态为_Gdead if isSystemGoroutine(gp, false) { atomic.Xadd(&sched.ngsys, -1) } // 下面的一段就是清零gp的属性 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 ...... dropg() // 解绑gp与当前m。_g_.m.curg.m = nil ; _g_.m.curg = nil 。 ...... gfput(_g_.m.p.ptr(), gp) // 放入空闲列表。若是本地队列太多,会转移一部分到全局队列。 ...... schedule() // 从新调度 }
goexit0作了如下工做:
- 将gp属性清零与m解绑
- gfput 放入空闲列表
- schedule 从新调度
工做线程的执行流程与调度循环
如下给出一个工做线程的执行流程简图:
能够看到工做线程的执行是从mstart开始的。schedule->......->goexit0->schedule造成了一个调度循环。
高度归纳一下执行流程与调度循环:
- mstart:主要是设置g0.stackguard0,g0.stackguard1。
- mstart1:调用save保存callerpc和callerpc到g0.sched。而后调用schedule开始调度循环。
- schedule:得到一个可执行的g。下面用gp代指。
- execute(gp *g, inheritTime bool):绑定gp与当前m,状态改成_Grunning。
- gogo(buf *gobuf):加载gp的上下文,跳转到buf.pc指向的函数。
- 执行buf.pc指向函数。
- goexit->goexit1:调用mcall(goexit0)。
- mcall(fn func(*g)):保存当前g(也就是gp)的上下文;切换到g0及其栈,调用fn,参数为gp。
- goexit0(gp *g):清零gp的属性,状态_Grunning改成_Gdead;dropg解绑m和gp;gfput放入队列;schedule从新调度。