golang底层 引导、初始化
CALL runtime·args(SB) // 整理命令行参数
CALL runtime·osinit(SB) // 确定cpu核心数
CALL runtime·schedinit(SB) // 初始化核心组件
CALL runtime·newproc(SB) // 创建主goroutine即runtime.main对应的g
CALL runtime·mstart(SB) // 启动调度循环
schedinit 调度器初始化,mcommoninit 初始化当前m,procresize创建p,保存到全局allp和sched.pidle中
schedinit -> mcommoninit
-> procresize -> pidleput
在调度器的初始化过程中,首先通过 mcommoninit 对 M 的信号 G 进行初始化。 而后通过procresize 创建与 CPU 核心数 (或与用户指定的 GOMAXPROCS) 相同的 P。 最后通过 newproc 创建包含要执行函数的执行栈、运行现场的 G,并将创建的 G 放入刚创建好的 P 的本地可运行队列(第一个入队的 G,也就是主 goroutine 要执行的函数体), 完成 G 的创建。
mcommoninit 只是对 M 进行一个初步的初始化
procresize:调整p数量,默认只有 schedinit 和 startTheWorld 会调⽤ procresize 函数
- 调用时已经 STW,记录调整 P 的时间;
- 按需调整 allp 的大小,按需初始化 allp 中的 P;
- 如果当前的 P 还可以继续使用(没有被移除),则将 P 设置为 _Prunning;否则将第一个 P 抢过来和当前 M 绑定
- 从 allp 移除不需要的 P,将释放的 P 队列中的任务扔进全局队列;
- 最后挨个检查 P,将没有任务的 P 放入 idle 队列
- 出去当前 P 之外,将有任务的 P 彼此串联成链表,将没有任务的 P 放回到 idle 链表中
初始化阶段,刚刚初始化了m0,所以第3步会将allp[0]绑定到m0上
运行时修改p的数量:runtime.GOMAXPROCS,需要stopTheWorld,startTheWorld
环境变量GOMAXPROCS
p最多256个
g的创建 go语句被编译成newproc,
- 先尝试gfget从本地复用链表里获取g,本地链表为空,则从全局移动一批到本地,全局也为空则malg新建g,此时g处于_Gidle 状态,默认栈为2kb,会被allg引用
- 创建完成后,g 被更改为 _Gdead 状态,并根据要执行函数的入口地址和参数,初始化执行栈的 SP 和参数的入栈位置,并将需要的参数拷贝一份存入执行栈中
- 根据 SP、参数,在 g.sched 中保存 SP 和 PC 指针来初始化 g 的运行现场
- 将调用方、要执行的函数的入口 PC 进行保存,并将 g 的状态更改为 _Grunnable
- 给 goroutine 分配 id,然后runqput放入p的runnext,原本runnext g放到本地队列p.runq里,本地放不下则runqputslow转移一半到全局sched.runq里
- 在某些情况下调用wakep唤醒一个空闲的p
newproc -> newproc1 -> gfget
-> malg
-> allgadd
-> runqput -> runqputslow
-> wakep
-> releasem
初始化时,newproc创建main goroutine对应的g,并将其放入本地队列
mstart,让当前runtime.m0被调度,执行runtime.main,即主goroutine。主 goroutine 会以被调度器调度的方式进行运行
mstart 除了在初始化阶段会被运行之外,也可能在每个 m 被创建时运行
runtime.main,即main goroutine主要做了以下几件事:
- 设定每个g能申请的最大栈空间(250M 或 1G)
- systemstack 会运行 newm(sysmon, nil) 启动后台监控
- runtime_init 负责执行运行时的多个初始化函数 runtime.init
- gcenable 启用垃圾回收器
- main_init 开始执行用户态 main.init 函数,这意味着所有的 main.init 均在同一个主 goroutine 中执行
- main_main 开始执行用户态 main.main 函数,这意味着 main.main 和 main.init 均在同一个 goroutine 中执行。
runtime.main -> newm(sysmon..) -> lockOsThread -> runtime.init -> runtime.gcenable -> main.init -> main.main
监控
sysmon -> retake -> handoffp / preemptone
sysmon线程,在runtime.main中启动,不在runtime.m0中运行,在单独的m中运行,
这个m不需要绑定p,与调度系统无关,内部是死循环,主要负责以下几件事:
- checkdead,检查是否所有 goroutine 都已经锁死,如果是的话,直接调用 runtime.throw,强制退出。这个操作只在启动的时候做一次
- 然后进入死循环,每次循环开头都会休眠,休眠时间开始为20us,如果连续50次循环没有gc,也没有抢占P和G,则把休眠时间翻倍,但最多不超过10ms。休眠结束后,如果正在gc,则继续休眠,此时休眠会被设置超时
- 超过10ms没有poll网络,则执行 netpoll 并将返回的结果注入到全局 sched 的任务队列,如果有空闲的p,则启动m来执行这些g
- 抢占因为 syscall 而长时间阻塞的 p,p的runq队列不为空则启动m来关联p
- 执行时间过长的 g
- 最近的timer的触发时间小于当前时间,则启动m来触发计时器
- 如果 span 内存闲置超过 5min,那么释放掉
- 如果超过 2 分钟没有gc,强制执⾏。
在进⼊垃圾回收状态时,sysmon 会⾃动进⼊休眠,所以我们才会在 syscall ⾥看到很多唤醒指令。另外,startTheWorld 也会做唤醒处理。
疑问:
sysmon发现超时的timer,启动一个m,这个m不执行这个timer怎么办???
阻塞于netpoll,有g就绪了也不会执行???