您的位置 首页 golang

Golang源码学习:调度逻辑(三)工作线程的执行流程与调度循环

本文内容主要分为三部分:

  1. main goroutine 的调度运行
  2. 非 main goroutine 的退出流程
  3. 工作线程的执行流程与调度循环。

main goroutine 的调度运行

runtime·rt0_go中在调用完runtime.newproc创建main goroutine后,就调用了runtime.mstart。让我们来分析一下这个函数。

mstart

mstart没什么太多工作,然后就调用了mstart1。

func mstart() {_g_ := getg()        // 在启动阶段,_g_.stack早就完成了初始化,所以osStack是false,下面被省略的也不会执行。osStack := _g_.stack.lo == 0 ......_g_.stackguard0 = _g_.stack.lo + _StackGuard_g_.stackguard1 = _g_.stackguard0mstart1()        ......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。绑定一个pacquirep(_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获取一个可执行的g

func schedule() {_g_ := getg()// g0        ......var gp *g// 初始化时,经过下面一系列查找,会找到main goroutine,因为目前为止整个运行时只有这一个g(除了g0)。var inheritTime bool        ......if gp == nil {                // 该p上每进行61次就从全局队列中获取一个gif _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中获取一个ggp, 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 goroutinegp.m = _g_.mcasgstatus(gp, _Grunnable, _Grunning)// 更改状态gp.waitsince = 0gp.preempt = falsegp.stackguard0 = gp.stack.lo + _StackGuardif !inheritTime {_g_.m.p.ptr().schedtick++}......gogo(&gp.sched)}

gogo(buf *gobuf)

在本方法下面的讲解中将使用newg代指被调度的g。

gogo函数是用汇编实现的。其作用是:加载newg的上下文,跳转到gobuf.pc指向的函数。

// go/src/runtime/asm_amd64.sTEXT runtime·gogo(SB), NOSPLIT, $16-8MOVQbuf+0(FP), BX// bx = &gp.schedMOVQgobuf_g(BX), DX// dx = gp.sched.g ,也就是存储的 newg 指针MOVQ0(DX), CX// make sure g != nilget_tls(CX)MOVQDX, g(CX)// newg指针设置到tlsMOVQgobuf_sp(BX), SP// 下面四条是加载上下文到cpu寄存器。MOVQgobuf_ret(BX), AXMOVQgobuf_ctxt(BX), DXMOVQgobuf_bp(BX), BPMOVQ$0, gobuf_sp(BX)// 下面四条是清零,减少gc的工作量。MOVQ$0, gobuf_ret(BX)MOVQ$0, gobuf_ctxt(BX)MOVQ$0, gobuf_bp(BX)MOVQgobuf_pc(BX), BX// gobuf.pc 存储的是要执行的函数指针,初始化时此函数为runtime.mainJMPBX// 跳转到要执行的函数

runtime.main:main函数的执行

在上面gogo执行最后的JMP指令,其实就是跳转到了runtime.main。

func main() {g := getg()// 获取当前g,已经不是g0了,我们暂且称为maing        if sys.PtrSize == 8 {// 64位系统,栈最大为1GBmaxstacksize = 1000000000} else {maxstacksize = 250000000}mainStarted = true        // 启动监控进程,抢占调度就是在这里实现的if GOARCH != "wasm" { // no threads on wasm yet, so no sysmonsystemstack(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主要做了以下的工作:

  • 启动监控进程。
  • 调用runtime的初始化函数。
  • 开启gc。
  • 调用main的初始化函数。
  • 调用main.main,执行完后退出。

非 main goroutine 的退出流程

首先明确一点,无论是main goroutine还是非main goroutine的都是调用newproc创建的,所以在调度上基本是一致的。

之前的文章中说过,在gostartcall函数中,会将goroutine要执行的函数fn伪造成是被goexit调用的。但是,当fn是runtime.main的时候是没有用的,因为在runtime.main末尾会调用exit(0)退出程序。所以,这只对非main goroutine起作用。让我们简单验证一下。

先给出一个简单的例子:

package mainimport "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 // 可以看到调用栈中确实存在goexit0  0x00000000004ad86f in main.foo   at ./foo.go:111  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:CALLruntime·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-0BYTE$0x90// NOPCALLruntime·goexit1(SB)// does not return// traceback from goexit1 must hit code range of goexitBYTE$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-8MOVQfn+0(FP), DI// 此处 di 存储的是 funcval 结构体指针,funcval.fn 指向的是 goexit0。get_tls(CX)MOVQg(CX), AX// 此处 ax 中存储的是foog        // 保存foog的上下文MOVQ0(SP), BX// caller's PC。mcall的返回地址,此处就是 goexit1 调用 mcall 时的pcMOVQBX, (g_sched+gobuf_pc)(AX)// foog.sched.pc = caller's PCLEAQfn+0(FP), BX// caller's SP。MOVQBX, (g_sched+gobuf_sp)(AX)// foog.sched.sp = caller's SPMOVQAX, (g_sched+gobuf_g)(AX)// foog.sched.g = foogMOVQBP, (g_sched+gobuf_bp)(AX)// foog.sched.bp = bp        // 切换到m.g0和它的栈,调用fn。MOVQg(CX), BX// 此处 bx 中存储的是foogMOVQg_m(BX), BX// bx = foog.mMOVQm_g0(BX), SI// si = m.g0CMPQSI, AX// if g == m->g0 call badmcallJNE3(PC)// 上面的结果不相等就跳转到下面第三行。MOVQ$runtime·badmcall(SB), AXJMPAXMOVQSI, g(CX)// g = m->g0。m.g0设置到tlsMOVQ(g_sched+gobuf_sp)(SI), SP// sp = m->g0->sched.sp。设置g0栈.PUSHQAX// fn的参数压栈,ax = foogMOVQDI, DXMOVQ0(DI), DI// 读取 funcval 结构的第一个成员,也就是 funcval.fn,此处是goexit0。CALLDI// 调用 goexit0(foog)。POPQAXMOVQ$runtime·badmcall2(SB), AXJMPAXRET

在此场景下,mcall做了以下工作:保存foog的上下文。切换到g0及其栈,调用传入的方法,并将foog作为参数。

可以看到mcall与gogo的作用正好相反:

  • gogo实现了从g0切换到某个goroutine,执行关联函数。
  • mcall实现了保存某个goroutine,切换到g0及其栈,并调用fn函数,其参数就是被保存的goroutine指针。

goexit0

func goexit0(gp *g) {_g_ := getg()// g0casgstatus(gp, _Grunning, _Gdead)// 更改gp状态为_Gdeadif isSystemGoroutine(gp, false) {atomic.Xadd(&sched.ngsys, -1)}        // 下面的一段就是清零gp的属性gp.m = nillocked := gp.lockedm != 0gp.lockedm = 0_g_.m.lockedg = 0gp.preemptStop = falsegp.paniconfault = falsegp._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 = nilgp.waitreason = 0gp.param = nilgp.labels = nilgp.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重新调度。

文章来源:智云一二三科技

文章标题:Golang源码学习:调度逻辑(三)工作线程的执行流程与调度循环

文章地址:https://www.zhihuclub.com/1040.shtml

关于作者: 智云科技

热门文章

网站地图