go程序启动过程分析
/ / 点击os:osx 10.14.5
go version: 1.12.5 darwin/amd64
不加特说说明的文件都是在 runtime 包下
程序入口
整个程序入口是在
rt0_darwin_amd64.s的第8行。此处只有一行汇编JMP _rt0_amd64(SB),跳转到asm_amd64.s中的第14行,配置好argcargv后又跳转到rt0_go(87行)。正式开始初始化go程序的运行时环境。
1 | TEXT runtime·rt0_go(SB),NOSPLIT,$0 |
引导
从上边汇编代码可以看出,整个过程按如下顺序执行:
- 初始化系统运行时信息
- 初始化调度时全局变量 m0 g0及关联关系
- runtime.check (实现在runtime1.go)对编译器工作进行校验,确保运行时类型正确
- runtime.args (runtime1.go).处理程序参数
- runtime.osinit (不同os在不同文件,os_darwin.go).获得CPU核心数
- runtime.schedinit (proc.go). 初始化调度器
- runtime.newproc (proc.go). 根据主goroutine入口地址创建G,并放至G队列中。
- runtime.mstart (proc.go). 开始调度循环
部分过程详细
runtime.schedinit 和 runtime.newproc
1 | // runtime/proc.go |
核心组件初始化
msigsave
stackinit
goroutine栈结构
1 | type g struct { |
G的创建:(proc.go newproc())
- 首先检查go函数及其参数的合法性
- 尝试从本地P的自由G列表和调度器的自由G列表获取可用G(line3271:gfget(
_p_)),如果未获取到,则新建G(line3273:malg(_StackMin))- 初始化G,包括关联go函数及设置该G的状态和ID等
- 尝试将G放入本地P的runnext字段(line3348:runqput()):如果runnext为空,则直接放到runnext并返回;如果不为空,则替换,并将原runnext值放到本地P.runq末尾,如果满了将后一半的G移动至全局G队列
mallocinit
内存分配器的初始化除去一些例行检查外,就是对堆的初始化了
1 | func mallocinit() { |
堆的初始化:
1 | // 堆初始化 |
mcommoninit
M初始化
M 创建时机:
- 程序运行之初的 M0,无需创建已经存在的系统线程,只需对其进行初始化即可。
1.1 schedinit –> mcommoninit –> mpreinit –> msigsave –> initSigmask –> mstart- 需要时创建的 M,某些特殊情况下一定会创建一个新的 M并进行初始化,而后创建系统线程:
2.1 startm 时没有空闲 m
2.2 startTemplateThread 时
2.3 startTheWorldWithSema 时 p 如果没有 m
2.4 main 时创建系统监控
2.5 oneNewExtraM 时
2.6 初始化过程: newm –> allocm –> mcommoninit –> mpreinit –> newm1 –> newosproc –> mstart
P初始化
G初始化
创建 G 的过程也是相对比较复杂的,我们来总结一下这个过程:
首先尝试从 P 本地 gfree 链表或全局 gfree 队列获取已经执行过的、已经执行过的 g 初始化过程中程序无论是本地队列还是全局队列都不可能获取到 g,因此创建一个新的 g,并为其分配运行线程(执行栈)。
这时 g 处于 _Gidle 状态创建完成后,g 被更改为 _Gdead 状态,并根据要执行函数的入口地址和参数,初始化执行栈的 SP 和参数的入栈位置,并将需要的参数拷贝一份存入执行栈中根据 SP、参数,在 g.sched 中保存 SP 和 PC 指针来初始化 g 的运行现场将调用方、要执行的函数的入口 PC 进行保存,并将 g 的状态更改为 _Grunnable给 goroutine 分配 id,并将其放入 P 本地队列的队头或全局队列(初始化阶段队列肯定不是满的,因此不可能放入全局队列)检查空闲的 P,将其唤醒,准备执行 G,但我们目前处于初始化阶段,主 goroutine 尚未开始执行,因此这里不会唤醒 P。
gcinit
procresize
- 调用时已经 STW;
- 记录调整 P 的时间;
- 按需调整 allp 的大小;
- 按需初始化 allp 中的 P;
- 从 allp 移除不需要的 P,将释放的 P 队列中的任务扔进全局队列;
- 如果当前的 P 还可以继续使用(没有被移除),则将 P 设置为 _Prunning;
- 否则将第一个 P 抢过来给当前 G 的 M 进行绑定
- 最后挨个检查 P,将没有任务的 P 放入 idle 队列
- 出去当前 P 之外,将有任务的 P 彼此串联成链表,将没有任务的 P 放回到 idle 链表中