golang底层 调度循环

启动m,进入调度循环

gosave初始化g0.sched执行现场,acquirep绑定p,schedule调度

mstart -> mstart1 -> save

          -> acquirep

          -> schedule

    -> mexit

 

休眠m

gc时,或找不到g的m,或者因执行时间过长、系统调用阻塞等原因被剥夺p的m,会进入休眠状态

mput将m放到sched.midle空闲m队列里,notesleep让m休眠,acquirep绑定p

stopm -> mput

    -> notesleep

    -> noteclear

    -> acquirep

 

唤醒m,进入调度循环

startm里如果没有传入p,则尝试pidleget获取空闲p,没有则return

mget从sched.midle获取到休眠m,设置m的nextp,notewakeup唤醒休眠m

mget没有获取到休眠m,则newm创建m,设置m的nextp,newosproc创建系统线程

mcommoninit会创建g0,g0栈8kb就是系统栈,systemstack作用是切换到g0栈上执行

wakep -> startm -> pidleget

        -> mget

        -> newm

        -> notewakeup

 

创建m,

newm -> allocm -> mcommoninit

    -> newosproc -> clone(linux系统调用)

 

m由自旋转到非自旋:resetspinning -> wakep

 

m/g 解绑 dropg

 

m退出

mstart -> mstart1 -> save

        -> schedule

    -> mexit

mstart/mstart1 是运行在 g0 上的,因此 save 将保存 mstart 的运行现场保存到 g0.sched 中。 当调度循环执行到 goexit0 时,会检查 m 与 g 之间是否被锁住。如果 g 锁在当前 m 上,则调用 gogo 恢复到 g0.sched 的执行现场,从而恢复到 mexit 调用。这是唯一跳出schedule循环执行mexit的方式

 

save 的作用是将调用方的 pc 和 sp 写入 getg().sched 中

 

schedule 调度循环

如果当前m锁定了g,则stoplockedm将当前m和p解绑,并parkm,直到获取了锁定的g,

runqget从p.runnext和p.runq获取g,globrunqget从全局转移一批g到本地

findrunnable从其他地方获取g,包括runqsteal从其他p里偷取g。实在获取不到,会stopm将m休眠

excute(g,...)执行g

gogo从g0栈切换到用户g栈,jmp指令跳到g的任务函数,gogo和goexit都是汇编

goexit1切换到g0栈执行goexit0,dropg解除m和g的关联

gfput将执行结束的g放到本地p.gFree里,本地gFree过多,则移动一批到全局sched.gFree里

schedule -> ..../ globrunqget / runqget / findrunnable

schedule -> excute -> gogo -> goexit -> goexit1 -> mcall(goexit0) -> dropg / gfput / schedule

 

findrunnable

  • 首先检查是是否正在进行 GC,如果是则暂止当前的 m 并阻塞休眠;
  • 尝试从本地队列中取 g,如果取到,则直接返回,否则继续从全局队列中找 g,如果找到则直接返回;
  • 检查是否存在 poll 网络的 g,如果有,则直接返回;
  • 如果此时仍然无法找到 g,则从其他 P 的本地队列中偷取;
  • 从其他 P 本地队列偷取的工作会执行四轮,在前两轮中只会查找 runnable 队列,后两轮则会优先查找 ready 队列,如果找到,则直接返回;
  • 所有的可能性都尝试过了,在准备暂止 m 之前,还要进行额外的检查;
  • 首先检查此时是否是 GC mark 阶段,如果是,则直接返回 mark 阶段的 g;
  • 如果仍然没有,则对当前的 p 进行快照,准备对调度器进行加锁;
  • 当调度器被锁住后,我们仍然还需再次检查这段时间里是否有进入 GC,如果已经进入了 GC,则回到第一步,阻塞 m 并休眠;
  • 当调度器被锁住后,如果我们又在全局队列中发现了 g,则直接返回;
  • 当调度器被锁住后,我们彻底找不到任务了,则归还释放当前的 P,将其放入 idle 链表中,并解锁调度器;
  • M/P 已经解绑后,我们需要将 m 的状态切换出自旋状态,并减少 nmspinning;
  • 此时我们仍然需要重新检查所有的队列;
  • 如果此时我们发现有一个 P 队列不空,则立刻尝试获取一个 P,如果获取到,则回到第一步,重新执行偷取工作,如果取不到,则说明系统已经满载,无需继续进行调度;
  • 同样,我们还需要再检查是否有 GC mark 的 g 出现,如果有,获取 P 并回到第一步,重新执行偷取工作;
  • 同样,我们还需要再检查是否存在 poll 网络的 g,如果有,则直接返回;
  • 终于,我们什么也没找到,暂止当前的 m 并阻塞休眠。

 

触发调度

  1. 线程启动 mstart
  2. g执行完,goexit0
  3. 主动挂起 gopark
  4. 退出系统调用 exitsyscall
  5. 主动让权 Gosched
  6. 系统监控 sysmon->retake
  7. preemptPark?semrelease?

 

gopark

gopark 将g设为 _Gwaiting状态,调用dropg解除和m的关联,然后就可以调用schedule调度其他g了。gopark不会将 G 放到待运⾏队列,需要用 goready 来恢复执⾏,G 被放回优先级最⾼的 P.runnext。

gopark -> mcall(park_m) -> dropg

            -> schedule

goready -> ready -> runqput

 

posted @ 2020-05-27 22:17  是的哟  阅读(633)  评论(0编辑  收藏  举报