Inside Go's Main Goroutine Scheduler (2)
9. Enter runtime/proc.go and scheduler initialization
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 |
func schedinit() {
// raceinit must be the first call to race detector.
// In particular, it must be done before mallocinit below calls racemapshadow.
// The getg function is implemented by the compiler, similar to executing the following two lines of code
// get_tls(CX)
// MOVQ g(cx), BX; BX register = tls[0] = address of the current g structure object
_g_ := getg() // During scheduler initialization, _g_ = &g0
if raceenabled {
_g_.racectx, raceprocctx0 = raceinit()
}
sched.maxmcount = 10000 // A maximum of 10,000 operating system threads can be started, and a maximum of 10,000 M
tracebackinit()
moduledataverify()
//Memory related initialization
stackinit()
mallocinit()
// M related initialization
mcommoninit(_g_.m)
cpuinit() // must run before alginit
alginit() // maps must not be used before this call
modulesinit() // provides activeModules
typelinksinit() // uses maps, activeModules
itabsinit() // uses activeModules
msigsave(_g_.m)
initSigmask = _g_.m.sigmask
// Store command line parameters and environment variables
goargs()
goenvs()
// Parse the debugging parameters of GODEBUG
parsedebugvars()
//Initialize the garbage collector
gcinit()
//Initialize poll time
sched.lastpoll = uint64(nanotime())
//Set GOMAXPROCS
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}
// For cgocheck > 1, we turn on the write barrier at all times
// and check all pointer writes. We can't do this until after
// procresize because the write barrier needs a P.
if debug.cgocheck > 1 {
writeBarrier.cgo = true
writeBarrier.enabled = true
for _, p := range allp {
p.wbBuf.reset()
}
}
if buildVersion == "" {
// Condition should never trigger. This code just serves
// to ensure runtime·buildVersion is kept in the resulting binary.
buildVersion = "unknown"
}
} |
The following is excerpted from Go Language Insider (6): Startup and Memory Allocation Initialization
runtime.tracebackinit runtime.tracebackinit is responsible for initializing traceback. traceback is a function stack. These functions will be called before we reach the current execution point. For example, we can see them every time a panic occurs. Traceback is generated by calling the runtime.gentraceback function. For this function to work, we need to know the addresses of some built-in functions (for example, because we don’t want them to be included in the traceback). runtime.traceback is responsible for initializing these addresses.
runtime.moduledataverify Linker symbols are data generated by the linker and output to the executable object file. Much of this data has been discussed in Go Language Insider (3): Linkers, Linkers, Relocations. In the runtime package, linker symbols are mapped to moduledata structures. The runtime.moduledataverify function is responsible for checking this data to ensure the correctness of all structures.
runtime.stackinit
1 2 3 4 5 6 7 8 9 10 11 |
func stackinit() {
if _StackCacheSize&_PageMask != 0 {
throw("cache size must be a multiple of page size")
}
for i := range stackpool {
stackpool[i].init()
}
for i := range stackLarge.free {
stackLarge.free[i].init()
}
} |
How to implement stack growth in Go: When a new goroutine is generated, the system will allocate a stack of 2k-8k size for it. When the stack reaches a certain threshold, the size of the stack is doubled and all data in the original stack is copied to the new stack.
There are many details, such as how to judge whether the threshold is reached, how Go adjusts the pointer in the stack, etc. When introducing stackguard0 and function metadata in the previous blog, I have already introduced some relevant content. For more information, you can refer to this document
When the runtime scheduler is initialized, the stackpool array is initialized through the runtime.stackinit function. Each item in this array is a linked list containing a stack of the same size.
This step also initializes another variable stackLarge.free. If the allocated content is a large object (size > 32k), it will be allocated directly from stackLarge.
Initialization stack space size and stack pool size in different operating systems:
| OS | FixedStack | NumStackOrders |
|---|---|---|
| linux/darwin/bsd | 2KB | 4 |
| windows/32 | 4KB | 3 |
| windows/64 | 8KB | 2 |
| plan9 | 4KB | 2 |
runtime.mallocinit
1 2 3 4 5 6 7 8 9 |
func mallocinit() {
// error checking
...
// 1. Initialize the heap content space mheap, mcentral, and mcache
// Initialize the heap.
mheap_.init() 、
_g_ := getg()
_g_.m.mcache = allocmcache()
} |
mheap structure
˜Represents all the heap memory held by the Go program. The Go program uses a global object _mheap of mheap to manage the heap memory.
1 2 3 4 5 6 7 8 9 10 11 12 |
type mheap struct {
lock mutex
free mTreap // Free span collection (tree structure)
allspans []*mspan // all spans
// mcentral memory allocation center, when mcache does not have enough memory allocation, it will allocate from mcentral
central [numSpanClasses]struct {
mcentral mcentral
pad [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
}
spanalloc fixalloc // span allocator
cachealloc fixalloc // mcache allocator
} |
For more detail on Go memory allocation, see these articles:
In-depth understanding of Go-memory allocation: https://www.tuicool.com/articles/2uAZBrM
Illustration of Golang’s memory allocation: https://i6448038.github.io/2019/05/18/golang-mem/
runtime.mcommoninit Initialize m0. After m0 completes the basic initialization, put m0 into the global linked list allm.
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 |
func mcommoninit(mp *m) {
_g_ := getg() // During scheduler initialization process _g_ = g0
// g0 stack won't make sense for user (and is not necessary unwindable).
if _g_ != _g_.m.g0 { //Function call stack traceback, no need to care
callers(1, mp.createstack[:])
}
lock(&sched.lock)
if sched.mnext+1 < sched.mnext {
throw("runtime: thread ID overflow")
}
mp.id = sched.mnext
sched.mnext++
checkmcount() //Check whether the number of created system threads exceeds the limit (10000)
mp.fastrand[0] = 1597334677 * uint32(mp.id)
mp.fastrand[1] = uint32(cputicks())
if mp.fastrand[0]|mp.fastrand[1] == 0 {
mp.fastrand[1] = 1
}
//Creating gsignal for signal processing simply allocates a g structure object from the heap, then sets the stack and returns
mpreinit(mp)
if mp.gsignal != nil {
mp.gsignal.stackguard1 = mp.gsignal.stack.lo + _StackGuard
}
//Hang m into the global linked list allm
mp.alllink = allm
// NumCgoCall() iterates over allm w/o schedlock,
// so we need to publish it safely.
atomicstorep(unsafe.Pointer(&allm), unsafe.Pointer(mp))
unlock(&sched.lock)
// Allocate memory to hold a cgo traceback if the cgo call crashes.
if iscgo || GOOS == "solaris" || GOOS == "illumos" || GOOS == "windows" {
mp.cgoCallers = new(cgoCallers)
}
} |
runtime.procresize This function code is relatively long, but not complicated. Here is a summary of the main process of this function: 1. Use make([]*p, nprocs) to initialize the global variable allp, that is, allp = make([]*p, nprocs) 2. Create and initialize nprocs p structure objects in a loop and save them in the allp slice in turn 3. Bind m0 and allp[0] together, that is, m0.p = allp[0], allp[0].m = m0 4. Put all p except allp[0] into the piddle idle queue of the global variable sched
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 |
func procresize(nprocsint32) *p {
old := gomaxprocs//When the system is initialized, gomaxprocs = 0
......
// Grow allp if necessary.
if nprocs > int32(len(allp)) { //Initialization len(allp) == 0
// Synchronize with retake, which could be running
// concurrently since it doesn't run on a P.
lock(&allpLock)
if nprocs <= int32(cap(allp)) {
allp = allp[:nprocs]
} else { //Enter this branch during initialization and create allp slice
nallp:=make([]*p, nprocs)
// Copy everything up to allp's cap so we
// never lose old allocated Ps.
copy(nallp, allp[:cap(allp)])
allp=nallp
}
unlock(&allpLock)
}
// initialize new P's
//Loop to create nprocs p and complete basic initialization
for i := int32(0); i<nprocs; i++{
pp := allp[i]
if pp == nil{
pp=new(p)//Call the memory allocator to allocate a struct p from the heap
pp.id=i
pp.status=_Pgcstop
......
atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
}
......
}
......
_g_:=getg() // _g_ = g0
if _g_.m.p != 0 && _g_.m.p.ptr().id < nprocs {//When initializing, m0->p has not been initialized yet, so this branch will not be executed.
// continue to use the current P
_g_.m.p.ptr().status=_Prunning
_g_.m.p.ptr().mcache.prepareForSweep()
} else {//Execute this branch during initialization
// release the current P and acquire allp[0]
if _g_.m.p != 0 {//This is not executed during initialization
_g_.m.p.ptr().m=0
}
_g_.m.p=0
_g_.m.mcache = nil
p := allp[0]
p.m = 0
p.status = _Pidle
acquirep(p) //Associating p with m0 is actually assigning values to each other between the members of these two strcts.
if trace.enabled {
traceGoStart()
}
}
//The following for loop puts all free p's into the free linked list
var runnablePs *p
for i := nprocs-1; i >= 0; i-- {
p := allp[i]
if _g_.m.p.ptr() == p {//allp[0] is associated with m0, so it cannot be left alone
continue
}
p.status = _Pidle
if runqempty(p) {//During initialization, except allp[0], all p will execute this branch and put it into the free linked list
pidleput(p)
} else {
......
}
}
......
return runnablePs
} |
At this point, the scheduler initialization is completed, m0 and allp are created. The next step is to create a new goroutine to execute the runtime·main function corresponding to mainPC.
10. Create main goroutine
Continue to return to ams_amd64.s file runtime·rt0_go down
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// create a new goroutine to start program
MOVQ $runtime·mainPC(SB), AX // entry runtime.main function address
// The second parameter of newproc is pushed onto the stack, which is the function that the new goroutine needs to execute.
PUSHQ AX // AX = &funcval{runtime·main}
// The first parameter of newproc is pushed onto the stack. This parameter indicates the parameter size required by the runtime.main function. Because runtime.main has no parameters, it is 0 here.
PUSHQ $0 // arg size
CALL runtime·newproc(SB) // proc.go creates main goroutine
POPQ AX
POPQ AX
// start this M
CALL runtime·mstart(SB) //The main thread enters the scheduling loop and runs the goroutine just created
// The above mstart should never return. If it does, there must be a problem with the code logic. Just abort it.
CALL runtime·abort(SB) // mstart should never return
RET
// Prevent dead-code elimination of debugCallV1, which is
// intended to be called by debuggers.
MOVQ $runtime·debugCallV1(SB), AX
RET |
runtime.newproc
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 |
func newproc(siz int32, fn *funcval) {
// The order in which function call parameters are pushed onto the stack is from right to left, and the stack grows from high address to low address. Compilers that support variable parameters are pushed into the stack from right to left.
// Note: argp points to the first parameter of the fn function, not the parameter of the newproc function
// Function fn stores the first parameter of the fn function at address +8 on the stack, because the parameters of the called function are placed on the stack of the calling function.
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
gp := getg() // Get the running g, which is m0.g0 during initialization
//The pc register stores the address of the next instruction to be executed.
// getcallerpc() returns an address, which is the return address of the function pushed on the stack by the call instruction when calling newproc.
// For the asm_amd64.s runtime·rt0_go function scenario, pc is the address of the POPQ AX instruction behind the CALLruntime·newproc(SB) instruction.
pc := getcallerpc()
// systemstack asm_amd64.s runtime·systemstack(SB) switches back to the g0 stack to execute the function as a parameter, and then switches to curg
systemstack(func() {
newproc1(fn, (*uint8)(argp), siz, gp, pc)
})
}
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
// Because it has been switched to the g0 stack, no matter what scenario there is _g_ = g0. Of course, this g0 refers to the g0 of the current working thread.
_g_ := getg()
if fn == nil {
_g_.m.throwing = -1 // do not dump full stacks
throw("go of nil func value")
}
acquirem() // disable preemption because it can be holding p in a local var
siz := narg
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")
}
// During initialization, _p_ = g0.m.p. From the previous analysis, we can know that it is actually allp[0]
_p_ := _g_.m.p.ptr()
// Get an unused g from the local cache of p. There is no one during initialization. Return nil.
newg := gfget(_p_)
if newg == nil {
//New a g structure object, then allocate the stack for it from the heap, and set the stack member and two stackgard members of g
newg = malg(_StackMin)
//Initialize the state of g to _Gdead
casgstatus(newg, _Gidle, _Gdead)
//Put in the global variable allgs and update allglen
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")
}
//Adjust the stack top pin of g, no need to pay attention
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 {
//Copy parameters from the stack where the newproc function is executed (g0 stack during initialization) to the stack of new g
// Starting from the argp address, copy nrag bytes to spArg
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))
if stkmap.nbit > 0 {
// We're in the prologue, so it's always stack map index 0.
bv := stackmapdata(stkmap, 0)
bulkBarrierBitmap(spArg, spArg, uintptr(bv.n)*sys.PtrSize, 0, bv.bytedata)
}
}
}
//Set all members of the newg.sched structure member to 0
memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
//Set the sched member of newg. The scheduler needs to rely on these fields to schedule goroutine to run on the CPU.
newg.sched.sp = sp // top of stack
newg.stktopsp = sp
// newg.sched.pc means that when newg is scheduled to run, instructions will be executed from this address.
//Set pc to the position of the goexit function offset 1 (sys.PCQuantum is equal to 1),
// As for why we need to do this, we will not know until we analyze the gostartcallfn function.
// Normally the next instruction of this gorutine should be the fn function
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.ancestors = saveAncestors(callergp)
//Set newg's startpc to fn.fn. This member is mainly used for traceback and stack shrinkage of the function call stack.
// Where newg actually starts execution does not depend on this member, but sched.pc
newg.startpc = fn.fn
if _g_.m.curg != nil {
newg.labels = _g_.m.curg.labels
}
if isSystemGoroutine(newg, false) {
atomic.Xadd(&sched.ngsys, +1)
}
newg.gcscanvalid = false
//Set the status of g to _Grunnable, indicating that the goroutine represented by g can run.
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)
}
//Put newg into the run queue of _p_. It must be the local run queue of p during initialization. At other times, it may be put into the global queue because the local queue is full.
runqput(_p_, newg, true)
if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {
wakep()
}
releasem(_g_.m)
} |
This part of the logic is difficult to understand. For details, please refer to the article https://www.cnblogs.com/abozhang/p/10825342.html
runtime·mstart
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 |
func mstart() {
_g_ := getg() //_g_ = g0
//For the startup process, g0's stack.lo has already been initialized, so onStack = false
osStack := _g_.stack.lo == 0
if osStack {
// Initialize stack bounds from system stack.
// Cgo may have left stack size in stack.hi.
// minit may update the stack bounds.
size := _g_.stack.hi
if size == 0 {
size = 8192 * sys.StackGuardMultiplier
}
_g_.stack.hi = uintptr(noescape(unsafe.Pointer(&size)))
_g_.stack.lo = _g_.stack.hi - size + 1024
}
// Initialize stack guards so that we can start calling
// both Go and C functions with stack growth prologues.
_g_.stackguard0 = _g_.stack.lo + _StackGuard
_g_.stackguard1 = _g_.stackguard0
mstart1()
// Exit this thread.
if GOOS == "windows" || GOOS == "solaris" || GOOS == "plan9" || GOOS == "darwin" || GOOS == "aix" {
// Window, Solaris, Darwin, AIX and Plan 9 always system-allocate
// the stack, but put it in _g_.stack before mstart,
// so the logic above hasn't set osStack yet.
osStack = true
}
mexit(osStack)
} |
The mstart function itself has nothing to say, it continues to call the mstart1 function.
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 |
func mstart1() {
_g_ := getg() //When starting the process _g_ = g0 of m0
if _g_ != _g_.m.g0 {
throw("bad runtime·mstart")
}
// Record the caller for use as the top of stack in mcall and
// for terminating the thread.
// We're never coming back to mstart1 after we call schedule,
// so other calls can reuse the current frame.
//getcallerpc() gets the return address after mstart1 is executed
//getcallersp() gets the top address of the stack when calling mstart1
save(getcallerpc(), getcallersp())
asminit() //In the AMD64 Linux platform, this function does nothing and is an empty function.
minit() //Initialization related to signals, no need to care about it currently
// Install signal handlers; after minit so that minit can
// prepare the thread to be able to handle the signals.
if _g_.m == &m0 { //When starting, _g_.m is m0, so the following mstartm0 function will be executed.
mstartm0() //It is also signal-related initialization, we are not concerned about it now
}
if fn := _g_.m.mstartfn; fn != nil { //fn == nil during initialization
fn()
}
if _g_.m != &m0 {// m0 has been bound to allp[0], if it is not m0, there is no p, so you need to get a p
acquirep(_g_.m.nextp.ptr())
_g_.m.nextp = 0
}
//schedule function never returns
schedule()
} |
mstart1 first calls save to record g0’s scheduling information. This line is one of the keys to understanding the scheduling loop. getcallerpc() returns the address pushed onto the stack by the call instruction when mstart invokes mstart1, while getcallersp() returns the top-of-stack address in mstart before that call. The next step is to examine what save records.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// save updates getg().sched to refer to pc and sp so that a following
// gogo will restore pc and sp.
//
// save must not have write barriers because invoking a write barrier
// can clobber getg().sched.
//
//go:nosplit
//go:nowritebarrierrec
func save(pc, sp uintptr) {
_g_ := getg()
_g_.sched.pc = pc //Instruction address when running again
_g_.sched.sp = sp //Go to the top of the stack when running again
_g_.sched.lr = 0
_g_.sched.ret = 0
_g_.sched.g = guintptr(unsafe.Pointer(_g_))
// We need to ensure ctxt is zero, but can't have a write
// barrier here. However, it should always already be zero.
// Assert that.
if _g_.sched.ctxt != nil {
badctxt()
}
} |
Continue to analyze the code. After the execution of the save function is completed, return to mstart1 to continue some other initializations related to m. After completing these initializations, the core function schedule() of the scheduling system is called to complete the scheduling of goroutine. The reason why it is said to be the core is that every time goroutine is scheduled, it starts from the schedule function.
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 |
// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.
func schedule() {
_g_ := getg() //_g_ = m.g0
if _g_.m.locks != 0 {
throw("schedule: holding locks")
}
if _g_.m.lockedg != 0 {
stoplockedm()
execute(_g_.m.lockedg.ptr(), false) // Never returns.
}
// We should not schedule away from a g that is executing a cgo call,
// since the cgo call is using the m's g0 stack.
if _g_.m.incgo {
throw("schedule: in cgo")
}
top:
if sched.gcwaiting != 0 {
gcstopm()
goto top
}
if _g_.m.p.ptr().runSafePointFn != 0 {
runSafePointFn()
}
var gp *g
var inheritTime bool
// Normal goroutines will check for need to wakeP in ready,
// but GCworkers and tracereaders will not, so the check must
// be done here instead.
tryWakeP := false
if trace.enabled || trace.shutdown {
gp = traceReader()
if gp != nil {
casgstatus(gp, _Gwaiting, _Grunnable)
traceGoUnpark(gp, 0)
tryWakeP = true
}
}
if gp == nil && gcBlackenEnabled != 0 {
gp = gcController.findRunnableGCWorker(_g_.m.p.ptr())
tryWakeP = tryWakeP || gp != nil
}
if gp == nil {
// Check the global runnable queue once in a while to ensure fairness.
// Otherwise two goroutines can completely occupy the local runqueue
// by constantly respawning each other.
// In order to ensure the fairness of scheduling, each worker thread needs to first obtain the goroutine from the global run queue and run it every 61 times of scheduling.
// Because if only the goroutine in the local run queue is scheduled, the goroutine in the global run queue may not be run.
if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock) // All worker threads can access the global run queue, so locks are required
gp = globrunqget(_g_.m.p.ptr(), 1) // Get 1 goroutine from the global run queue
unlock(&sched.lock)
}
}
if gp == nil {
// Get the goroutine from the local run queue of p associated with m
gp, inheritTime = runqget(_g_.m.p.ptr())
if gp != nil && _g_.m.spinning {
throw("schedule: spinning with local work")
}
}
if gp == nil {
// If the goroutine that needs to be run is not found from the local run queue and the global run queue,
// Then call the findrunnable function to steal from the run queue of other worker threads. If it cannot be stolen, the current worker thread goes to sleep.
// The findrunnable function will not return until the goroutine that needs to be run is obtained.
gp, inheritTime = findrunnable() // blocks until work is available
}
// This thread is going to run a goroutine and is not spinning anymore,
// so if it was marked as spinning we need to reset it now and potentially
// start a new spinning M.
if _g_.m.spinning {
resetspinning()
}
if sched.disable.user && !schedEnabled(gp) {
// Scheduling of this goroutine is disabled. Put it on
// the list of pending runnable goroutines for when we
// re-enable user scheduling and look again.
lock(&sched.lock)
if schedEnabled(gp) {
// Something re-enabled scheduling while we
// were acquiring the lock.
unlock(&sched.lock)
} else {
sched.disable.runnable.pushBack(gp)
sched.disable.n++
unlock(&sched.lock)
goto top
}
}
// If about to schedule a not-normal goroutine (a GCworker or tracereader),
// wake a P if there is one.
if tryWakeP {
if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 {
wakep()
}
}
if gp.lockedm != 0 {
// Hands off own p to the locked m,
// then blocks waiting for a new p.
startlockedm(gp)
goto top
}
// The runtime code is currently running, and the function call stack uses the stack space of g0
//Call execte to switch to gp's code and stack space for running
execute(gp, inheritTime)
}
// Schedules gp to run on the current M.
// If inheritTime is true, gp inherits the remaining time in the
// current time slice. Otherwise, it starts a new time slice.
// Never returns.
//
// Write barriers are allowed because this is called immediately after
// acquiring a P in several places.
//
//go:yeswritebarrierrec
func execute(gp *g, inheritTime bool) {
_g_ := getg() //g0
//Set the status of g to be run to _Grunning
casgstatus(gp, _Grunnable, _Grunning)
//......
//Associate g and m
_g_.m.curg = gp
gp.m = _g_.m
//......
//gogo completes the real switch from g0 to gp
gogo(&gp.sched)
} |
The first parameter gp of the execute function is the goroutine that needs to be scheduled to run. Here, first change the status of gp from _Grunnable to _Grunning, and then associate gp with m, so that through m you can find which goroutine the current working thread is executing, and vice versa.
After completing the preparations before running gp, execute calls the gogo function to complete the switch from g0 to gp: the transfer of CPU execution rights and the switch of the stack.
The gogo function is also written in assembly language. The reason why assembly is needed here is because the scheduling of goroutine involves switching between different execution streams. We have seen it before when discussing the operating system switching threads. The switching of execution streams is essentially the switching of CPU registers and function call stacks. However, neither high-level languages such as go nor c can accurately control the modification of CPU registers. Therefore, high-level languages are powerless here and can only rely on assembly instructions to achieve the purpose.
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 |
# func gogo(buf *gobuf)
# restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $16-8
#buf = &gp.sched
MOVQ buf+0(FP), BX # BX = buf
#gobuf->g --> dx register
MOVQ gobuf_g(BX), DX # DX = gp.sched.g
#The following line of code has no real effect. Check whether gp.sched.g is nil. If it is nil, the process will crash and die.
MOVQ 0(DX), CX # make sure g != nil
get_tls(CX)
#Put the pointer of g to be run into thread local storage, so that subsequent code can pass through thread local storage
#Get the g structure object of the currently executing goroutine to find the m and p associated with it
MOVQ DX, g(CX)
#Set the CPU's SP register to sched.sp to complete the stack switching
MOVQ gobuf_sp(BX), SP # restore SP
#The following three items also restore the scheduling context to the CPU related registers
MOVQ gobuf_ret(BX), AX
MOVQ gobuf_ctxt(BX), DX
MOVQ gobuf_bp(BX), BP
#Clear the value of sched, because we have put the relevant value into the corresponding register of the CPU and it is no longer needed. This can reduce the workload of gc.
MOVQ $0, gobuf_sp(BX) # clear to help garbage collector
MOVQ $0, gobuf_ret(BX)
MOVQ $0, gobuf_ctxt(BX)
MOVQ $0, gobuf_bp(BX)
#Put the sched.pc value into the BX register
MOVQ gobuf_pc(BX), BX
#JMP puts the address value contained in the BX register into the IP register of the CPU. Then, the CPU jumps to the address and continues to execute the instruction.
JMP BX |
Now we have switched from g0 to the gp goroutine. For our scenario, gp is scheduled to run for the first time. Its entry function is runtime.main, so then the CPU starts executing the runtime.main function:
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 |
// The main goroutine. func main() { g := getg() // g = main goroutine, no longer g0 // ...... // 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 { //The stack of each goroutine on a 64-bit system can reach a maximum of 1G maxstacksize = 1000000000 } else { maxstacksize = 250000000 } // Allow newproc to start new Ms. mainStarted = true if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon //Now the main goroutine is executed, so the main goroutine stack is used. You need to switch to the g0 stack to execute newm() systemstack(func() { //Create a monitoring thread. This thread is independent of the scheduler and does not need to be associated with p to run. newm(sysmon, nil) }) } //...... //Call the initialization function of the runtime package, implemented by the compiler runtime_init() // must be before defer // Record when the world started. runtimeInitTime = nanotime() gcenable() //Enable garbage collector //...... //The initialization function of the main package is also implemented by the compiler and will recursively call the initialization function of the package we import. 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() //...... //Call main.main function 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() //...... //Enter the system call and exit the process. It can be seen that the main goroutine did not return, but directly entered the system call to exit the process. exit(0) //Protective code. If exit returns unexpectedly, the following code will also cause the process to crash and die. for { var x *int32 *x = 0 } } |
The main workflow of the runtime.main function is as follows:
Start a sysmon system monitoring thread. This thread is responsible for monitoring the gc, preemption scheduling, netpoll and other functions of the entire program. In the chapter on preemption scheduling, we will continue to analyze how sysmon assists in completing the preemption scheduling of goroutine;
Execute the initialization of the runtime package;
Execute the initialization of the main package and all packages imported by the main package;
Execute main.main function;
After returning from the main.main function, call the exit system call to exit the process;
Author zhengxz
LastMod 2019-11-13