go的启动流程

361 阅读8分钟

go版本: 1.17 使用goland以及gdb

1.找到程序入口

image.png

1.1先build出可执行程序

go build main.go

1.2.用gdb找到入口

sudo gdb main (我自己是macos,不加sudo的话,gdb会卡死)

 $  sudo gdb main
Password:
GNU gdb (GDB) 11.1
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-apple-darwin20.4.0".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from main...
(No debugging symbols found in main)
Loading Go Runtime support.
(gdb) info files
Symbols from "goroutine/main".
Local exec file:
	`goroutine/main', file type mach-o-x86-64.
	Entry point: 0x105c660
	0x0000000001001000 - 0x000000000105e270 is .text
	0x000000000105e280 - 0x000000000105e35e is __TEXT.__symbol_stub1
	0x000000000105e360 - 0x000000000108b0eb is __TEXT.__rodata
	0x000000000108b100 - 0x000000000108b59c is __TEXT.__typelink
	0x000000000108b5a0 - 0x000000000108b5a8 is __TEXT.__itablink
	0x000000000108b5a8 - 0x000000000108b5a8 is __TEXT.__gosymtab
	0x000000000108b5c0 - 0x00000000010c7658 is __TEXT.__gopclntab
	0x00000000010c8000 - 0x00000000010c8020 is __DATA.__go_buildinfo
	0x00000000010c8020 - 0x00000000010c8148 is __DATA.__nl_symbol_ptr
	0x00000000010c8160 - 0x00000000010c9360 is __DATA.__noptrdata
	0x00000000010c9360 - 0x00000000010cb150 is .data
	0x00000000010cb160 - 0x00000000010f8470 is .bss
	0x00000000010f8480 - 0x00000000010fd570 is __DATA.__noptrbss

找到了入口点为0x105c660,那么在这下断点

(gdb) b *0x105c660
Breakpoint 1 at 0x105c660
(gdb) r
Starting program: goroutine/main
[New Thread 0x1f03 of process 10002]
[New Thread 0x2303 of process 10002]
warning: unhandled dyld version (17)

Thread 2 hit Breakpoint 1, 0x000000000105c660 in _rt0_amd64_darwin ()
(gdb)

找到了入口函数为rt0_amd64_darwin

2.分析汇编代码启动流程

2.1 runtime._rt0_amd64_darwin

TEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8
   JMP    _rt0_amd64(SB)

2.2 runtime._rt0_amd64

// _rt0_amd64 is common startup code for most amd64 systems when using
// internal linking. This is the entry point for the program from the
// kernel for an ordinary -buildmode=exe program. The stack holds the
// number of arguments and the C-style argv.
TEXT _rt0_amd64(SB),NOSPLIT,$-8
   MOVQ   0(SP), DI  // argc
   LEAQ   8(SP), SI  // argv
   JMP    runtime·rt0_go(SB)

// main is common startup code for most amd64 systems when using
// external linking. The C startup code will call the symbol "main"
// passing argc and argv in the usual C ABI registers DI and SI.
TEXT main(SB),NOSPLIT,$-8
   JMP    runtime·rt0_go(SB)

2.3 runtime·rt0_go

TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
   // 前面看不懂,就忽略了
   ...
   // 这里注释的很明显了,把g0和m0关联起来(这里的g0和m0是全局变量)
   LEAQ   runtime·g0(SB), CX
   MOVQ   CX, g(BX)
   LEAQ   runtime·m0(SB), AX

   // save m->g0 = g0
   MOVQ   CX, m_g0(AX)
   // save m0 to g0->m
   MOVQ   AX, g_m(CX)

   CLD             // convention is D is always left cleared
   CALL   runtime·check(SB)

   MOVL   16(SP), AX    // copy argc
   MOVL   AX, 0(SP)
   MOVQ   24(SP), AX    // copy argv
   MOVQ   AX, 8(SP)
   CALL   runtime·args(SB)
   CALL   runtime·osinit(SB)
   CALL   runtime·schedinit(SB)

   // create a new goroutine to start program
   MOVQ   $runtime·mainPC(SB), AX       // entry  这里调用runtime.main函数
   PUSHQ  AX
   PUSHQ  $0       // arg size
   CALL   runtime·newproc(SB)
   POPQ   AX
   POPQ   AX

   // start this M
   CALL   runtime·mstart(SB)

   CALL   runtime·abort(SB)  // mstart should never return
   RET

   // Prevent dead-code elimination of debugCallV2, which is
   // intended to be called by debuggers.
   MOVQ   $runtime·debugCallV2<ABIInternal>(SB), AX
   RET

2.4 runtime·check

运行时类型检查

func check() {
   var (
      a     int8
      b     uint8
      (...)
   )
   (...)
   // 检查size int8是否为1
   if unsafe.Sizeof(a) != 1 {
      throw("bad a")
   }
   if unsafe.Sizeof(b) != 1 {
      throw("bad b")
   }
   (...)

2.5 runtime·args

处理argcargv

func args(c int32, v **byte) {
   argc = c
   argv = v
   sysargs(c, v)
}

2.6 runtime·osinit

获取cpu核心数

// BSD interface for threading.
func osinit() {
   // pthread_create delayed until end of goenvs so that we
   // can look at the environment first.

   ncpu = getncpu()
   physPageSize = getPageSize()
}

3.核心启动流程

接下来就到了核心的地方了

   // 初始化调度器
   CALL   runtime·schedinit(SB)

   // 创建一个goroutine来运行程序
   // create a new goroutine to start program
   MOVQ   $runtime·mainPC(SB), AX       // entry  这里调用runtime.main函数
   PUSHQ  AX
   PUSHQ  $0       // arg size
   // 启动M,永远不会返回
   CALL   runtime·newproc(SB)
   POPQ   AX
   POPQ   AX

   // start this M
   CALL   runtime·mstart(SB)

3.1 func schedinit

调度器启动

// The bootstrap sequence is:
//
// call osinit
// call schedinit
// make & queue new G
// call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {

   ...

   // raceinit must be the first call to race detector.
   // In particular, it must be done before mallocinit below calls racemapshadow.
   _g_ := getg()

   sched.maxmcount = 10000

   ...
   
   mcommoninit(_g_.m, -1)
   
   ...
   
   lock(&sched.lock)
   sched.lastpoll = uint64(nanotime())
   procs := ncpu
   if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
      procs = n
   }
   if procresize(procs) != nil {
      throw("unknown runnable goroutine during bootstrap")
   }
   ...
}

3.1.1 M初始化

func mcommoninit(mp *m, id int64) {
   _g_ := getg()

   // g0 stack won't make sense for user (and is not necessary unwindable).
   if _g_ != _g_.m.g0 {
      callers(1, mp.createstack[:])
   }

   lock(&sched.lock)

   if id >= 0 {
      mp.id = id
   } else {
      // 
      mp.id = mReserveID()
   }

   (...)
   
   // 添加到allm中
   // Add to allm so garbage collector doesn't free g->m
   // when it is just in a register or thread-local storage.
   mp.alllink = allm

   
   // 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)
   }
}
func mReserveID() int64 {
   assertLockHeld(&sched.lock)

   if sched.mnext+1 < sched.mnext {
      throw("runtime: thread ID overflow")
   }
   // mnext表示当前M的数量,并且还表示下一个M的id
   id := sched.mnext
   sched.mnext++
   checkmcount()
   return id
}

maxmcount代表创建的最大线程的M数量,而GOMAXPROCS可以控制P的个数(并行最大数量)

3.1.2 P的初始化

所有的P都在这里初始化

// Change number of processors.
//
// sched.lock must be held, and the world must be stopped.
//
// gcworkbufs must not be being modified by either the GC or the write barrier
// code, so the GC must not be running if the number of Ps actually changes.
//
// Returns list of Ps with local work, they need to be scheduled by the caller.
func procresize(nprocs int32) *p {
   assertLockHeld(&sched.lock)
   assertWorldStopped()
   // 获取之前的P数量
   old := gomaxprocs
   if old < 0 || nprocs <= 0 {
      throw("procresize: invalid arg")
   }

   // update statistics
   now := nanotime()
   if sched.procresizetime != 0 {
      sched.totaltime += int64(old) * (now - sched.procresizetime)
   }
   sched.procresizetime = now

   maskWords := (nprocs + 31) / 32
   
   // 必要的话增加allp
   if nprocs > int32(len(allp)) {
      // Synchronize with retake, which could be running
      // concurrently since it doesn't run on a P.
      lock(&allpLock)
      if nprocs <= int32(cap(allp)) {
         // nprocs调小了,多余的扔掉
         allp = allp[:nprocs]
      } else {
         // 调大了,申请个大的
         nallp := make([]*p, nprocs)
         // 把之前的copy过去,不浪费旧的
         copy(nallp, allp[:cap(allp)])
         allp = nallp
      }
      ...
      unlock(&allpLock)
   }

   // initialize new P's
   for i := old; i < nprocs; i++ {
      pp := allp[i]
      // 如果pp是新建的,则需要初始化
      if pp == nil {
         pp = new(p)
      }
      pp.init(i)
      atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
   }

   _g_ := getg()
   // 如果当前有P能用,则用
   if _g_.m.p != 0 && _g_.m.p.ptr().id < nprocs {
      // continue to use the current P
      _g_.m.p.ptr().status = _Prunning
      _g_.m.p.ptr().mcache.prepareForSweep()
   } else {
      // 否则抢第一个P给当前G的M绑定
      // release the current P and acquire allp[0].
      //
      // We must do this before destroying our current P
      // because p.destroy itself has write barriers, so we
      // need to do that from a valid P.
      if _g_.m.p != 0 {
         if trace.enabled {
            // Pretend that we were descheduled
            // and then scheduled again to keep
            // the trace sane.
            traceGoSched()
            traceProcStop(_g_.m.p.ptr())
         }
         _g_.m.p.ptr().m = 0
      }
      _g_.m.p = 0
      p := allp[0]
      p.m = 0
      p.status = _Pidle
      // 将当前allp[0]绑定到当前的M
      acquirep(p)
      if trace.enabled {
         traceGoStart()
      }
   }

   // g.m.p is now set, so we no longer need mcache0 for bootstrapping.
   mcache0 = nil

   // 不用的P全部释放
   for i := nprocs; i < old; i++ {
      p := allp[i]
      p.destroy()
      // can't free P itself because it can be referenced by an M in syscall
   }

   // 清理完后,截断下allp,保证allp的长度和期望的P数量相等
   if int32(len(allp)) != nprocs {
      lock(&allpLock)
      allp = allp[:nprocs]
      idlepMask = idlepMask[:maskWords]
      timerpMask = timerpMask[:maskWords]
      unlock(&allpLock)
   }

   var runnablePs *p
   // 将除allp[0]以外的全部P都放入空闲列表
   for i := nprocs - 1; i >= 0; i-- {
      p := allp[i]
      if _g_.m.p.ptr() == p {
         continue
      }
      p.status = _Pidle
      // runqempty判断P的本地队列是否没有goroutine,如果没有,则放入空闲列表
      if runqempty(p) {
         pidleput(p)
      } else {
         // 有goroutine则表示有任务要执行,则绑定一个M,这个M从midle链表里获取
         p.m.set(mget())
         // 此处构建可运行的P链表,下一个指向上一个
         p.link.set(runnablePs)
         runnablePs = p
      }
   }
   stealOrder.reset(uint32(nprocs))
   var int32p *int32 = &gomaxprocs // make compiler check that gomaxprocs is an int32
   atomic.Store((*uint32)(unsafe.Pointer(int32p)), uint32(nprocs))
   return runnablePs
}
// Try to get an m from midle list.
// sched.lock must be held.
// May run during STW, so write barriers are not allowed.
//go:nowritebarrierrec
func mget() *m {
   assertLockHeld(&sched.lock)

   mp := sched.midle.ptr()
   if mp != nil {
      sched.midle = mp.schedlink
      sched.nmidle--
   }
   return mp
}
// destroy releases all of the resources associated with pp and
// transitions it to status _Pdead.
//
// sched.lock must be held and the world must be stopped.
func (pp *p) destroy() {
   assertLockHeld(&sched.lock)
   assertWorldStopped()

   // 把p中所有runnable的g全部放到全局队列里
   // Move all runnable goroutines to the global queue
   for pp.runqhead != pp.runqtail {
      // Pop from tail of local queue
      pp.runqtail--
      gp := pp.runq[pp.runqtail%uint32(len(pp.runq))].ptr()
      // Push onto head of global queue
      globrunqputhead(gp)
   }
   if pp.runnext != 0 {
      globrunqputhead(pp.runnext.ptr())
      pp.runnext = 0
   }
   (...)
   // 将P中free的g全部放到sched.gFree中
   gfpurge(pp)
   (...)
   pp.status = _Pdead
}

procresize总结:

  1. 如果allp的P少于期望的P,则扩容
  2. 初始化新创建的P
  3. 如果当前的P能用,则设置为_Prunning
  4. 否则将第一个P抢过来,给当前GM进行绑定
  5. 销毁多余的P
    1. 将P中本地队列中所有g放入全局队列中(sched.runq)
    2. 将P中所有free的g放入ched.gFree中
  6. allp再次截断不要的P
  7. 再次遍历除了P[0]所有的P,将没任务的放入idle链表,有任务的组成一个链表

P初始化之前,刚刚初始化完M,所以步骤4中的P会绑定到初始M上,并且程序刚开始,P的g队列都是空的,所以这些P都会被放到可运行的P链表上,状态都为_Pidle

3.2 G的初始化

我们先看下rt0_go里调用newproc之前的代码

// create a new goroutine to start program
MOVQ   $runtime·mainPC(SB), AX       // entry
PUSHQ  AX
PUSHQ  $0       // arg size
CALL   runtime·newproc(SB)

先把main函数入栈,再把参数个数入栈(这里是0个,对应着函数newproc的两个参数

func newproc(siz int32, fn *funcval) {
   // 函数的参数地址,这里目前指向的是main函数,siz为0,所以之后并没用到
   argp := add(unsafe.Pointer(&fn), sys.PtrSize)
   gp := getg()
   // 获取调用方的PC
   pc := getcallerpc()
   // 这里用g0的系统栈创建g
   systemstack(func() {
      newg := newproc1(fn, argp, siz, gp, pc)

      _p_ := getg().m.p.ptr()
      runqput(_p_, newg, true)

      if mainStarted {
         wakep()
      }
   })
}
// Create a new g in state _Grunnable, starting at fn, with narg bytes
// of arguments starting at argp. callerpc is the address of the go
// statement that created this. The caller is responsible for adding
// the new g to the scheduler.
//
// This must run on the system stack because it's the continuation of
// newproc, which cannot split the stack.
//
//go:systemstack
func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) *g {
   (...)
   // 因为是被systemstack调用的,所以这个g是g0
   _g_ := getg()

   if fn == nil {
      _g_.m.throwing = -1 // do not dump full stacks
      throw("go of nil func value")
   }
   // 在这里锁住g的m,不让被抢占
   acquirem() // disable preemption because it can be holding p in a local var
   siz := narg
   siz = (siz + 7) &^ 7

   
   _p_ := _g_.m.p.ptr()
   // 从P中获得一个新的g但是因为只是初始化阶段,所以并没有g
   newg := gfget(_p_)
   if newg == nil {
      // 新建一个栈为_StackMin(2KB)大小的g
      newg = malg(_StackMin)
      // 将g的状态改为_Gdead
      casgstatus(newg, _Gidle, _Gdead)
      // 放入allg,所以gc不会扫描为初始化的g
      allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
   }
   (...)
   // 空间对齐
   totalSize := 4*sys.PtrSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frame
   totalSize += -totalSize & (sys.StackAlign - 1)               // align to StackAlign
   // 确定新建g的栈的sp的位置,用于存放参数
   sp := newg.stack.hi - totalSize
   spArg := sp
   (...)
   // 如果有参数,则将参数都赋值到新建g的栈上用存储参数的位置上
   if narg > 0 {
      memmove(unsafe.Pointer(spArg), 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)
         }
      }
   }

   memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
   // 新建的g的sp赋值为刚刚算出来的sp
   newg.sched.sp = sp
   newg.stktopsp = sp
   newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
   newg.sched.g = guintptr(unsafe.Pointer(newg))
   gostartcallfn(&newg.sched, fn)
   // 赋值为newproc中获取的调用方的PC
   newg.gopc = callerpc
   newg.ancestors = saveAncestors(callergp)
   newg.startpc = fn.fn
   
   (...)
   
   // 将新建g的状态设置为runnable
   casgstatus(newg, _Gdead, _Grunnable)

   //设置goid 
   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)
   }
   releasem(_g_.m)

   return newg
}

接下来继续执行

newg := newproc1(fn, argp, siz, gp, pc)

_p_ := getg().m.p.ptr()
// 放入p的队列中,true表示放入到runnext
runqput(_p_, newg, true)

if mainStarted {
   wakep()
}
  1. 从P的free链表或者全局free链表获取g,但由于是初始化阶段,不可能有g
  2. 所以要新建g,并将状态设置为dead
  3. 将参数拷贝单g上
  4. 根据SP,参数等,初始化g的运行现场
  5. 所以要新建g,并将状态设置为runnable
  6. 将g加入p的队列中
  7. 因为现在是初始化阶段,主goroutine还未执行,所以不会唤醒P

3.3 启动调度器

TEXT runtime·mstart(SB),NOSPLIT|TOPFRAME,$0
   CALL   runtime·mstart0(SB)
   RET // not reached
// mstart0 is the Go entry-point for new Ms.
// This must not split the stack because we may not even have stack
// bounds set up yet.
//
// May run during STW (because it doesn't have a P yet), so write
// barriers are not allowed.
//
//go:nosplit
//go:nowritebarrierrec
func mstart0() {
   _g_ := getg()

   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.
      //
      // Note: these bounds may not be very accurate.
      // We set hi to &size, but there are things above
      // it. The 1024 is supposed to compensate this,
      // but is somewhat arbitrary.
      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 guard so that we can start calling regular
   // Go code.
   _g_.stackguard0 = _g_.stack.lo + _StackGuard
   // This is the g0, so we can also call go:systemstack
   // functions, which check stackguard1.
   _g_.stackguard1 = _g_.stackguard0
   mstart1()

   // Exit this thread.
   if mStackIsSystemAllocated() {
      // Windows, Solaris, illumos, 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 go:noinline is to guarantee the getcallerpc/getcallersp below are safe,
// so that we can set up g0.sched to return to the call of mstart1 above.
//go:noinline
func mstart1() {
   _g_ := getg()

   if _g_ != _g_.m.g0 {
      throw("bad runtime·mstart")
   }

   // Set up m.g0.sched as a label returning to just
   // after the mstart1 call in mstart0 above, for use by goexit0 and mcall.
   // We're never coming back to mstart1 after we call schedule,
   // so other calls can reuse the current frame.
   // And goexit0 does a gogo that needs to return from mstart1
   // and let mstart0 exit the thread.
   _g_.sched.g = guintptr(unsafe.Pointer(_g_))
   _g_.sched.pc = getcallerpc()
   _g_.sched.sp = getcallersp()

   asminit()
   minit()

   // Install signal handlers; after minit so that minit can
   // prepare the thread to be able to handle the signals.
   if _g_.m == &m0 {
      mstartm0()
   }
   // 当前为空
   if fn := _g_.m.mstartfn; fn != nil {
      fn()
   }

   if _g_.m != &m0 {
      acquirep(_g_.m.nextp.ptr())
      _g_.m.nextp = 0
   }
   schedule()
}

3.3.1 schedule

// 调度器的一轮,找到一个runnable的g并且execute它,永不反回
func schedule() {
   _g_ := getg()

   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:
   pp := _g_.m.p.ptr()
   pp.preempt = false

   if sched.gcwaiting != 0 {
      gcstopm()
      goto top
   }
   if pp.runSafePointFn != 0 {
      runSafePointFn()
   }

   // Sanity check: if we are spinning, the run queue should be empty.
   // Check this before calling checkTimers, as that might call
   // goready to put a ready goroutine on the local run queue.
   if _g_.m.spinning && (pp.runnext != 0 || pp.runqhead != pp.runqtail) {
      throw("schedule: spinning with local work")
   }

   checkTimers(pp, 0)

   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())
      if gp != nil {
         tryWakeP = true
      }
   }
   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.
      if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
         lock(&sched.lock)
         gp = globrunqget(_g_.m.p.ptr(), 1)
         unlock(&sched.lock)
      }
   }
   if gp == nil {
      gp, inheritTime = runqget(_g_.m.p.ptr())
      // We can see gp != nil here even if the M is spinning,
      // if checkTimers added a local goroutine via goready.
   }
   if gp == nil {
      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 {
      wakep()
   }
   if gp.lockedm != 0 {
      // Hands off own p to the locked m,
      // then blocks waiting for a new p.
      startlockedm(gp)
      goto top
   }

   execute(gp, inheritTime)
}