稍微了解过 Go runtime 的人想必都听过 goroutine 的 PMG 模型,哪么它到底代表什么意思呢? Golang 源码中又是如何实现的?
前言
关于 PMG 的解释网上有很对,随便 copy 一个:
- M 代表
Machine
,系统线程,它由操作系统管理的,goroutine就是跑在M之上的;M 是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息。 - P 是
Processor
,处理器,它的主要用途就是用来执行goroutine的,它维护了一个goroutine队列,即runqueue。Processor是让我们从N:1调度到M:N调度的重要部分。 - G 代表
goroutine
它包含了栈,指令指针,以及其他对调度goroutine很重要的信息,例如其阻塞的channel。
通常 go 程序中可以用 GOMAXPROCS
设置 Processor 的个数; 而 M 则是 clone系统调用创建的,或者用linux pthread 库创建出来的线程实体。 M 与 P 是一对一的关系。
基本结构体
打开 src/runtime/runtime2.go
文件,p,m,g 三个结构体的定义是按顺序在一起的,除此之外还有一个 schedt,与 goroutine 的调度相关。
g 结构体
G 就是 goroutine 的意思,每个 Goroutine 对应一个 g 结构体,它有自己的栈内存, G 存储 Goroutine 的运行堆栈、状态以及任务函数。 当一个 goroutine 退出时,g 会被放到一个空闲的对象池中以用于后续的 goroutine 的使用, 以减少内存分配开销。
g 结构体的成员变量非常多,分批介绍几个关键的变量,其他的暂时还不理解用途的以后再更新。
type g struct {
m *m // m 结构体指针
sched gobuf // 与 goroutine 调度相关
atomicstatus uint32 // 表示 goroutine 的状态
}
在源码文件的开头定义了多个 goroutine 的状态,部分状态转换的关系如下
其中 gobuf 结构体定义
type gobuf struct {
sp uintptr // 栈指针位置
pc uintptr // 程序计数器
g guintptr // 当前 gobuf 的 Goroutine
ctxt unsafe.Pointer
ret sys.Uintreg // 系统调用的结果
lr uintptr
bp uintptr // for GOEXPERIMENT=framepointer
}
接下来是与抢占相关的
type g struct {
stack stack
stackguard0 uintptr
stackguard1 uintptr
preempt bool // 抢占标记,其值为true 执行 stackguard0 = stackpreempt
preemptStop bool // 将抢占标记修改为 _Gpreedmpted,如果修改失败则取消
preemptShrink bool // shrink stack at synchronous safe point
}
stack 就是 goroutine 用了栈信息的结构,stackguard0 和 stackguard1 均是一个栈指针,用于扩容场景,前者用于 Go stack ,后者用于 C stack。
如果 stackguard0 字段被设置成 stackpreempt
意味着当前 Goroutine 发出了抢占请求。
p 结构体
p 对应操作系统线程,表示逻辑处理器。
type p struct {
status uint32
schedtick uint32 // incremented on every scheduler call
syscalltick uint32 // incremented on every system call
sysmontick sysmontick // last tick observed by sysmon
}
status 表示 p 的状态,有以下几种
_Pidle
, 处理器没有运行用户代码或者调度器_Prunning
, p 被 m 持有,并且正在执行用户代码或者调度器。只能由拥有当前P的M才可能修改此状态。_Psyscall
, 当前P没有执行用户代码,当前线程陷入系统调用_Pgcstop
, p 被 m 持有,当前 p 由于垃圾回收被停止_Pdead
,当前 p 已经不被使用,如通过动态调小 GOMAXPROCS进行P收缩
p 的状态转换关系如下图:
type p struct {
m muintptr // back-link to associated m (nil if idle)
mcache *mcache
pcache pageCache
raceprocctx uintptr
}
其中
mcache
是每个p的小对象缓存,无锁,对应 mcache 结构体pcache
页面缓存,对应 pageCache 结构体,不需要锁
接下来是运行队列相关的
type p struct {
runqhead uint32
runqtail uint32
runq [256]guintptr
// runnext, if non-nil, is a runnable G that was ready'd by
// the current G and should be run next instead of what's in
// runq if there's time remaining in the running G's time
// slice. It will inherit the time left in the current time
// slice. If a set of goroutines is locked in a
// communicate-and-wait pattern, this schedules that set as a
// unit and eliminates the (potentially large) scheduling
// latency that otherwise arises from adding the ready'd
// goroutines to the end of the run queue.
runnext guintptr
}
每个 P 都有一个自己的runq,用来存放可以 runnable 状态的 goroutines, 最多可以存放256个 goroutine。一般在介绍GMP关系时,我们称之为 local queue,还有一个global queue 。
关于其他的字段先暂时忽略。
m 结构体
上一节介绍 p 的数量是和 GOMAXPROCS
绑定的,有多少个 GOMAXPROCS
就有多少个 p。 而 m 的数量则不一定,m 对应操作系统线程,由 Go Runtime 决定。
挑几个重要的结构体成员来说明一下
type m struct {
g0 *g // goroutine with scheduling stack
morebuf gobuf // gobuf arg to morestack
curg *g // current running goroutine
caughtsig guintptr // goroutine running during fatal signal
p puintptr // attached p for executing go code (nil if not executing go code)
nextp puintptr
oldp puintptr
}
g0,是带有调度栈的 goroutine,这是一个比较特殊的 goroutine。普通的 goroutine 的栈是在堆上分配的可增长的栈,而 g0的栈是 M 对应的线程的栈。所有调度相关的代码,会先切换到该 goroutine 的栈中再执行。也就是说线程的栈也是用的g实现,而不是使用的OS的。
gobuf 结构体在前面已经见过,用于保存 g 的上下文信息。
- curg 表示当前正在运行的 goroutine,
- p 用于执行go code 的 p,就是当前正在m绑定的P,如果没有运行code 的话,值为nil
- nextp 下次运行的 p
- oldp 上次运行的 p
剩下的很多成员等到分析具体的代码时再做解释。
schedt
schedt
与 goroutine 的调度相关。 前面我们看了 p,m,g 的基本结构和功能,那么 schedt 就是将这三者结合起来的角色。
调度器循环的机制大致是从各种队列、 P 的本地队列中获取 G,然后切换到 G 的执行栈上并执行 G 的函数,调用 Goexit 做清理工作并回到 M,如此反复
schedt 结构体也非常庞大,本文中只关注一下几个队列成员。
type schedt struct {
// Global runnable queue.
runq gQueue
runqsize int32
midle muintptr // idle状态的m
pidle puintptr // idle的p
}
我们看到有 M 的 idle 队列,P 的 idle 队列,还有一个 runq
。 记得在前面 p 结构体中也有一个 runq,那么 schedt 中的与 p 中的区别是什么呢?
schedt 中的 runq 可以被称为全局队列,p 中的 runq 可以被称为本地队列,当一个 goroutine 被创建是,也就是一个新 g,会优先放在 p 的本地队列,当本地队列放满 256 个后,再放入全局队列。
M 会先从关联的 P 的本地队列中获取待执行的 G,没有的话,再到全局队列中获取;如果这里也没有了,就去其他P的本地队列中获取一些任务。
本文是研究 golang runtime 的第一篇文章,大概了解一下 runtime 中 PMG 结构体,并没有对相关代码进行详细的分析,在后续的文章中会更加深入的研究 runtime。