xpm是什么

G、P、M是Go调度器的三个核心组件,各司其职。在它们精密地配合下,Go调度器得以高效运转,这也是Go天然支持高并发的内在动力。今天这篇文章我们来深入理解GPM模型。 先看G,取goroutine的首字母,主要保存goroutine的一些状态信息以及CPU的一些寄存器的值,例如IP寄存器,

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

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

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

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

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

typegstruct{

//goroutine使用的栈
stack       stack   //offsetknowntoruntime/cgo
//用于栈的扩张和收缩检查,抢占标志
stackguard0uintptr//offsetknowntoliblink
stackguard1uintptr//offsetknowntoliblink

_panic         *_panic//innermostpanic-offsetknowntoliblink
_defer         *_defer//innermostdefer
//当前与g绑定的m
m              *m      //currentm;offsetknowntoarmliblink
//goroutine的运行现场
sched          gobuf
syscallsp      uintptr        //ifstatus==Gsyscall,syscallsp=sched.sptouseduringgc
syscallpc      uintptr        //ifstatus==Gsyscall,syscallpc=sched.pctouseduringgc
stktopsp       uintptr        //expectedspattopofstack,tocheckintraceback
//wakeup时传入的参数
param          unsafe.Pointer//passedparameteronwakeup
atomicstatus   uint32
stackLock      uint32//sigprof/scanglock;TODO:foldintoatomicstatus
goid           int64
//g被阻塞之后的近似时间
waitsince      int64  //approxtimewhenthegbecomeblocked
//g被阻塞的原因
waitreason     string//ifstatus==Gwaiting
//指向全局队列里下一个g
schedlink      guintptr
//抢占调度标志。这个为true时,stackguard0等于stackpreempt
preempt        bool     //preemptionsignal,duplicatesstackguard0=stackpreempt
paniconfault   bool     //panic(insteadofcrash)onunexpectedfaultaddress
preemptscan    bool     //preemptedgdoesscanforgc
gcscandone     bool     //ghasscannedstack;protectedby_Gscanbitinstatus
gcscanvalid    bool     //falseatstartofgccycle,trueifGhasnotrunsincelastscan;TODO:remove?
throwsplit     bool     //mustnotsplitstack
raceignore     int8     //ignoreracedetectionevents
sysblocktracedbool     //StartTracehasemittedEvGoInSyscallaboutthisgoroutine
//syscall返回之后的cputicks,用来做tracing
sysexitticks   int64    //cputickswhensyscallhasreturned(fortracing)
traceseq       uint64   //traceeventsequencer
tracelastp     puintptr//lastPemittedaneventforthisgoroutine
//如果调用了LockOsThread,那么这个g会绑定到某个m上
lockedm        *m
sig            uint32
writebuf       []byte
sigcode0       uintptr
sigcode1       uintptr
sigpc          uintptr
//创建该goroutine的语句的指令地址
gopc           uintptr//pcofgostatementthatcreatedthisgoroutine
//goroutine函数的指令地址
startpc        uintptr//pcofgoroutinefunction
racectx        uintptr
waiting        *sudog         //sudogstructuresthisgiswaitingon(thathaveavalidelemptr);inlockorder
cgoCtxt        []uintptr      //cgotracebackcontext
labels         unsafe.Pointer//profilerlabels
//time.Sleep缓存的定时器
timer          *timer         //cachedtimerfortime.Sleep

gcAssistBytesint64
}

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

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

//描述栈的数据结构,栈的范围:[lo,hi)
typestackstruct{
//栈顶,低地址
louintptr
//栈低,高地址
hiuintptr
}

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

typegobufstruct{
//存储rsp寄存器的值
sp   uintptr
//存储rip寄存器的值
pc   uintptr
//指向goroutine
g    guintptr
ctxtunsafe.Pointer//thishastobeapointersothatgcscansit
//保存系统调用的返回值
ret  sys.Uintreg
lr   uintptr
bp   uintptr//forGOEXPERIMENT=framepointer
}

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

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

结构体m的源码如下:

