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 函数

  1. 调用时已经 STW,记录调整 P 的时间;
  2. 按需调整 allp 的大小,按需初始化 allp 中的 P;
  3. 如果当前的 P 还可以继续使用(没有被移除),则将 P 设置为 _Prunning;否则将第一个 P 抢过来和当前 M 绑定
  4.  allp 移除不需要的 P,将释放的 P 队列中的任务扔进全局队列;
  5. 最后挨个检查 P,将没有任务的 P 放入 idle 队列
  6. 出去当前 P 之外,将有任务的 P 彼此串联成链表,将没有任务的 P 放回到 idle 链表中

初始化阶段,刚刚初始化了m0,所以第3步会将allp[0]绑定到m0上

 

运行时修改p的数量:runtime.GOMAXPROCS,需要stopTheWorld,startTheWorld

环境变量GOMAXPROCS

p最多256个

 

g的创建 go语句被编译成newproc,

  1. 先尝试gfget从本地复用链表里获取g,本地链表为空,则从全局移动一批到本地,全局也为空则malg新建g,此时g处于_Gidle 状态,默认栈为2kb,会被allg引用
  2. 创建完成后,g 被更改为 _Gdead 状态,并根据要执行函数的入口地址和参数,初始化执行栈的 SP 和参数的入栈位置,并将需要的参数拷贝一份存入执行栈中
  3. 根据 SP、参数,在 g.sched 中保存 SP 和 PC 指针来初始化 g 的运行现场
  4. 将调用方、要执行的函数的入口 PC 进行保存,并将 g 的状态更改为 _Grunnable
  5. goroutine 分配 id,然后runqput放入p的runnext,原本runnext g放到本地队列p.runq里,本地放不下则runqputslow转移一半到全局sched.runq里
  6. 在某些情况下调用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主要做了以下几件事:

  1. 设定每个g能申请的最大栈空间(250M 或 1G)
  2. systemstack 会运行 newm(sysmon, nil) 启动后台监控
  3. runtime_init 负责执行运行时的多个初始化函数 runtime.init
  4. gcenable 启用垃圾回收器
  5. main_init 开始执行用户态 main.init 函数,这意味着所有的 main.init 均在同一个主 goroutine 中执行
  6. 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,与调度系统无关,内部是死循环,主要负责以下几件事:

  1. checkdead,检查是否所有 goroutine 都已经锁死,如果是的话,直接调用 runtime.throw,强制退出。这个操作只在启动的时候做一次
  2. 然后进入死循环,每次循环开头都会休眠,休眠时间开始为20us,如果连续50次循环没有gc,也没有抢占P和G,则把休眠时间翻倍,但最多不超过10ms。休眠结束后,如果正在gc,则继续休眠,此时休眠会被设置超时
  3. 超过10ms没有poll网络,则执行 netpoll 并将返回的结果注入到全局 sched 的任务队列,如果有空闲的p,则启动m来执行这些g
  4. 抢占因为 syscall 而长时间阻塞的 p,p的runq队列不为空则启动m来关联p
  5. 执行时间过长的 g
  6. 最近的timer的触发时间小于当前时间,则启动m来触发计时器
  7. 如果 span 内存闲置超过 5min,那么释放掉
  8. 如果超过 2 分钟没有gc,强制执⾏。

在进⼊垃圾回收状态时,sysmon 会⾃动进⼊休眠,所以我们才会在 syscall ⾥看到很多唤醒指令。另外,startTheWorld 也会做唤醒处理。

 

疑问:

sysmon发现超时的timer,启动一个m,这个m不执行这个timer怎么办???

阻塞于netpoll,有g就绪了也不会执行???

 

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