手记

三足鼎立 —— GPM 到底是什么?(一)

G、P、M 是 Go 调度器的三个核心组件,各司其职。在它们精密地配合下,Go 调度器得以高效运转,这也是 Go 天然支持高并发的内在动力。今天这篇文章我们来深入理解 GPM 模型。

先看 G,取 goroutine 的首字母,主要保存 goroutine 的一些状态信息以及 CPU 的一些寄存器的值,例如 IP 寄存器,以便在轮到本 goroutine 执行时,CPU 知道要从哪一条指令处开始执行。

当 goroutine 被调离 CPU 时,调度器负责把 CPU 寄存器的值保存在 g 对象的成员变量之中。

当 goroutine 被调度起来运行时,调度器又负责把 g 对象的成员变量所保存的寄存器值恢复到 CPU 的寄存器。

上面这段描述来自公众号“go语言核心编程技术”的调度器系列文章,写得非常好,推荐大家去看,参考资料【阿波张调度器系列教程】可以到达原文。

本系列教程使用的代码版本是 1.9.2,来看一下 g 的源码:

  1. type g struct {


  2.    // goroutine 使用的栈

  3.    stack       stack   // offset known to runtime/cgo

  4.    // 用于栈的扩张和收缩检查,抢占标志

  5.    stackguard0 uintptr // offset known to liblink

  6.    stackguard1 uintptr // offset known to liblink


  7.    _panic         *_panic // innermost panic - offset known to liblink

  8.    _defer         *_defer // innermost defer

  9.    // 当前与 g 绑定的 m

  10.    m              *m      // current m; offset known to arm liblink

  11.    // goroutine 的运行现场

  12.    sched          gobuf

  13.    syscallsp      uintptr        // if status==Gsyscall, syscallsp = sched.sp to use during gc

  14.    syscallpc      uintptr        // if status==Gsyscall, syscallpc = sched.pc to use during gc

  15.    stktopsp       uintptr        // expected sp at top of stack, to check in traceback

  16.    // wakeup 时传入的参数

  17.    param          unsafe.Pointer // passed parameter on wakeup

  18.    atomicstatus   uint32

  19.    stackLock      uint32 // sigprof/scang lock; TODO: fold in to atomicstatus

  20.    goid           int64

  21.    // g 被阻塞之后的近似时间

  22.    waitsince      int64  // approx time when the g become blocked

  23.    // g 被阻塞的原因

  24.    waitreason     string // if status==Gwaiting

  25.    // 指向全局队列里下一个 g

  26.    schedlink      guintptr

  27.    // 抢占调度标志。这个为 true 时,stackguard0 等于 stackpreempt

  28.    preempt        bool     // preemption signal, duplicates stackguard0 = stackpreempt

  29.    paniconfault   bool     // panic (instead of crash) on unexpected fault address

  30.    preemptscan    bool     // preempted g does scan for gc

  31.    gcscandone     bool     // g has scanned stack; protected by _Gscan bit in status

  32.    gcscanvalid    bool     // false at start of gc cycle, true if G has not run since last scan; TODO: remove?

  33.    throwsplit     bool     // must not split stack

  34.    raceignore     int8     // ignore race detection events

  35.    sysblocktraced bool     // StartTrace has emitted EvGoInSyscall about this goroutine

  36.    // syscall 返回之后的 cputicks,用来做 tracing

  37.    sysexitticks   int64    // cputicks when syscall has returned (for tracing)

  38.    traceseq       uint64   // trace event sequencer

  39.    tracelastp     puintptr // last P emitted an event for this goroutine

  40.    // 如果调用了 LockOsThread,那么这个 g 会绑定到某个 m 上

  41.    lockedm        *m

  42.    sig            uint32

  43.    writebuf       []byte

  44.    sigcode0       uintptr

  45.    sigcode1       uintptr

  46.    sigpc          uintptr

  47.    // 创建该 goroutine 的语句的指令地址

  48.    gopc           uintptr // pc of go statement that created this goroutine

  49.    // goroutine 函数的指令地址

  50.    startpc        uintptr // pc of goroutine function

  51.    racectx        uintptr

  52.    waiting        *sudog         // sudog structures this g is waiting on (that have a valid elem ptr); in lock order

  53.    cgoCtxt        []uintptr      // cgo traceback context

  54.    labels         unsafe.Pointer // profiler labels

  55.    // time.Sleep 缓存的定时器

  56.    timer          *timer         // cached timer for time.Sleep


  57.    gcAssistBytes int64

  58. }