//m代表工作线程,保存了自身使用的栈信息
typemstruct{
//记录工作线程(也就是内核线程)使用的栈信息。在执行调度代码时需要使用
//执行用户goroutine代码时,使用用户goroutine自己的栈,因此调度时会发生栈的切换
g0      *g     //goroutinewithschedulingstack/
morebufgobuf  //gobufargtomorestack
divmod  uint32//div/moddenominatorforarm-knowntoliblink

//Fieldsnotknowntodebuggers.
procid        uint64     //fordebuggers,butoffsetnothard-coded
gsignal       *g         //signal-handlingg
sigmask       sigset     //storageforsavedsignalmask
//通过tls结构体实现m与工作线程的绑定
//这里是线程本地存储
tls           [6]uintptr//thread-localstorage(forx86externregister)
mstartfn      func()
//指向正在运行的gorutine对象
curg          *g       //currentrunninggoroutine
caughtsig     guintptr//goroutinerunningduringfatalsignal
//当前工作线程绑定的p
p             puintptr//attachedpforexecutinggocode(nilifnotexecutinggocode)
nextp         puintptr
id            int32
mallocing     int32
throwing      int32
//该字段不等于空字符串的话,要保持curg始终在这个m上运行
preemptoff    string//if!="",keepcurgrunningonthism
locks         int32
softfloat     int32
dying         int32
profilehz     int32
helpgc        int32
//为true时表示当前m处于自旋状态,正在从其他线程偷工作
spinning      bool//misoutofworkandisactivelylookingforwork
//m正阻塞在note上
blocked       bool//misblockedonanote
//m正在执行writebarrier
inwb          bool//misexecutingawritebarrier
newSigstack   bool//minitonCthreadcalledsigaltstack
printlock     int8
//正在执行cgo调用
incgo         bool//misexecutingacgocall
fastrand      uint32
//cgo调用总计数
ncgocall      uint64      //numberofcgocallsintotal
ncgo          int32       //numberofcgocallscurrentlyinprogress
cgoCallersUseuint32      //ifnon-zero,cgoCallersinusetemporarily
cgoCallers    *cgoCallers//cgotracebackifcrashingincgocall
//没有goroutine需要运行时,工作线程睡眠在这个park成员上,
//其它线程通过这个park唤醒该工作线程
park          note
//记录所有工作线程的链表
alllink       *m//onallm
schedlink     muintptr
mcache        *mcache
lockedg       *g
createstack   [32]uintptr//stackthatcreatedthisthread.
freglo        [16]uint32  //d[i]lsbandf[i]
freghi        [16]uint32  //d[i]msbandf[i+16]
fflag         uint32      //floatingpointcompareflags
locked        uint32      //trackingforlockosthread
//正在等待锁的下一个m
nextwaitm     uintptr     //nextmwaitingforlock
needextram    bool
traceback     uint8
waitunlockf   unsafe.Pointer//todogofunc(*g,unsafe.pointer)bool
waitlock      unsafe.Pointer
waittraceev   byte
waittraceskipint
startingtracebool
syscalltick   uint32
//工作线程id
thread        uintptr//threadhandle

//theseareherebecausetheyaretoolargetobeonthestack
//oflow-levelNOSPLITfunctions.
libcall   libcall
libcallpcuintptr//forcpuprofiler
libcallspuintptr
libcallg  guintptr
syscall   libcall//storessyscallparametersonwindows

mOS
}

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

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

//p保存go运行时所必须的资源
typepstruct{
lockmutex

//在allp中的索引
id          int32
status      uint32//oneofpidle/prunning/...
link        puintptr
//每次调用schedule时会加一
schedtick   uint32
//每次系统调用时加一
syscalltickuint32
//用于sysmon线程记录被监控p的系统调用时间和运行时间
sysmontick  sysmontick//lasttickobservedbysysmon
//指向绑定的m,如果p是idle的话,那这个指针是nil
m           muintptr   //back-linktoassociatedm(nilifidle)
mcache      *mcache
racectx     uintptr

deferpool    [5][]*_defer//poolofavailabledeferstructsofdifferentsizes(seepanic.go)
deferpoolbuf[5][32]*_defer

//Cacheofgoroutineids,amortizesaccessestoruntime·sched.goidgen.
goidcache    uint64
goidcacheenduint64

//Queueofrunnablegoroutines.Accessedwithoutlock.
//本地可运行的队列,不用通过锁即可访问
runqheaduint32//队列头
runqtailuint32//队列尾
//使用数组实现的循环队列
runq     [256]guintptr

//runnext非空时,代表的是一个runnable状态的G,
//这个G被当前G修改为ready状态,相比runq中的G有更高的优先级。
//如果当前G还有剩余的可用时间,那么就应该运行这个G
//运行之后,该G会继承当前G的剩余时间
runnextguintptr

//AvailableG's(status==Gdead)
//空闲的g
gfree    *g
gfreecntint32

sudogcache[]*sudog
sudogbuf   [128]*sudog

tracebuftraceBufPtr
traceSwept,traceReclaimeduintptr

pallocpersistentAlloc//per-Ptoavoidmutex

//Per-PGCstate
gcAssistTime     int64//NanosecondsinassistAlloc
gcBgMarkWorker   guintptr
gcMarkWorkerModegcMarkWorkerMode
runSafePointFnuint32//if1,runsched.safePointFnatnextsafepoint

pad[sys.CacheLineSize]byte
}

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

描述三者的关系:

在这里插入图片描述

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

最后我们从宏观上总结一下GPM,这篇文章尝试从它们的状态流转角度总结。

首先是G的状态流转:

在这里插入图片描述

说明一下,上图省略了一些垃圾回收的状态。

接着是P的状态流转:

在这里插入图片描述

通常情况下(在程序运行时不调整P的个数),P只会在上图中的四种状态下进行切换。当程序刚开始运行进行初始化时,所有的P都处于_Pgcstop状态,随着P的初始化(runtime.procresize),会被置于_Pidle

当M需要运行时,会runtime.acquirep来使P变成Prunning状态,并通过runtime.releasep来释放。

当G执行时需要进入系统调用,P会被设置为_Psyscall,如果这个时候被系统监控抢夺(runtime.retake),则P会被重新修改为_Pidle

如果在程序运行中发生GC,则P会被设置为_Pgcstop,并在runtime.startTheWorld时重新调整为_Prunning

最后,我们来看M的状态变化:

在这里插入图片描述

M只有自旋和非自旋两种状态。自旋的时候,会努力找工作;找不到的时候会进入非自旋状态,之后会休眠,直到有工作需要处理时,被其他工作线程唤醒,又进入自旋状态。

本文节选于Go合集《Go 语言问题集》:GOLANG ROADMAP 一个专注Go语言学习、求职的社区。

知秋君
上一篇 2024-08-08 12:02
下一篇 2024-08-08 11:36

相关推荐