携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第25天,点击查看活动详情
GPM
推荐学习:
hello world!
我们一直写 Go 程序,你是否考虑过,一个 hello word!他是如何运行起来的。我们来深挖一波!!
package main
import "fmt"
func main() {
fmt.Println("hello world!")
}
这段程序编译后成为为一个可执行文件,执行时候可执行文件被加载到内存中。相信大家在学校里面学过编译原理,一段简单的汇编代码会分成多个段:数据段,代码段,堆栈段,子程序等等。
数据段
对于数据段,有很多重要的全局变量,我们一起来看看。
- 主协程:g0 与 主线程 m0
协程对应的数据结构是 runtime.g,工作线程对应的数据结构是 runtime.m。``
g0 就是主协程对应的 g,与其他协程不同,他的协程栈实际上是在主线程栈上分配的。
m0 是主线程对应的 m。
g0 持有 m0 的指针,m0 也记录着 g0 的指针。一开始 m0 上执行的协程正是 g0。m0 与 g0 就联系起来了。
- allgs,allm,allp
allgs 记录着所有的 g。
allm 记录着所有的 m。
allp 记录着所有的 p。
- sched
最初 Go 语言的调度模型里面只有 M 和 G,所以会有一个 G run queue 里面都是待执行的 G ,每个 M 来到队列获取一个 G 时候都会加锁。多个 M 会分担多个 G 的任务,途中会因为前面的 M 频繁加锁和解锁而发生等待,影响程序的并发性能。
为了解决这个问题,又引入了一个 P,P 对应的数据结构是 runtime.p。他有一个 runq [256]guintptr ,通过把一个 P 关联到一个 M,这个 M 就可以从 P 的本地 runq 这里获取待执行的 G,不用每次都与其他的 M 在队列中争抢任务了,性能也会提升。
全局 runq 保存在 全局变量 sched 中,sched 代表是调度器,对应的数据结构是 runtime.schedt。这里记录着所有空闲的 M,空闲的 P,以及许多与调度相关的内容。
如果一个 P 的本地 runq 已满,那么等待执行的 G 会被放到这个全局 runq 中。M 执行 G 过程如下:
M会先去执行对应的P本地runq中待执行的G,- 如果没有的话,再到调度器这里全局
runq领取任务。 - 如果也没有了,就会从别的
P那里分担一部分的G过来执行。

代码段
对于这段程序编译成的代码段,程序入口并不是 main.main,不同平台下程序开始进入入口不同。简单的流程如下:
- 进入后再进行一系列检查与初始化等准备工作后,
- 当
main.goroutine创建后会被加入到当前P的本地队列中, - 然后通过
mstart函数开启调度循环,队列中只有main.goroutine正在等待执行,所以g0会切换成main.goroutine, - 执行入口就是
runtime.main,他会做一些准备工作,监控线程,包初始化等, - 然后就要调用
main.main了,终于输出了hello world!。