源码中,比较重要的字段我已经作了注释,其他未作注释的与调度关系不大或者我暂时也没有理解的。

g 结构体关联了两个比较简单的结构体,stack 表示 goroutine 运行时的栈:

// 描述栈的数据结构,栈的范围:[lo, hi)type stack struct {    // 栈顶,低地址    lo uintptr    // 栈低,高地址    hi uintptr}

Goroutine 运行时,光有栈还不行,至少还得包括 PC,SP 等寄存器,gobuf 就保存了这些值:

type gobuf struct {    // 存储 rsp 寄存器的值    sp   uintptr    // 存储 rip 寄存器的值    pc   uintptr    // 指向 goroutine    g    guintptr    ctxt unsafe.Pointer // this has to be a pointer so that gc scans it    // 保存系统调用的返回值    ret  sys.Uintreg    lr   uintptr    bp   uintptr // for GOEXPERIMENT=framepointer}

再来看 M,取 machine 的首字母,它代表一个工作线程,或者说系统线程。G 需要调度到 M 上才能运行,M 是真正工作的人。结构体 m 就是我们常说的 M,它保存了 M 自身使用的栈信息、当前正在 M 上执行的 G 信息、与之绑定的 P 信息……

当 M 没有工作可做的时候,在它休眠前,会“自旋”地来找工作:检查全局队列,查看 network poller,试图执行 gc 任务,或者“偷”工作。

结构体 m 的源码如下:

  1. // m 代表工作线程,保存了自身使用的栈信息

  2. type m struct {

  3.    // 记录工作线程(也就是内核线程)使用的栈信息。在执行调度代码时需要使用

  4.    // 执行用户 goroutine 代码时,使用用户 goroutine 自己的栈,因此调度时会发生栈的切换

  5.    g0      *g     // goroutine with scheduling stack/

  6.    morebuf gobuf  // gobuf arg to morestack

  7.    divmod  uint32 // div/mod denominator for arm - known to liblink


  8.    // Fields not known to debuggers.

  9.    procid        uint64     // for debuggers, but offset not hard-coded

  10.    gsignal       *g         // signal-handling g

  11.    sigmask       sigset     // storage for saved signal mask

  12.    // 通过 tls 结构体实现 m 与工作线程的绑定

  13.    // 这里是线程本地存储

  14.    tls           [6]uintptr // thread-local storage (for x86 extern register)

  15.    mstartfn      func()

  16.    // 指向正在运行的 gorutine 对象

  17.    curg          *g       // current running goroutine

  18.    caughtsig     guintptr // goroutine running during fatal signal

  19.    // 当前工作线程绑定的 p

  20.    p             puintptr // attached p for executing go code (nil if not executing go code)

  21.    nextp         puintptr

  22.    id            int32

  23.    mallocing     int32

  24.    throwing      int32

  25.    // 该字段不等于空字符串的话,要保持 curg 始终在这个 m 上运行

  26.    preemptoff    string // if != "", keep curg running on this m

  27.    locks         int32

  28.    softfloat     int32

  29.    dying         int32

  30.    profilehz     int32

  31.    helpgc        int32

  32.    // 为 true 时表示当前 m 处于自旋状态,正在从其他线程偷工作

  33.    spinning      bool // m is out of work and is actively looking for work

  34.    // m 正阻塞在 note 上

  35.    blocked       bool // m is blocked on a note

  36.    // m 正在执行 write barrier

  37.    inwb          bool // m is executing a write barrier

  38.    newSigstack   bool // minit on C thread called sigaltstack

  39.    printlock     int8

  40.    // 正在执行 cgo 调用

  41.    incgo         bool // m is executing a cgo call

  42.    fastrand      uint32

  43.    // cgo 调用总计数

  44.    ncgocall      uint64      // number of cgo calls in total

  45.    ncgo          int32       // number of cgo calls currently in progress

  46.    cgoCallersUse uint32      // if non-zero, cgoCallers in use temporarily

  47.    cgoCallers    *cgoCallers // cgo traceback if crashing in cgo call

  48.    // 没有 goroutine 需要运行时,工作线程睡眠在这个 park 成员上,

  49.    // 其它线程通过这个 park 唤醒该工作线程

  50.    park          note

  51.    // 记录所有工作线程的链表

  52.    alllink       *m // on allm

  53.    schedlink     muintptr

  54.    mcache        *mcache

  55.    lockedg       *g

  56.    createstack   [32]uintptr // stack that created this thread.

  57.    freglo        [16]uint32  // d[i] lsb and f[i]

  58.    freghi        [16]uint32  // d[i] msb and f[i+16]

  59.    fflag         uint32      // floating point compare flags

  60.    locked        uint32      // tracking for lockosthread

  61.    // 正在等待锁的下一个 m

  62.    nextwaitm     uintptr     // next m waiting for lock

  63.    needextram    bool

  64.    traceback     uint8

  65.    waitunlockf   unsafe.Pointer // todo go func(*g, unsafe.pointer) bool

  66.    waitlock      unsafe.Pointer

  67.    waittraceev   byte

  68.    waittraceskip int

  69.    startingtrace bool

  70.    syscalltick   uint32

  71.    // 工作线程 id

  72.    thread        uintptr // thread handle


  73.    // these are here because they are too large to be on the stack

  74.    // of low-level NOSPLIT functions.

  75.    libcall   libcall

  76.    libcallpc uintptr // for cpu profiler

  77.    libcallsp uintptr

  78.    libcallg  guintptr

  79.    syscall   libcall // stores syscall parameters on windows


  80.    mOS

  81. }

再来看 P,取 processor 的首字母,为 M 的执行提供“上下文”,保存 M 执行 G 时的一些资源,例如本地可运行 G 队列,memeory cache 等。

一个 M 只有绑定 P 才能执行 goroutine,当 M 被阻塞时,整个 P 会被传递给其他 M ,或者说整个 P 被接管。

  1. // p 保存 go 运行时所必须的资源

  2. type p struct {

  3.    lock mutex


  4.    // 在 allp 中的索引

  5.    id          int32

  6.    status      uint32 // one of pidle/prunning/...

  7.    link        puintptr

  8.    // 每次调用 schedule 时会加一

  9.    schedtick   uint32

  10.    // 每次系统调用时加一

  11.    syscalltick uint32

  12.    // 用于 sysmon 线程记录被监控 p 的系统调用时间和运行时间

  13.    sysmontick  sysmontick // last tick observed by sysmon

  14.    // 指向绑定的 m,如果 p 是 idle 的话,那这个指针是 nil

  15.    m           muintptr   // back-link to associated m (nil if idle)

  16.    mcache      *mcache

  17.    racectx     uintptr


  18.    deferpool    [5][]*_defer // pool of available defer structs of different sizes (see panic.go)

  19.    deferpoolbuf [5][32]*_defer


  20.    // Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.

  21.    goidcache    uint64

  22.    goidcacheend uint64


  23.    // Queue of runnable goroutines. Accessed without lock.

  24.    // 本地可运行的队列,不用通过锁即可访问

  25.    runqhead uint32 // 队列头

  26.    runqtail uint32 // 队列尾

  27.    // 使用数组实现的循环队列

  28.    runq     [256]guintptr


  29.    // runnext 非空时,代表的是一个 runnable 状态的 G,

  30.    // 这个 G 被 当前 G 修改为 ready 状态,相比 runq 中的 G 有更高的优先级。

  31.    // 如果当前 G 还有剩余的可用时间,那么就应该运行这个 G

  32.    // 运行之后,该 G 会继承当前 G 的剩余时间

  33.    runnext guintptr


  34.    // Available G's (status == Gdead)

  35.    // 空闲的 g

  36.    gfree    *g

  37.    gfreecnt int32


  38.    sudogcache []*sudog

  39.    sudogbuf   [128]*sudog


  40.    tracebuf traceBufPtr

  41.    traceSwept, traceReclaimed uintptr


  42.    palloc persistentAlloc // per-P to avoid mutex


  43.    // Per-P GC state

  44.    gcAssistTime     int64 // Nanoseconds in assistAlloc

  45.    gcBgMarkWorker   guintptr

  46.    gcMarkWorkerMode gcMarkWorkerMode

  47.    runSafePointFn uint32 // if 1, run sched.safePointFn at next safe point


  48.    pad [sys.CacheLineSize]byte

  49. }

GPM 三足鼎力,共同成就 Go scheduler。G 需要在 M 上才能运行,M 依赖 P 提供的资源,P 则持有待运行的 G。你中有我,我中有你。

借用曹大 golang notes 的一幅图,描述三者的关系:

M 会从与它绑定的 P 的本地队列获取可运行的 G,也会从 network poller 里获取可运行的 G,还会从其他 P 偷 G。


0人推荐
随时随地看视频
慕课网APP