go语言调度gmp原理(3)
调度循环
调度器启动之后,go语言运行时会调用runtime.mstart和runtime.mstart1,前者会初始化g0的stackguard0和stackguard1字段,后者会初始化线程并调用runtime.schedule进入调度循环
func schedule() {
mp := getg().m
if mp.locks != 0 {
throw("schedule: holding locks")
}
if mp.lockedg != 0 {
stoplockedm()
execute(mp.lockedg.ptr(), false) // Never returns.
}
// We should not schedule away from a g that is executing a cgo call,
// since the cgo call is using the m's g0 stack.
if mp.incgo {
throw("schedule: in cgo")
}
top:
pp := mp.p.ptr()
pp.preempt = false
// Safety check: if we are spinning, the run queue should be empty.
// Check this before calling checkTimers, as that might call
// goready to put a ready goroutine on the local run queue.
if mp.spinning && (pp.runnext != 0 || pp.runqhead != pp.runqtail) {
throw("schedule: spinning with local work")
}
gp, inheritTime, tryWakeP := findRunnable() // blocks until work is available
// This thread is going to run a goroutine and is not spinning anymore,
// so if it was marked as spinning we need to reset it now and potentially
// start a new spinning M.
if mp.spinning {
resetspinning()
}
if sched.disable.user && !schedEnabled(gp) {
// Scheduling of this goroutine is disabled. Put it on
// the list of pending runnable goroutines for when we
// re-enable user scheduling and look again.
lock(&sched.lock)
if schedEnabled(gp) {
// Something re-enabled scheduling while we
// were acquiring the lock.
unlock(&sched.lock)
} else {
sched.disable.runnable.pushBack(gp)
sched.disable.n++
unlock(&sched.lock)
goto top
}
}
// If about to schedule a not-normal goroutine (a GCworker or tracereader),
// wake a P if there is one.
if tryWakeP {
wakep()
}
if gp.lockedm != 0 {
// Hands off own p to the locked m,
// then blocks waiting for a new p.
startlockedm(gp)
goto top
}
execute(gp, inheritTime)
}
runtime.schedule函数会从下面几处查找待执行的goroutine
- 为了保证公平,当全局运行队列中有待执行的goroutine时,通过schedtick保证有一定概率会从全局的运行队列中查找对应的goroutine
- 从处理器本地的运行队列中查找待执行的goroutine
- 如果前两种方法都没有找到goroutine,会通过runtime.findrunnable阻塞地查找goroutine
runtime.findrunnable获取goroutine过程
- 从本地运行队列、全局运行队列中查找
- 从网络轮询器中查找是否有goroutine等待运行
- 通过runtime.runqsteal尝试从其他随机的处理器中窃取待运行的goroutine,该函数还可能窃取处理器的计时器
当前函数一定会返回一个可执行的goroutine,如果当前不存在就会阻塞地等待
接下来由runtime.execute执行获取的goroutine,做好准备后,它会通过runtime.gogo将goroutine调度到当前线程上
func execute(gp *g, inheritTime bool) {
mp := getg().m
if goroutineProfile.active {
// Make sure that gp has had its stack written out to the goroutine
// profile, exactly as it was when the goroutine profiler first stopped
// the world.
tryRecordGoroutineProfile(gp, osyield)
}
// Assign gp.m before entering _Grunning so running Gs have an
// M.
mp.curg = gp
gp.m = mp
casgstatus(gp, _Grunnable, _Grunning)
gp.waitsince = 0
gp.preempt = false
gp.stackguard0 = gp.stack.lo + _StackGuard
if !inheritTime {
mp.p.ptr().schedtick++
}
// Check whether the profiler needs to be turned on or off.
hz := sched.profilehz
if mp.profilehz != hz {
setThreadCPUProfiler(hz)
}
if trace.enabled {
// GoSysExit has to happen when we have a P, but before GoStart.
// So we emit it here.
if gp.syscallsp != 0 && gp.sysblocktraced {
traceGoSysExit(gp.sysexitticks)
}
traceGoStart()
}
gogo(&gp.sched)
}
runtime.gogo从runtime.gobuf中取出了runtime.goexit的程序计数器和待执行函数的程序计数器,其中:
- runtime.goexit的程序计数器被放到栈的SP上
- 待执行函数的程序计数器被放到了寄存器BX上
正常的函数调用都会使用CALL指令,该指令会将调用方的返回地址加入栈寄存器SP中,然后跳转到目标函数;当目标函数返回后,会从堆中查找调用的地址并跳转回调用方继续执行剩余代码
当goroutine中运行的函数返回时,就会跳转到runtime.goexit所在位置执行该函数
经过一系列复杂的函数调用,我们最终在当前线程的g0的栈上调用runtime.goexit0函数,该函数会将goroutine转换为_Gdead状态、清除其中的字段、移除goroutine和线程的关联,并调用runtime.gfput重新加入处理器的goroutine空闲列表gFree
最后runtime.goexit0会重新调用runtime.schedule触发新一轮的goroutine调度,go语言中的运行时,调度循环会从runtime.schedule开始,最终又回到runtime.schedule
这里介绍的是goroutine正常执行并退出的逻辑,实际情况会复杂得多,多数情况下goroutine在执行过程中会经历写作时调度或者抢占式调度
标签:runtime,sched,gp,goroutine,调度,schedule,mp,go,gmp From: https://www.cnblogs.com/zpf253/p/17406849.html