稍微了解过 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 的状态,有以下几种

  1. _Pidle, 处理器没有运行用户代码或者调度器
  2. _Prunning, p 被 m 持有,并且正在执行用户代码或者调度器。只能由拥有当前P的M才可能修改此状态。
  3. _Psyscall, 当前P没有执行用户代码,当前线程陷入系统调用
  4. _Pgcstop, p 被 m 持有,当前 p 由于垃圾回收被停止
  5. _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。

参考资料