• golang的垃圾回收算法之五GMP模型


    先解释一下为什么把这个放到了前面,没办法啊,越分析发现越需要解释这三个G 、M 、P是什么东东。学过操作系统的都应该知道,操作系统面对的其实就是进程,线程其实是后来加进去的。以至于现在看一些稍微古旧一些的计算机操作系统方面的书籍其实是没有线程这个概念的,更不要提下面的协程这个概念了。理解GMP模型就需要知道什么是协程?协程说的简单一些,就是用户态的线程。更具体的可以看以前写的协程相关的文章(一直向前翻,你就会看到)。协程分有栈和无栈协程两大类,一般协程是绑定到具体的线程上的,而线程是由OS来实际操作的。Golang的协程属于有栈协程。

    一、什么是GMP

    前面解释过GMP,这里再重复一下:
    G: Goroutine 执行的上下文环境。
    M: 操作系统线程。
    P: Processer。进程调度的关键,调度器,也可以认为约等于CPU。
    其实GMP引入的目的就是为了引出并行的这个概念。说并发也无不可,自己明白怎么回事儿就行。golang似乎对这个也不是区分的特别清楚,GMP的目的就是通过一套机制,有效提高对多任务的处理。这也是人们常说的GMP模型。简单回忆一下进程的调度模型,基本上是通过相关队列来管理相关的进程(包括就绪队列、等待队列和工作队列,当然五态和七态模型又有不同),在CPU有空闲时或者时间片轮转到后,就执行就绪队列中的相关任务。同时,随时可能因为某种情况就绪态或者工作状态会转为等待状态;反之,等待状态也可能因为条件的满足而转化了就绪态。明白了这个就基本可以顺延到GMP模型中的GMP之间的关系。
    首先要知道,协程G是要跑在工作线程M上的,这个无论是有、无栈协程都是如此。而P负责G任务队列的调度管理。前面提到过P并不是真实的物理CPU,但一般来说其运行数量和实际的物理CPU数量保持一致(这玩意儿和线程池有点类似啊),P管理着一个G的队列,周期的调度G到M上工作。需要注意的是,go本身除了维持着每个P的G队列还维持一个全局的G队列(系统IO中恢复的G),P会定时查看此队列一旦发现有就绪的G就拿到自己的队列中,防止类似线程饿死的现象发生。
    为了保证线程对CPU的利用,所以M的数量要大于实际的P,这样保证实际运行的M和P保持一致。同时,其也引入了类似线程窃取的功能,这些都和OS中的类似。而通过这个模型又可以引申出Go语言的CSP的并发模型,这就是另外一话题了,有时间再分析。举一个例子分析来看:
    当一个操作系统的线程在执行M1中的G1遇到阻塞的情况时,调度器让M1丢弃P,等待G1返回,然后重新启动一个M2接收P来执行其它的goroutine队列(G2、G3…),当G1结束后,M1会重新拿回P来完成,如果拿不到就放到全局运行队列中,然后自己放到线程池或转入休眠状态。在前面也提到了空闲的P会定时的检查全局队列上的G并执行。
    当然任务窃取也是一种情况。即有些P太闲而其他P很忙的时候,会从其它P上窃取一些G运行。

    二、相关的源码

    首先来看一下,源码中对这三个的类定义:

    type g struct {
    	// Stack parameters.
    	// stack describes the actual stack memory: [stack.lo, stack.hi).
    	// stackguard0 is the stack pointer compared in the Go stack growth prologue.
    	// It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
    	// stackguard1 is the stack pointer compared in the C stack growth prologue.
    	// It is stack.lo+StackGuard on g0 and gsignal stacks.
    	// It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
    	stack       stack   // offset known to runtime/cgo
    	stackguard0 uintptr // offset known to liblink
    	stackguard1 uintptr // offset known to liblink
    
    	_panic         *_panic // innermost panic - offset known to liblink
    	_defer         *_defer // innermost defer
    	m              *m      // current m; offset known to arm liblink
    	stackAlloc     uintptr // stack allocation is [stack.lo,stack.lo+stackAlloc)
    	sched          gobuf
    	syscallsp      uintptr        // if status==Gsyscall, syscallsp = sched.sp to use during gc
    	syscallpc      uintptr        // if status==Gsyscall, syscallpc = sched.pc to use during gc
    	stkbar         []stkbar       // stack barriers, from low to high (see top of mstkbar.go)
    	stkbarPos      uintptr        // index of lowest stack barrier not hit
    	stktopsp       uintptr        // expected sp at top of stack, to check in traceback
    	param          unsafe.Pointer // passed parameter on wakeup
    	atomicstatus   uint32
    	stackLock      uint32 // sigprof/scang lock; TODO: fold in to atomicstatus
    	goid           int64
    	waitsince      int64  // approx time when the g become blocked
    	waitreason     string // if status==Gwaiting
    	schedlink      guintptr
    	preempt        bool     // preemption signal, duplicates stackguard0 = stackpreempt
    	paniconfault   bool     // panic (instead of crash) on unexpected fault address
    	preemptscan    bool     // preempted g does scan for gc
    	gcscandone     bool     // g has scanned stack; protected by _Gscan bit in status
    	gcscanvalid    bool     // false at start of gc cycle, true if G has not run since last scan; transition from true to false by calling queueRescan and false to true by calling dequeueRescan
    	throwsplit     bool     // must not split stack
    	raceignore     int8     // ignore race detection events
    	sysblocktraced bool     // StartTrace has emitted EvGoInSyscall about this goroutine
    	sysexitticks   int64    // cputicks when syscall has returned (for tracing)
    	traceseq       uint64   // trace event sequencer
    	tracelastp     puintptr // last P emitted an event for this goroutine
    	lockedm        *m
    	sig            uint32
    	writebuf       []byte
    	sigcode0       uintptr
    	sigcode1       uintptr
    	sigpc          uintptr
    	gopc           uintptr // pc of go statement that created this goroutine
    	startpc        uintptr // pc of goroutine function
    	racectx        uintptr
    	waiting        *sudog    // sudog structures this g is waiting on (that have a valid elem ptr); in lock order
    	cgoCtxt        []uintptr // cgo traceback context
    
    	// Per-G GC state
    
    	// gcRescan is this G's index in work.rescan.list. If this is
    	// -1, this G is not on the rescan list.
    	//
    	// If gcphase != _GCoff and this G is visible to the garbage
    	// collector, writes to this are protected by work.rescan.lock.
    	gcRescan int32
    
    	// gcAssistBytes is this G's GC assist credit in terms of
    	// bytes allocated. If this is positive, then the G has credit
    	// to allocate gcAssistBytes bytes without assisting. If this
    	// is negative, then the G must correct this by performing
    	// scan work. We track this in bytes to make it fast to update
    	// and check for debt in the malloc hot path. The assist ratio
    	// determines how this corresponds to scan work debt.
    	gcAssistBytes int64
    }
    
    type m struct {
    	g0      *g     // goroutine with scheduling stack
    	morebuf gobuf  // gobuf arg to morestack
    	divmod  uint32 // div/mod denominator for arm - known to liblink
    
    	// Fields not known to debuggers.
    	procid        uint64     // for debuggers, but offset not hard-coded
    	gsignal       *g         // signal-handling g
    	sigmask       sigset     // storage for saved signal mask
    	tls           [6]uintptr // thread-local storage (for x86 extern register)
    	mstartfn      func()
    	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
    	id            int32
    	mallocing     int32
    	throwing      int32
    	preemptoff    string // if != "", keep curg running on this m
    	locks         int32
    	softfloat     int32
    	dying         int32
    	profilehz     int32
    	helpgc        int32
    	spinning      bool // m is out of work and is actively looking for work
    	blocked       bool // m is blocked on a note
    	inwb          bool // m is executing a write barrier
    	newSigstack   bool // minit on C thread called sigaltstack
    	printlock     int8
    	fastrand      uint32
    	ncgocall      uint64      // number of cgo calls in total
    	ncgo          int32       // number of cgo calls currently in progress
    	cgoCallersUse uint32      // if non-zero, cgoCallers in use temporarily
    	cgoCallers    *cgoCallers // cgo traceback if crashing in cgo call
    	park          note
    	alllink       *m // on allm
    	schedlink     muintptr
    	mcache        *mcache
    	lockedg       *g
    	createstack   [32]uintptr // stack that created this thread.
    	freglo        [16]uint32  // d[i] lsb and f[i]
    	freghi        [16]uint32  // d[i] msb and f[i+16]
    	fflag         uint32      // floating point compare flags
    	locked        uint32      // tracking for lockosthread
    	nextwaitm     uintptr     // next m waiting for lock
    	gcstats       gcstats
    	needextram    bool
    	traceback     uint8
    	waitunlockf   unsafe.Pointer // todo go func(*g, unsafe.pointer) bool
    	waitlock      unsafe.Pointer
    	waittraceev   byte
    	waittraceskip int
    	startingtrace bool
    	syscalltick   uint32
    	thread        uintptr // thread handle
    
    	// these are here because they are too large to be on the stack
    	// of low-level NOSPLIT functions.
    	libcall   libcall
    	libcallpc uintptr // for cpu profiler
    	libcallsp uintptr
    	libcallg  guintptr
    	syscall   libcall // stores syscall parameters on windows
    
    	mOS
    }
    
    type p struct {
    	lock mutex
    
    	id          int32
    	status      uint32 // one of pidle/prunning/...
    	link        puintptr
    	schedtick   uint32   // incremented on every scheduler call
    	syscalltick uint32   // incremented on every system call
    	m           muintptr // back-link to associated m (nil if idle)
    	mcache      *mcache
    	racectx     uintptr
    
    	deferpool    [5][]*_defer // pool of available defer structs of different sizes (see panic.go)
    	deferpoolbuf [5][32]*_defer
    
    	// Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.
    	goidcache    uint64
    	goidcacheend uint64
    
    	// Queue of runnable goroutines. Accessed without lock.
    	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
    
    	// Available G's (status == Gdead)
    	gfree    *g
    	gfreecnt int32
    
    	sudogcache []*sudog
    	sudogbuf   [128]*sudog
    
    	tracebuf traceBufPtr
    
    	palloc persistentAlloc // per-P to avoid mutex
    
    	// Per-P GC state
    	gcAssistTime     int64 // Nanoseconds in assistAlloc
    	gcBgMarkWorker   guintptr
    	gcMarkWorkerMode gcMarkWorkerMode
    
    	// gcw is this P's GC work buffer cache. The work buffer is
    	// filled by write barriers, drained by mutator assists, and
    	// disposed on certain GC state transitions.
    	gcw gcWork
    
    	runSafePointFn uint32 // if 1, run sched.safePointFn at next safe point
    
    	pad [sys.CacheLineSize]byte
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198

    其实协程的调度和进程线程没有啥本质不同。上面的三个数据结构都在runtime2.go这个文件中,可以清楚的看到,在G中可以看stack,m以及相关的goid。后面还有一些状态控制,异常控制以及缓冲区等。一个g会运行在一个m上。而在M中,第一个就是g的调度堆栈,curg当前绑定的G,其后还有p和id,而在P中有m,mcache,gfree,sudobuf,这个sudobuf是一个等待队列,所以是一个数组。在最后是不是看到了GC相关的gcw.

    
    
    // sudog represents a g in a wait list, such as for sending/receiving
    // on a channel.
    //
    // sudog is necessary because the g ↔ synchronization object relation
    // is many-to-many. A g can be on many wait lists, so there may be
    // many sudogs for one g; and many gs may be waiting on the same
    // synchronization object, so there may be many sudogs for one object.
    //
    // sudogs are allocated from a special pool. Use acquireSudog and
    // releaseSudog to allocate and free them.
    type sudog struct {
    	// The following fields are protected by the hchan.lock of the
    	// channel this sudog is blocking on. shrinkstack depends on
    	// this.
    
    	g          *g
    	selectdone *uint32 // CAS to 1 to win select race (may point to stack)
    	next       *sudog
    	prev       *sudog
    	elem       unsafe.Pointer // data element (may point to stack)
    
    	// The following fields are never accessed concurrently.
    	// waitlink is only accessed by g.
    
    	acquiretime int64
    	releasetime int64
    	ticket      uint32
    	waitlink    *sudog // g.waiting list
    	c           *hchan // channel
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    这个明显是一个list的结构啊。
    P中还有两个变量schedtick和syscalltick用来作为调度和调用的计数器。看一下P中定义的常量:

    const (
    	// The max value of GOMAXPROCS.
    	// There are no fundamental restrictions on the value.
    	_MaxGomaxprocs = 1 << 8
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这个最大值其实就是CPU的数量(核心的数量),也就代表着同时能运行的最大的G的数量。再看一下gobuf定义:

    type gobuf struct {
    	// The offsets of sp, pc, and g are known to (hard-coded in) libmach.
    	//
    	// ctxt is unusual with respect to GC: it may be a
    	// heap-allocated funcval so write require a write barrier,
    	// but gobuf needs to be cleared from assembly. We take
    	// advantage of the fact that the only path that uses a
    	// non-nil ctxt is morestack. As a result, gogo is the only
    	// place where it may not already be nil, so gogo uses an
    	// explicit write barrier. Everywhere else that resets the
    	// gobuf asserts that ctxt is already nil.
    	sp   uintptr
    	pc   uintptr
    	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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    都不用去看什么注释,你看看它的成员变量定义,都能猜得出这个数据结构是干啥的。除了sp,pc,bp这些寄存器的名字就是ret返回寄存器,其它的也都差不多。所以它的用处必然是在G发生上下文切换时的状态保存用的。
    下面看一下如何创建一个G:

    // Create a new g running fn with siz bytes of arguments.
    // Put it on the queue of g's waiting to run.
    // The compiler turns a go statement into a call to this.
    // Cannot split the stack because it assumes that the arguments
    // are available sequentially after &fn; they would not be
    // copied if a stack split occurred.
    //go:nosplit
    func newproc(siz int32, fn *funcval) {
    	argp := add(unsafe.Pointer(&fn), sys.PtrSize)
    	pc := getcallerpc(unsafe.Pointer(&siz))
    	systemstack(func() {
    		newproc1(fn, (*uint8)(argp), siz, 0, pc)
    	})
    }
    
    // Create a new g running fn with narg bytes of arguments starting
    // at argp and returning nret bytes of results.  callerpc is the
    // address of the go statement that created this. The new g is put
    // on the queue of g's waiting to run.
    func newproc1(fn *funcval, argp *uint8, narg int32, nret int32, callerpc uintptr) *g {
    	_g_ := getg()
    
    	if fn == nil {
    		_g_.m.throwing = -1 // do not dump full stacks
    		throw("go of nil func value")
    	}
    	_g_.m.locks++ // disable preemption because it can be holding p in a local var
    	siz := narg + nret
    	siz = (siz + 7) &^ 7
    
    	// We could allocate a larger initial stack if necessary.
    	// Not worth it: this is almost always an error.
    	// 4*sizeof(uintreg): extra space added below
    	// sizeof(uintreg): caller's LR (arm) or return address (x86, in gostartcall).
    	if siz >= _StackMin-4*sys.RegSize-sys.RegSize {
    		throw("newproc: function arguments too large for new goroutine")
    	}
    
    	_p_ := _g_.m.p.ptr()
    	newg := gfget(_p_)
    	if newg == nil {
    		newg = malg(_StackMin)
    		casgstatus(newg, _Gidle, _Gdead)
    		newg.gcRescan = -1
    		allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
    	}
    	if newg.stack.hi == 0 {
    		throw("newproc1: newg missing stack")
    	}
    
    	if readgstatus(newg) != _Gdead {
    		throw("newproc1: new g is not Gdead")
    	}
    
    	totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frame
    	totalSize += -totalSize & (sys.SpAlign - 1)                  // align to spAlign
    	sp := newg.stack.hi - totalSize
    	spArg := sp
    	if usesLR {
    		// caller's LR
    		*(*uintptr)(unsafe.Pointer(sp)) = 0
    		prepGoExitFrame(sp)
    		spArg += sys.MinFrameSize
    	}
    	if narg > 0 {
    		memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg))
    		// This is a stack-to-stack copy. If write barriers
    		// are enabled and the source stack is grey (the
    		// destination is always black), then perform a
    		// barrier copy. We do this *after* the memmove
    		// because the destination stack may have garbage on
    		// it.
    		if writeBarrier.needed && !_g_.m.curg.gcscandone {
    			f := findfunc(fn.fn)
    			stkmap := (*stackmap)(funcdata(f, _FUNCDATA_ArgsPointerMaps))
    			// We're in the prologue, so it's always stack map index 0.
    			bv := stackmapdata(stkmap, 0)
    			bulkBarrierBitmap(spArg, spArg, uintptr(narg), 0, bv.bytedata)
    		}
    	}
    
    	memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
    	newg.sched.sp = sp
    	newg.stktopsp = sp
    	newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
    	newg.sched.g = guintptr(unsafe.Pointer(newg))
    	gostartcallfn(&newg.sched, fn)
    	newg.gopc = callerpc
    	newg.startpc = fn.fn
    	if isSystemGoroutine(newg) {
    		atomic.Xadd(&sched.ngsys, +1)
    	}
    	// The stack is dirty from the argument frame, so queue it for
    	// scanning. Do this before setting it to runnable so we still
    	// own the G. If we're recycling a G, it may already be on the
    	// rescan list.
    	if newg.gcRescan == -1 {
    		queueRescan(newg)
    	} else {
    		// The recycled G is already on the rescan list. Just
    		// mark the stack dirty.
    		newg.gcscanvalid = false
    	}
    	casgstatus(newg, _Gdead, _Grunnable)
    
    	if _p_.goidcache == _p_.goidcacheend {
    		// Sched.goidgen is the last allocated id,
    		// this batch must be [sched.goidgen+1, sched.goidgen+GoidCacheBatch].
    		// At startup sched.goidgen=0, so main goroutine receives goid=1.
    		_p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch)
    		_p_.goidcache -= _GoidCacheBatch - 1
    		_p_.goidcacheend = _p_.goidcache + _GoidCacheBatch
    	}
    	newg.goid = int64(_p_.goidcache)
    	_p_.goidcache++
    	if raceenabled {
    		newg.racectx = racegostart(callerpc)
    	}
    	if trace.enabled {
    		traceGoCreate(newg, newg.startpc)
    	}
    	runqput(_p_, newg, true)
    
    	if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && runtimeInitTime != 0 {
    		wakep()
    	}
    	_g_.m.locks--
    	if _g_.m.locks == 0 && _g_.preempt { // restore the preemption request in case we've cleared it in newstack
    		_g_.stackguard0 = stackPreempt
    	}
    	return newg
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132

    为了能够监测大量G,GO在pro.go中创建了一系统线程M来处理G并根据实际情况对本身的运行进行时间上的控制,下面看一下创建main函数后,init后并调用sysmon:

    func main() {
    	g := getg()
    
    	// Racectx of m0->g0 is used only as the parent of the main goroutine.
    	// It must not be used for anything else.
    	g.m.g0.racectx = 0
    
    	// Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
    	// Using decimal instead of binary GB and MB because
    	// they look nicer in the stack overflow failure message.
    	if sys.PtrSize == 8 {
    		maxstacksize = 1000000000
    	} else {
    		maxstacksize = 250000000
    	}
    
    	// Record when the world started.
    	runtimeInitTime = nanotime()
    
    	//表示这个函数必须在系统栈上运行
    	systemstack(func() {
    		newm(sysmon, nil)
    	})
    
    	// Lock the main goroutine onto this, the main OS thread,
    	// during initialization. Most programs won't care, but a few
    	// do require certain calls to be made by the main thread.
    	// Those can arrange for main.main to run in the main thread
    	// by calling runtime.LockOSThread during initialization
    	// to preserve the lock.
    	lockOSThread()
    
    	if g.m != &m0 {
    		throw("runtime.main not on m0")
    	}
    
    	runtime_init() // must be before defer
    
    	// Defer unlock so that runtime.Goexit during init does the unlock too.
    	needUnlock := true
    	defer func() {
    		if needUnlock {
    			unlockOSThread()
    		}
    	}()
    
    	gcenable()
    
    	main_init_done = make(chan bool)
    	if iscgo {
    		if _cgo_thread_start == nil {
    			throw("_cgo_thread_start missing")
    		}
    		if GOOS != "windows" {
    			if _cgo_setenv == nil {
    				throw("_cgo_setenv missing")
    			}
    			if _cgo_unsetenv == nil {
    				throw("_cgo_unsetenv missing")
    			}
    		}
    		if _cgo_notify_runtime_init_done == nil {
    			throw("_cgo_notify_runtime_init_done missing")
    		}
    		cgocall(_cgo_notify_runtime_init_done, nil)
    	}
    
    	fn := main_init // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
    	fn()
    	close(main_init_done)
    
    	needUnlock = false
    	unlockOSThread()
    
    	if isarchive || islibrary {
    		// A program compiled with -buildmode=c-archive or c-shared
    		// has a main, but it is not executed.
    		return
    	}
    	fn = main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
    	fn()
    	if raceenabled {
    		racefini()
    	}
    
    	// Make racy client program work: if panicking on
    	// another goroutine at the same time as main returns,
    	// let the other goroutine finish printing the panic trace.
    	// Once it does, it will exit. See issue 3934.
    	if panicking != 0 {
    		gopark(nil, nil, "panicwait", traceEvGoStop, 1)
    	}
    
    	exit(0)
    	for {
    		var x *int32
    		*x = 0
    	}
    }
    
    
    // start forcegc helper goroutine
    func init() {
    	go forcegchelper()
    }
    
    func forcegchelper() {
    	forcegc.g = getg()
    	for {
    		lock(&forcegc.lock)
    		if forcegc.idle != 0 {
    			throw("forcegc: phase error")
    		}
    		atomic.Store(&forcegc.idle, 1)
    		goparkunlock(&forcegc.lock, "force gc (idle)", traceEvGoBlock, 1)
    		// this goroutine is explicitly resumed by sysmon
    		if debug.gctrace > 0 {
    			println("GC forced")
    		}
    		gcStart(gcBackgroundMode, true)
    	}
    }
    // Always runs without a P, so write barriers are not allowed.
    //
    //go:nowritebarrierrec
    func sysmon() {
    	// If a heap span goes unused for 5 minutes after a garbage collection,
    	// we hand it back to the operating system.
    	scavengelimit := int64(5 * 60 * 1e9)
    
    	if debug.scavenge > 0 {
    		// Scavenge-a-lot for testing.
    		forcegcperiod = 10 * 1e6
    		scavengelimit = 20 * 1e6
    	}
    
    	lastscavenge := nanotime()
    	nscavenge := 0
    
    	lasttrace := int64(0)
    	idle := 0 // how many cycles in succession we had not wokeup somebody
    	delay := uint32(0)
    	for {
    		if idle == 0 { // start with 20us sleep...
    			delay = 20
    		} else if idle > 50 { // start doubling the sleep after 1ms...
    			delay *= 2
    		}
    		if delay > 10*1000 { // up to 10ms
    			delay = 10 * 1000
    		}
    		usleep(delay)
    		if debug.schedtrace <= 0 && (sched.gcwaiting != 0 || atomic.Load(&sched.npidle) == uint32(gomaxprocs)) {
    			lock(&sched.lock)
    			if atomic.Load(&sched.gcwaiting) != 0 || atomic.Load(&sched.npidle) == uint32(gomaxprocs) {
    				atomic.Store(&sched.sysmonwait, 1)
    				unlock(&sched.lock)
    				// Make wake-up period small enough
    				// for the sampling to be correct.
    				maxsleep := forcegcperiod / 2
    				if scavengelimit < forcegcperiod {
    					maxsleep = scavengelimit / 2
    				}
    				notetsleep(&sched.sysmonnote, maxsleep)
    				lock(&sched.lock)
    				atomic.Store(&sched.sysmonwait, 0)
    				noteclear(&sched.sysmonnote)
    				idle = 0
    				delay = 20
    			}
    			unlock(&sched.lock)
    		}
    		// poll network if not polled for more than 10ms
    		lastpoll := int64(atomic.Load64(&sched.lastpoll))
    		now := nanotime()
    		unixnow := unixnanotime()
    		if lastpoll != 0 && lastpoll+10*1000*1000 < now {
    			atomic.Cas64(&sched.lastpoll, uint64(lastpoll), uint64(now))
    			gp := netpoll(false) // non-blocking - returns list of goroutines
    			if gp != nil {
    				// Need to decrement number of idle locked M's
    				// (pretending that one more is running) before injectglist.
    				// Otherwise it can lead to the following situation:
    				// injectglist grabs all P's but before it starts M's to run the P's,
    				// another M returns from syscall, finishes running its G,
    				// observes that there is no work to do and no other running M's
    				// and reports deadlock.
    				incidlelocked(-1)
    				injectglist(gp)
    				incidlelocked(1)
    			}
    		}
    		// retake P's blocked in syscalls
    		// and preempt long running G's
    		if retake(now) != 0 {
    			idle = 0
    		} else {
    			idle++
    		}
    		// check if we need to force a GC
    		lastgc := int64(atomic.Load64(&memstats.last_gc))
    		if gcphase == _GCoff && lastgc != 0 && unixnow-lastgc > forcegcperiod && atomic.Load(&forcegc.idle) != 0 {
    			lock(&forcegc.lock)
    			forcegc.idle = 0
    			forcegc.g.schedlink = 0
    			injectglist(forcegc.g)
    			unlock(&forcegc.lock)
    		}
    		// scavenge heap once in a while
    		if lastscavenge+scavengelimit/2 < now {
    			mheap_.scavenge(int32(nscavenge), uint64(now), uint64(scavengelimit))
    			lastscavenge = now
    			nscavenge++
    		}
    		if debug.schedtrace > 0 && lasttrace+int64(debug.schedtrace)*1000000 <= now {
    			lasttrace = now
    			schedtrace(debug.scheddetail > 0)
    		}
    	}
    }
    
    var pdesc [_MaxGomaxprocs]struct {
    	schedtick   uint32
    	schedwhen   int64
    	syscalltick uint32
    	syscallwhen int64
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227

    分析代码是个系统工程,有些代码还真得需要反复的看上下文的代码。不过看代码确实是痛苦,主要是得不断适应开发者的思维和思想以及设计的思路。不过话又说回来,见多才能识广。诸君共勉!

    三、总结

    每个语言都有自己独特的地方,本来这种任务调度模型一般来说都是类似的。但golang搞了一个协程出来,使得原来基于线程(进程)的相关OS调度模型不能够较好的描述这个运行机制。所以才搞出来这个GMP模型,其实就是打通了任务调度和实际的协程与处理器以及线程的具体的逻辑关系和调度方式。
    模型是抽象描述,代码是具体的实现,明白了模型,对着代码,好好学习一下,自然就什么都明白了。“源码之前,了无秘密”吧。

  • 相关阅读:
    古瑞瓦特冲刺港交所上市:创下“多个第一”,获IDG资本9亿元投资
    【C++ 初阶】运算符重载详解✌
    CSS 创建
    你可曾知道,Java为什么需要虚拟机?
    Flask 学习-59.解决celery 在windows 上接收任务不执行的问题
    运营业务指标
    一篇五分生信临床模型预测文章代码复现——Figure 2. 生存分析,箱线图表达改变分析(一)
    采集Prestashop独立站
    Linux UWB Stack实现——MCPS调度接口(数据结构)
    【PyTorch教程】03-张量运算详细总结
  • 原文地址:https://blog.csdn.net/fpcc/article/details/126036654