微信搜索【 脑子进煎鱼了】关注这一只爆肝煎鱼。本文 GitHub github.com/eddycjy/blog 已收录,有个人系列文章、资料和开源 Go 图书。
你们好,我是煎鱼。linux
自古应用程序均从 Hello World 开始,你我所写的 Go 语言亦然:git
import "fmt" func main() { fmt.Println("hello world.") }
这段程序的输出结果为 hello world.
,就是这么的简单又直接。但这时候又不由思考了起来,这个 hello world.
是怎么输出来,经历了什么过程。github
真是很是的好奇,今天咱们就一块儿来探一探 Go 程序的启动流程。
其中涉及到 Go Runtime 的调度器启动,g0,m0 又是什么?golang
车门焊死,正式开始吸鱼之路。面试
首先编译上文提到的示例程序:算法
$ GOFLAGS="-ldflags=-compressdwarf=false" go build
在命令中指定了 GOFLAGS 参数,这是由于在 Go1.11 起,为了减小二进制文件大小,调试信息会被压缩。致使在 MacOS 上使用 gdb 时没法理解压缩的 DWARF 的含义是什么(而我偏偏就是用的 MacOS)。shell
所以须要在本次调试中将其关闭,再使用 gdb 进行调试,以此达到观察的目的:微信
$ gdb awesomeProject (gdb) info files Symbols from "/Users/eddycjy/go-application/awesomeProject/awesomeProject". Local exec file: `/Users/eddycjy/go-application/awesomeProject/awesomeProject', file type mach-o-x86-64. Entry point: 0x1063c80 0x0000000001001000 - 0x00000000010a6aca is .text ... (gdb) b *0x1063c80 Breakpoint 1 at 0x1063c80: file /usr/local/Cellar/go/1.15/libexec/src/runtime/rt0_darwin_amd64.s, line 8.
经过 Entry point 的调试,可看到真正的程序入口在 runtime 包中,不一样的计算机架构指向不一样。例如:数据结构
src/runtime/rt0_darwin_amd64.s
。src/runtime/rt0_linux_amd64.s
。其最终指向了 rt0_darwin_amd64.s 文件,这个文件名称很是的直观:架构
Breakpoint 1 at 0x1063c80: file /usr/local/Cellar/go/1.15/libexec/src/runtime/rt0_darwin_amd64.s, line 8.
rt0 表明 runtime0 的缩写,指代运行时的创世,超级奶爸:
同时 Go 语言还支持更多的目标系统架构,例如:AMD6四、AMR、MIPS、WASM 等:
如有兴趣可到 src/runtime
目录下进一步查看,这里就不一一介绍了。
在 rt0_linux_amd64.s 文件中,可发现 _rt0_amd64_darwin
JMP 跳转到了 _rt0_amd64
方法:
TEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8 JMP _rt0_amd64(SB) ...
紧接着又跳转到 runtime·rt0_go
方法:
TEXT _rt0_amd64(SB),NOSPLIT,$-8 MOVQ 0(SP), DI // argc LEAQ 8(SP), SI // argv JMP runtime·rt0_go(SB)
该方法将程序输入的 argc 和 argv 从内存移动到寄存器中。
栈指针(SP)的前两个值分别是 argc 和 argv,其对应参数的数量和具体各参数的值。
程序参数准备就绪后,正式初始化的方法落在 runtime·rt0_go
方法中:
TEXT runtime·rt0_go(SB),NOSPLIT,$0 ... 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 PUSHQ AX PUSHQ $0 // arg size CALL runtime·newproc(SB) POPQ AX POPQ AX // start this M CALL runtime·mstart(SB) ...
int8
在 unsafe.Sizeof
方法下是否等于 1 这类动做。runtime·rt0_go
中指向的是$runtime·mainPC
,但实质指向的是 runtime.main
。runtime.main
方法(也就是应用程序中的入口 main 方法)。并将其放入 m0 绑定的p的本地队列中去,以便后续调度。在 runtime·rt0_go
方法中,其主要是完成各种运行时的检查,系统参数设置和获取,并进行大量的 Go 基础组件初始化。
初始化完毕后进行主协程(main goroutine)的运行,并放入等待队列(GMP 模型),最后调度器开始进行循环调度。
根据上述源码剖析,能够得出以下 Go 应用程序引导的流程图:
在 Go 语言中,实际的运行入口并非用户平常所写的 main func
,更不是 runtime.main
方法,而是从 rt0_*_amd64.s
开始,最终再一路 JMP 到 runtime·rt0_go
里去,再在该方法里完成一系列 Go 自身所须要完成的绝大部分初始化动做。
其中总体包括:
后续将会继续剖析将进一步剖析 runtime·rt0_go
里的爱与恨,尤为像是 runtime.main
、runtime.schedinit
等调度方法,都有很是大的学习价值,有兴趣的小伙伴能够持续关注。
知道了 Go 程序是怎么引导起来的以后,咱们须要了解 Go Runtime 中调度器是怎么流转的。
这里主要关注 runtime.mstart
方法:
func mstart() { // 获取 g0 _g_ := getg() // 肯定栈边界 osStack := _g_.stack.lo == 0 if osStack { 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 } _g_.stackguard0 = _g_.stack.lo + _StackGuard _g_.stackguard1 = _g_.stackguard0 // 启动 m,进行调度器循环调度 mstart1() // 退出线程 if mStackIsSystemAllocated() { osStack = true } mexit(osStack) }
getg
方法获取 GMP 模型中的 g,此处获取的是 g0。_g_.stack
的边界(堆栈的边界正好是 lo, hi)来肯定是否为系统栈。如果,则根据系统栈初始化 g 执行栈的边界。mstart1
方法启动系统线程 m,进行调度器循环调度。mexit
方法退出系统线程 m。这么看来其实质逻辑在 mstart1
方法,咱们继续往下剖析:
func mstart1() { // 获取 g,并判断是否为 g0 _g_ := getg() if _g_ != _g_.m.g0 { throw("bad runtime·mstart") } // 初始化 m 并记录调用方 pc、sp save(getcallerpc(), getcallersp()) asminit() minit() // 设置信号 handler 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() }
getg
方法获取 g。而且经过前面绑定的 _g_.m.g0
判断所获取的 g 是否 g0。若不是,则直接抛出致命错误。由于调度器仅在 g0 上运行。minit
方法初始化 m,并记录调用方的 PC、SP,便于后续 schedule 阶段时的复用。mstartm0
方法,设置信号 handler。该动做必须在 minit
方法以后,这样 minit
方法能够提早准备好线程,以便可以处理信号。acquirep
方法获取并绑定 p,也就是 m 与 p 绑定。schedule
方法进行正式调度。忙活了一大圈,终于进入到开题的主菜了,原来潜伏的很深的 schedule
方法才是真正作调度的方法,其余都是前置处理和准备数据。
因为篇幅问题,schedule
方法会放到下篇再继续剖析,咱们先聚焦本篇的一些细节点。
不过到这里篇幅也已经比较长了,积累了很多问题。咱们针对在 Runtime 中出镜率最高的两个元素进行剖析:
m0
是什么,做用是?g0
是什么,做用是?m0 是 Go Runtime 所建立的第一个系统线程,一个 Go 进程只有一个 m0,也叫主线程。
从多个方面来看:
var m0 m
,没什么特别之处。g 通常分为三种,分别是:
runtime.main
的 main goroutine。g0 比较特殊,每个 m 都只有一个 g0(仅此只有一个 g0),且每一个 m 都只会绑定一个 g0。在 g0 的赋值上也是经过汇编赋值的,其他后续所建立的都是常规的 g。
从多个方面来看:
var g0 g
,没什么特别之处。在本章节中咱们讲解了 Go 调度器初始化的一个过程,分别涉及:
基于此也了解到了在调度器初始化过程当中,须要准备什么,初始化什么。另外针对调度过程当中最常提到的 m0、g0 的概念咱们进行了梳理和说明。
在今天这篇文章中,咱们详细的介绍了 Go 语言的引导启动过程当中的全部流程和初始化动做。
同时针对调度器的初始化进行了初步分析,详细介绍了 m0、g0 的用途和区别。
在下一篇文章中咱们将进一步对真正调度的 schedule
方法进行详解,这块也是个硬骨头了。
如有任何疑问欢迎评论区反馈和交流,最好的关系是互相成就,各位的点赞就是煎鱼创做的最大动力,感谢支持。
文章持续更新,能够微信搜【脑子进煎鱼了】阅读,回复【 000】有我准备的一线大厂面试算法题解和资料;本文 GitHub github.com/eddycjy/blog 已收录,欢迎 Star 催更。