开篇
以下的内容刨除了包括 GC
、mallco
、netpoll
、timers
等等其他部分的相关逻辑,很多细节略过,并且只关乎调度。
进程启动
go
进程启动,执行的是 main
包中的 main
方法。runtime
包会在 main
方法执行之前做一些初始化的动作,并且会和 GMP
调度联动。
更具体一点来说,go
进程启动刚开始调用的是汇编函数,在汇编函数中会新建 m0
和 g0
,并且互相绑定。
- 其中
m0
作为初始化的第一个M
,会和p
绑定之后执行main
方法,这个逻辑下面会介绍。 -
g0
作为m
执行调度切换时的特殊栈,m
执行调度切换或者相关函数之前,需要先切换到g0
之后会做一些系统参数相关的初始化,然后初始化 p
数组, p
的个数和系统的 CPU
核数一致。初始化 p
数组主要会做两件事:
- 把数组中第一个
p
和m0
绑定 - 把所有的
p
放到allp
数组中,把空闲的p
放到全局调度器的空闲p
链表中
m
和 p
都有了,接下来就需要 g
来供 m
和 p
调度执行了。同样是汇编代码中的逻辑,会把 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
则抢占p
的m
的当前正在执行的g
上面的抢占逻辑适用正常执行的 g
。
如果是陷入系统调用的g
,判断抢占的逻辑和上面几乎一样,只是在最后一步并不是抢占正在执行的g
,而是把p
交给其他m
。
信号
找到了执行时间过长的 groutine
之后,会发送一个抢占信号给对应的 m
,m
收到信号之后会执行相应的信号处理函数,进行抢占处理。
其中信号处理函数是 m0
在启动调度的时候注册的,信号处理函数进程中的所有线程共用。
针对抢占信号的处理逻辑如下:
- 把
g
改成待执行 - 切断
g
和m
的关系 - 把
g
放到全局待执行队列中 - 重新调度
并行
上面的调度中,只有一个 p
被 m0
绑定,开始了调度。其他的 p
都放在全局调度器的空闲 p
链表中,并没有被绑定和执行。
其他 p
的绑定和执行在生成 groutine
的方法 runtime.newproc
中。当生成一个 groutine
并且把 groutine
放到当前 p
上之后,
会判断全局调度器的空闲 p
链表中是否有空闲的 p
,假如存在就唤醒或者新建一个 m
绑定 p
,并且执行调度方法
文章来源于互联网:GO:从进程启动到调度