Go1.18 调度器执行源码剖析

发表于 · 归类于 代码 · 阅读完需 9 分钟 · 报告错误 · 阅读:

执行

execute 切换到G.stack,执行并发函数。

// proc.go

// Schedules gp to run on the current M.
// If inheritTime is true, gp inherits the remaining time in the
// current time slice. Otherwise, it starts a new time slice.
// Never returns.

func execute(gp *g, inheritTime bool) {
    _g_ := getg()

    // 设置任务状态。
    _g_.m.curg = gp
    gp.m = _g_.m
    casgstatus(gp, _Grunnable, _Grunning)
    gp.waitsince = 0
    gp.preempt = false

    /*
        lo           stackguard0                                  hi
        +------------+---------------------------------------------+
        | StackGuard |               stack frames                  |
        +------------+---------------------------------------------+
                                                        <--- SP ----

        StackGuard: 溢出保护区。确保某些操作可以安全分配。
        stckguard0, stackguard1:与SP比较,判断是否溢出,是否需要扩容。
    */
    gp.stackguard0 = gp.stack.lo + _StackGuard
    
    // 累加执行计数器。
    if !inheritTime {
        _g_.m.p.ptr().schedtick++
    }

    // 执行。
    gogo(&gp.sched)
}

函数 gogo 使用汇编实现的执行函数,它会切换到G.stack,并跳转到用户函数。

 

结束

无论是execute,还是gogo都没有直接回到schedule的意思。那么,如何清理执行现场?比如将dead G放回复用链表。如何继续循环调度呐?

newproc1设置G.sched的时候,实际保存在pc的是goexit

// proc.go

func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
    
    newg.sched.sp = sp
    newg.stktopsp = sp

    newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
    newg.sched.g = guintptr(unsafe.Pointer(newg))
    gostartcallfn(&newg.sched, fn)
    
    newg.gopc = callerpc
    newg.startpc = fn.fn

    return newg
}

关键就在于gostartcallfn做了什么。

// stack.go

// adjust Gobuf as if it executed a call to fn
// and then stopped before the first instruction in fn.

func gostartcallfn(gobuf *gobuf, fv *funcval) {
    var fn unsafe.Pointer
    if fv != nil {
        fn = unsafe.Pointer(fv.fn)
    } else {
        fn = unsafe.Pointer(abi.FuncPCABIInternal(nilfunc))
    }
    gostartcall(gobuf, fn, unsafe.Pointer(fv))
}
// sys_x86.go

// adjust Gobuf as if it executed a call to fn with context ctxt
// and then stopped before the first instruction in fn.

func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) {
    sp := buf.sp
    
       // 将 G.sched.sp 上移一个指针空间。
    sp -= goarch.PtrSize
    
       // 存储 G.sched.pc,也就是预设的 goexit。
    *(*uintptr)(unsafe.Pointer(sp)) = buf.pc
    
       // 调整 G.sched 设置,此时 pc 指向用户函数。
    buf.sp = sp
    buf.pc = uintptr(fn)
    buf.ctxt = ctxt
}

execute通过gogo切换到G.stack,执行用户函数时,其堆栈如下:

lo  |          |
    | G.fn     |
    +----------+  G.sched.sp
    | goexit   |
hi  +----------+
    |          |

gogoJMP调用G.fn,就是说不会将gogoIP/PC入栈。等G.fn RET执行时,POP PC的结果自然是goexit。该函数以mcall切换回g0栈,完成清理操作,重新回到调度循环。

// asm_amd64.s

// The top-most function running on a goroutine
// returns to goexit+PCQuantum.

TEXT runtime·goexit(SB),NOSPLIT|TOPFRAME,$0-0
    CALL    runtime·goexit1(SB)	// does not return
// proc.go

// Finishes execution of the current goroutine.
func goexit1() {
    mcall(goexit0)
}

// goexit continuation on g0.
func goexit0(gp *g) {
    _g_ := getg()
    _p_ := _g_.m.p.ptr()
    
    // 清理已结束任务状态,并记入 GC。
    casgstatus(gp, _Grunning, _Gdead)
    gcController.addScannableStack(_p_, -int64(gp.stack.hi-gp.stack.lo))
    
    gp.m = nil
    gp.lockedm = 0
    ...
    gp.timer = nil

    // 解除与当前 M 的关联。
    dropg()
    
    // 放回闲置链表(P.gFree),等待复用。
    gfput(_p_, gp)

    // 重回调度函数。
    schedule()
}