在这里只是简单的用一个例子去介绍了一些相关的代码与变量,后续会进行详细解答。
详解 GPM
我们在前面总说 GPM ,那么他们分别代表什么意思呢?
G代表Goroutine,Golang中的协程,通过Goroutine封装的代码片段将以协程方式并发执行,是GPM调度器调度的基本单位。P代表Processor,GPM调度器中关联内核级线程与协程的中间调度器,帮助线程去执行协程的任务。M代表Machine,是内核线程的封装,Goroutine的执行提供了底层线程能力支持。
白话解释:
M需要去执行G的任务,为了更高并发执行性能,我们引入P,来起到帮助作用。
M
我们来看看 M 的结构:
type m struct {
g0 *g //g 结构体指针,主协程对应的 g0
mstartfn func() //函数类型,对应着当前内核线程需要执行的 Goroutine 函数片段
curg *g //g 结构体指针,对应着当前该 M 相关联(要执行)的 G。
p puintptr //地址类型,对应着当前该 M 关联的 P。
nextp puintptr //地址类型,标识下一个可能与该 M 存在关联的 P。
oldp puintptr //地址类型,记录上一个与该 M 关联的 P。
lockedg guintptr //地址类型,标识当前正在锁定该 M 的G,通过 LockOSThread 进行 G 和 M 的锁定,一旦 G 和 M 锁定后,该 G 只可由该 M 执行。
spinning bool //布尔类型,表示当前是否正在自旋,自旋则代表当前 M 正在寻找可执行的 G。
incgo bool //布尔类型,表示当前是否正在执行 cgo 调用。
ncgo int32 //int32类型,表示当前正在执行的 cgo 调用数目。
// 忽略
}
P
const (
_Pidle = iota //当前p尚未与任何m关联,处于空闲状态 ->0
_Prunning //当前p已经和m关联,并且正在运行g代码 ->1
_Psyscall //当前p正在执行系统调用 ->2
_Pgcstop //当前p需要停止调度,一般在GC前或者刚被创建时 ->3
_Pdead //当前p已死亡,不会再被调度 ->4
)
type p struct {
status uint32 //表示当前P的状态,为上述五个状态之一
schedtick uint32 //调度计数器,每被调度一次则自增1
syscalltick uint32 //系统调用计数器,每进行一次系统调用则自增1
m muintptr //即将要关联的m,M的nextp字段对应着该P
runqhead uint32 //可运行G队列头,标识目前正在运行的G
runqtail uint32 //可运行G队列尾
runq [256]guintptr //可运行的G队列,默认容量为256个G
runnext guintptr //下一个将要运行的G
gFree struct { //空闲G列表,存储着状态为Gdead的G,当其数目过多时,将会被转移到调度器全局G列表,用于被其他P再次使用(相当于一个G缓存池)
gList
n int32
}
}
P 的生命周期:
G
const (
_Gidle = iota //当前 G 刚被分配,还未初始化 ->0
_Grunnable //正在可运行队列等待运行 ->1
_Grunning //正在运行中,执行G函数 ->2
_Gsyscall //正在执行系统调用 ->3
_Gwaiting //正在被阻塞,一般是该G正在执行网络I/O操作,或正在执行time.Timer、time.Sleep ->4
_Gmoribund_unused //_Gmoribund_unused is currently unused, but hardcoded in gdb ->5
_Gdead //已经使用完正在闲置,放入空闲G列表中,可被再次使用(和P不同,P处于Pdead状态则无法被再次调度) ->6
_Genqueue_unused //_ Genqueue _ unused 当前未使用。 ->7
_Gcopystack //表示当前 G 的栈正在被移动,可能是因为栈的收缩或扩容 ->8
_Gscan = 0x1000 //表明当前正在进行GC扫描,由于在GC扫描的过程中肯定会处于某个前置状态,
_Gscanrunnable = _Gscan + _Grunnable //代表当前 G 正等待运行,同时栈正被 GC 扫描 // 0x1001
_Gscanrunning = _Gscan + _Grunning //表示正处于 Grunning状态,同时栈在被 GC 扫描 // 0x1002
_Gscansyscall = _Gscan + _Gsyscall //表示正处于 Gwaiting状态,同时栈在被 GC 扫描 // 0x1003
_Gscanwaiting = _Gscan + _Gwaiting //表示正处于 Gsyscall状态,同时栈在被 GC 扫描 // 0x1004
_Gscanpreempted = _Gscan + _Gpreempted // 0x1009
)
type g struct {
stack stack // offset known to runtime/cgo //当前G所被分配的栈内存空间,由lo及hi两个内存指针组成
stackguard0 uintptr // offset known to liblink g0的最大栈内存地址,当超过了这个数值则需要进行栈扩张
stackguard1 uintptr //普通用户G的最大栈内存地址,当超过了这个数值则需要进行栈扩张
m *m // current m; offset known to arm liblink 当前关联该G实例的M实例
sched gobuf //记录G上下文环境,用于上下文切换
atomicstatus uint32 //G的状态值,表示上述几个状态
waitreason waitReason // if status==Gwaiting 处于Gwaiting的原因
preempt bool // preemption signal, duplicates stackguard0 = st 当前G是否可抢占
startpc uintptr // pc of goroutine function 当前G所绑定的函数内存地址
}
G 生命周期:
sched (调度器)
type schedt struct {
// 全局唯一id
goidgen uint64
// 记录的最后一次从i/o中查询G的时间
lastpoll uint64
// 互斥锁
lock mutex
// M的空闲链表,通过m.schedlink组成一个M空闲链表
midle muintptr
// 正处于自旋状态的M数量
nmidle int32
// 已经被锁定且正在自旋的M数量
nmidlelocked int32
// 下一个M的id,或者是目前已存在的M数量
mnext int64
// M数量的最大值
maxmcount int32
// 已被释放掉的M数量
nmfreed int64
// 系统所开启的协程数量(非用户协程)
ngsys uint32
// 空闲P列表
pidle puintptr
// 空闲的P数量
npidle uint32
// 全局的G队列
// 根据runqhead可以获取队列头的G及g.schedlink形成G链表
runqhead guintptr
runqtail guintptr
// 全局G队列大小
runqsize int32
// 等待释放的M列表
freem *m
// 是否需要暂停调度(通常因为GC带来的STW)
gcwaiting uint32
// 需要停止但是仍为停止的P数量
stopwait int32
// 实现stopwait事件通知
stopnote note
// 停止调度期间是否进行系统监控任务
sysmonwait uint32
// 实现sysmonwait事件通知
sysmonnote note
}