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 +----------+
| |
而gogo
以JMP
调用G.fn
,就是说不会将gogo
的IP/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()
}