在Golang中,程序的执行入口为 main()
函数,那么底层又是如何工作的呢? 这个问题的答案我们可以在runtime源码找到。对它的解释主要在 src/runtime/proc.go
文件,下面我们看一下它是如何一步一步开始执行的。go version 1.15.6
在文件头部有一段对 Goroutine scheduler
的介绍,我们先了解一下。
调度器的工作是分发goroutines
到工作线程让其运行。一句话指明了调度器的存在意义,就是指挥协调GPM干活。
主要包含三部分G
指的是 goroutineM
工作线程,也叫machine
P
处理器(逻辑CPU),执行 Go code
的一种资源。这里的Go code 其实就是 goroutine里的代码。
M
必须被指派给P
去执行 Go code
, 但可以被阻塞或通过P进行系统调用。
设计文档 https://golang.org/s/go11sched
再往下会发现一段注释说明
// src/runtime/proc.go // The bootstrap sequence is: // // call osinit // call schedinit // make & queue new G // call runtime·mstart // // The new G calls runtime·main.
不错,这个说的就是我们今天的重点。共分四大阶段, 在第三阶段创建新G是通过调用 runtime.main()
函数实现的,主要用来创建一个main goroutine,然后再运行 runtime.mstart
并启动m0
。整个流程如图所示

M0
是启动程序后的编号为0
的主线程,这个M
对应的实例会在全局变量 runtime.m0
中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G, 在之后M0就和其他的M一样了。
目录
osinit
系统初始化, runtime.osinit()
函数,这个文件的位置根据操作系统架构的使用的文件不一样。如果为linux的话,则文件为os_linux.go, windows为 os_windows.go。
以下是 os_linux.go
文件里的 osinit
函数原型
func osinit() { ncpu = getproccount() physHugePageSize = getHugePageSize() osArchInit() }
主要是获取CPU数量
,页大小
和 操作系统初始化
工作。
schedinit
调度器初始化, 调用的函数为schedinit()
。
func schedinit() { lockInit(&sched.lock, lockRankSched) lockInit(&sched.sysmonlock, lockRankSysmon) lockInit(&sched.deferlock, lockRankDefer) lockInit(&sched.sudoglock, lockRankSudog) lockInit(&deadlock, lockRankDeadlock) lockInit(&paniclk, lockRankPanic) lockInit(&allglock, lockRankAllg) lockInit(&allpLock, lockRankAllp) lockInit(&reflectOffs.lock, lockRankReflectOffs) lockInit(&finlock, lockRankFin) lockInit(&trace.bufLock, lockRankTraceBuf) lockInit(&trace.stringsLock, lockRankTraceStrings) lockInit(&trace.lock, lockRankTrace) lockInit(&cpuprof.lock, lockRankCpuprof) lockInit(&trace.stackTab.lock, lockRankTraceStackTab) ... }
全是与锁有关的函数,前四个是和调度器相关,接着两个与panic和deadlock相关。
lockInit(&allglock, lockRankAllg)
和 lockInit(&allpLock, lockRankAllp)
是和G、P有关。lockInit(&cpuprof.lock, lockRankCpuprof)
与性能分析有关
func schedinit() { ... // raceinit must be the first call to race detector. // In particular, it must be done before mallocinit below calls racemapshadow. _g_ := getg() if raceenabled { _g_.racectx, raceprocctx0 = raceinit() } sched.maxmcount = 10000 ... }
调用函数 getg()
获取当前的一个G。
如果启用race的话,进行 raceinit()
调用。
设置 m
的最大数量,这里是固定的10000
,有必要记住这一点,面试经常问的。
func schedinit() { ... tracebackinit() moduledataverify() stackinit() mallocinit() fastrandinit() // must run before mcommoninit mcommoninit(_g_.m, -1) cpuinit() // must run before alginit alginit() // maps must not be used before this call modulesinit() // provides activeModules typelinksinit() // uses maps, activeModules itabsinit() // uses activeModules ... }
stackinit()
栈初始化mallocinit()
mcommoninit()
对当前m进行初始化,有锁操作(重点了解)cpuinit()
读取环境变量GODEBUG,并调用 internal/cpu.Initializealginit()
map使用必须调用,算法相关typelinksinit()
map相关modulesinit()
是与go module 相关itabsinit()
与go module 相关
func schedinit() { ... sigsave(&_g_.m.sigmask) initSigmask = _g_.m.sigmask goargs() goenvs() parsedebugvars() gcinit() ... }
gcinit()
GC初始化
func schedinit() { ... 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") } ... }
sched.lastpoll
设置调度器初始化轮训时间procs
: 设置当前cpu个数,在 osinit()
函数里已经获取到。如果环境变量设置了CPU个数,直使用设置个数。调用函数procresize() 函数调整cpu 数量,此操作会导致STW,和shched被锁。
func schedinit() { ... // 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" } if len(modinfo) == 1 { // Condition should never trigger. This code just serves // to ensure runtime·modinfo is kept in the resulting binary. modinfo = "" } }
cgocheck 与cgo 相关,可能会与 writeBarrier 相关,建议了解一下 writeBarrier
总结
这个函数是首个调用的函数,大部分与基本配置有关,如锁、M的最大数量为10000,CPU 个数,GC等等。
make && queue new G
调用函数 newproc
,创建一个新的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. // 使用一个 siz 字节的参数创建一个 fn 的新 g,将它放在g队列里等待运行 // 编译器将 go 语句转换为对这个函数的调用 // // The stack layout of this call is unusual: it assumes that the // arguments to pass to fn are on the stack sequentially immediately // after &fn. Hence, they are logically part of newproc's argument // frame, even though they don't appear in its signature (and can't // because their types differ between call sites). // // This must be nosplit because this stack layout means there are // untyped arguments in newproc's argument frame. Stack copies won't // be able to adjust them and stack splits won't be able to copy them. // //go:nosplit func newproc(siz int32, fn *funcval) {}
mstart
调用 runtime.mstart()
函数。这个函数是M的入口。函数原型:
// mstart is the 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 mstart() {}
mstart
函数是一个新M的入口。
我们接着看一下函数体。栈是不能拆分的,因为有可能还未设置栈边界。
可运行在 STW
期间(还没有P),所以 写屏障
是不允许的。
//go:nosplit //go:nowritebarrierrec func mstart() { // 获取一个G(当前为g0) _g_ := getg() // 检查当前G的边界lo是否等于0,如果等于则初始化系统栈 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. // 从 system statck 中初始化 _g_.stack 边界 size := _g_.stack.hi // Cgo if size == 0 { size = 8192 * sys.StackGuardMultiplier } // 初始化_g_.stack _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,以便可以运行 go code _g_.stackguard0 = _g_.stack.lo + _StackGuard // This is the g0, so we can also call go:systemstack // functions, which check stackguard1. // 这是g0,所以我们也可以调用go:systemstack 函数检查 stackguard1 _g_.stackguard1 = _g_.stackguard0 // 启动m mstart1() // Exit this thread. // 退出当前线程 switch GOOS { case "windows", "solaris", "illumos", "plan9", "darwin", "aix": // 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) }
函数 mstart()
主要工作就是先获取一个G
(g0),然后进行一些栈和栈保护的初始化工作,然后再启动M,最后再退出M。这里关注重点是 mstart1()
和 mexit()
函数。
func mstart1() { _g_ := getg() // 判断当前g是否为g0, 在 mstart() 函数里获取的就是g0,这里再判断一次 // g0 是m创建的第一个goroutine,与后面创建的普通goroutine不同,g0主要用来实现对普通goroutine的调度 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. // 记录caller用在mcall中栈顶和终止线程 // 在调用 schedule 后,将不会再返回到 mstart1,所以其它调用可以复用当前 frame // 需要关注下 minit() 函数 save(getcallerpc(), getcallersp()) asminit() minit() // Install signal handlers; after minit so that minit can // prepare the thread to be able to handle the signals. // 安装信息处理器,以便 minit 后,线程可以处理信息 // 当前g0 是 m0 ,则直接启用 m0, m0是一个全局变量 if _g_.m == &m0 { mstartm0() } // 当前m0注册有初始化函数 if fn := _g_.m.mstartfn; fn != nil { fn() } // 当前g0 不是 m0(上面是相等的判断),则从当前绑定的m 里获取一个准备好的P (_g_.m.nextp.ptr())并关联到当前 m 上 if _g_.m != &m0 { acquirep(_g_.m.nextp.ptr()) _g_.m.nextp = 0 } // 调度 重点!重点!重点! schedule() }
主要相关的函数有 minit()
、mstartm0()
、acquirep()
和 schedule()
。
执行顺序从上到下依次为:
minit()
调用以初始化新的m(包括引导程序m),在新线程上调用,无法分配内存mstartm0()
此函数实现了仅在 m0
上运行的mstart1
的一部分acquirep()
关联一个P到当前的mschedule()
一轮调度,找到一个runnable
状态的goroutine并执行
参考
- https://www.cnblogs.com/ts65214/p/12977057.html
- http://www.voidcn.com/article/p-rxywtcud-bqd.html
- https://segmentfault.com/a/1190000021951119
- https://zhuanlan.zhihu.com/p/27328476