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:

  1. 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;

  2. Execute the initialization of the runtime package;

  3. Execute the initialization of the main package and all packages imported by the main package;

  4. Execute main.main function;

  5. After returning from the main.main function, call the exit system call to exit the process;