Go GMP调度 · 2023年5月8日 0

GO:从进程启动到调度【】

开篇

以下的内容刨除了包括 GCmallconetpolltimers 等等其他部分的相关逻辑,很多细节略过,并且只关乎调度。

进程启动

go 进程启动,执行的是 main 包中的 main 方法。runtime 包会在 main 方法执行之前做一些初始化的动作,并且会和 GMP 调度联动。

更具体一点来说,go 进程启动刚开始调用的是汇编函数,在汇编函数中会新建 m0g0,并且互相绑定。

  • 其中 m0 作为初始化的第一个 M,会和 p 绑定之后执行 main 方法,这个逻辑下面会介绍。
  • g0 作为 m 执行调度切换时的特殊栈,m 执行调度切换或者相关函数之前,需要先切换到 g0

之后会做一些系统参数相关的初始化,然后初始化 p 数组, p 的个数和系统的 CPU 核数一致。初始化 p 数组主要会做两件事:

  • 把数组中第一个 pm0 绑定
  • 把所有的 p 放到 allp 数组中,把空闲的 p 放到全局调度器的空闲 p 链表中

mp 都有了,接下来就需要 g 来供 mp 调度执行了。同样是汇编代码中的逻辑,会把 runtime.main 函数作为 groutine 的执行函数传入,然后新建一个 g

这个 g 会放到 m0 绑定的 p 的可执行队列上,就可以开始调度了。其中 rutime.main 函数中会执行 main.main 方法和各种包的 init 方法,当然还有一些其他关键作用,后面会讲到。

调度

m 开始调度, 会执行 runtime.schedule 方法。在调度中除了 GMP 之外还有全局调度器的角色,主要负责一些全局变量的存储和部分组件的缓存,包括多余的可执行 groutine

g 被生产的大致逻辑为:

  • 放到当前 p 的可执行队列中
  • 满了,则放到全局队列中

其中 g 被调度大致的逻辑为:

  • m 会先从当前 p 的可执行队列中寻找可执行的 g
  • 没有,则去全局队列中寻找
  • 没有,则去其他 p 中偷

除此之外,还有一些补充逻辑:

  • m 每隔 61 次会优先从全局调度器的可执行队列中寻找 g
  • 去其他 p 中偷的时候,最多偷四次,每次随机一个 p 开始,偷不到就会释放 p 并且阻塞 m

抢占

上面的顺序调度会存在问题,当一个 groutine 执行时间太长(逻辑太长or陷入系统调用),p 上的其他 groutine 会有饿死的风险。

为了防止这种情况,runtime.main 方法在调用 main.main 方法之前,会启动一个 m 单独执行 runtime.sysmon 方法,在 sysmon 方法中会判断当前执行的 g 是否需要抢占。

判断抢占的逻辑大致如下:

  • 循环遍历所有 p ,把当前时间和 p 的调度次数存快照
  • 比较 p 的调度次数和快照次数,不一样则表示两次循环之间,p 发生了调度,不需要抢占
  • 否则比较当前时间和快照时间,差值超过 10ms 则抢占 pm 的当前正在执行的 g

上面的抢占逻辑适用正常执行的 g

如果是陷入系统调用的g,判断抢占的逻辑和上面几乎一样,只是在最后一步并不是抢占正在执行的g,而是把p交给其他m

信号

找到了执行时间过长的 groutine之后,会发送一个抢占信号给对应的 mm 收到信号之后会执行相应的信号处理函数,进行抢占处理。

其中信号处理函数是 m0 在启动调度的时候注册的,信号处理函数进程中的所有线程共用。

针对抢占信号的处理逻辑如下:

  • g 改成待执行
  • 切断 gm 的关系
  • g 放到全局待执行队列中
  • 重新调度

并行

上面的调度中,只有一个 pm0 绑定,开始了调度。其他的 p 都放在全局调度器的空闲 p 链表中,并没有被绑定和执行。

其他 p 的绑定和执行在生成 groutine 的方法 runtime.newproc 中。当生成一个 groutine 并且把 groutine 放到当前 p 上之后,

会判断全局调度器的空闲 p 链表中是否有空闲的 p,假如存在就唤醒或者新建一个 m 绑定 p,并且执行调度方法

文章来源于互联网:GO:从进程启动到调度

打赏 赞(0) 分享'
分享到...
微信
支付宝
微信二维码图片

微信扫描二维码打赏

支付宝二维码图片

支付宝扫描二维码打赏

文章目录