关于golang:详解-Go-程序的启动流程你知道-g0m0-是什么吗

55次阅读

共计 5274 个字符,预计需要花费 14 分钟才能阅读完成。

微信搜寻【脑子进煎鱼了】关注这一只爆肝煎鱼。本文 GitHub github.com/eddycjy/blog 已收录,有我的系列文章、材料和开源 Go 图书。

大家好,我是煎鱼。

自古应用程序均从 Hello World 开始,你我所写的 Go 语言亦然:

import "fmt"

func main() {fmt.Println("hello world.")
}

这段程序的输入后果为 hello world.,就是这么的简略又间接。但这时候又不禁思考了起来,这个 hello world. 是怎么输入来,经验了什么过程。

真是十分的好奇,明天咱们就一起来探一探 Go 程序的启动流程。
其中波及到 Go Runtime 的调度器启动,g0,m0 又是什么?

车门焊死,正式开始吸鱼之路。

Go 疏导阶段

查找入口

首先编译上文提到的示例程序:

$ GOFLAGS="-ldflags=-compressdwarf=false" go build 

在命令中指定了 GOFLAGS 参数,这是因为在 Go1.11 起,为了缩小二进制文件大小,调试信息会被压缩。导致在 MacOS 上应用 gdb 时无奈了解压缩的 DWARF 的含意是什么(而我恰好就是用的 MacOS)。

因而须要在本次调试中将其敞开,再应用 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 包中,不同的计算机架构指向不同。例如:

  • MacOS 在 src/runtime/rt0_darwin_amd64.s
  • Linux 在 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 的缩写,指代运行时的创世,超级奶爸:

  • darwin 代表指标操作系统(GOOS)。
  • amd64 代表指标操作系统架构(GOHOSTARCH)。

同时 Go 语言还反对更多的指标零碎架构,例如:AMD64、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)
    ...
  • runtime.check:运行时类型查看,次要是校验编译器的翻译工作是否正确,是否有“坑”。根本代码均为查看 int8unsafe.Sizeof 办法下是否等于 1 这类动作。
  • runtime.args:零碎参数传递,次要是将零碎参数转换传递给程序应用。
  • runtime.osinit:零碎根本参数设置,次要是获取 CPU 外围数和内存物理页大小。
  • runtime.schedinit:进行各种运行时组件的初始化,蕴含调度器、内存分配器、堆、栈、GC 等一大堆初始化工作。会进行 p 的初始化,并将 m0 和某一个 p 进行绑定。
  • runtime.main:次要工作是运行 main goroutine,尽管在runtime·rt0_go 中指向的是$runtime·mainPC,但本质指向的是 runtime.main
  • runtime.newproc:创立一个新的 goroutine,且绑定 runtime.main 办法(也就是应用程序中的入口 main 办法)。并将其放入 m0 绑定的 p 的本地队列中去,以便后续调度。
  • runtime.mstart:启动 m,调度器开始进行循环调度。

runtime·rt0_go 办法中,其次要是实现各类运行时的查看,零碎参数设置和获取,并进行大量的 Go 根底组件初始化。

初始化结束后进行主协程(main goroutine)的运行,并放入期待队列(GMP 模型),最初调度器开始进行循环调度。

小结

根据上述源码分析,能够得出如下 Go 应用程序疏导的流程图:

在 Go 语言中,理论的运行入口并不是用户日常所写的 main func,更不是 runtime.main 办法,而是从 rt0_*_amd64.s 开始,最终再一路 JMP 到 runtime·rt0_go 里去,再在该办法里实现一系列 Go 本身所须要实现的绝大部分初始化动作。

其中整体包含:

  • 运行时类型查看、零碎参数传递、CPU 核数获取及设置、运行时组件的初始化(调度器、内存分配器、堆、栈、GC 等)。
  • 运行 main goroutine。
  • 运行相应的 GMP 等大量缺省行为。
  • 波及到调度器相干的大量常识。

后续将会持续分析将进一步分析 runtime·rt0_go 里的爱与恨,尤其像是 runtime.mainruntime.schedinit 等调度办法,都有十分大的学习价值,有趣味的小伙伴能够继续关注。

Go 调度器初始化

晓得了 Go 程序是怎么疏导起来的之后,咱们须要理解 Go Runtime 中调度器是怎么流转的。

runtime.mstart

这里次要关注 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 的执行栈 _g_.stack 的边界(堆栈的边界正好是 lo, hi)来确定是否为零碎栈。若是,则依据零碎栈初始化 g 执行栈的边界。
  • 调用 mstart1 办法启动零碎线程 m,进行调度器循环调度。
  • 调用 mexit 办法退出零碎线程 m。

runtime.mstart1

这么看来其实质逻辑在 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 阶段时的复用。
  • 若确定以后的 g 所绑定的 m 是 m0,则调用 mstartm0 办法,设置信号 handler。该动作必须在 minit 办法之后,这样 minit 办法能够提前准备好线程,以便可能解决信号。
  • 若以后 g 所绑定的 m 有启动函数,则运行。否则跳过。
  • 若以后 g 所绑定的 m 不是 m0,则须要调用 acquirep 办法获取并绑定 p,也就是 m 与 p 绑定。
  • 调用 schedule 办法进行正式调度。

忙活了一大圈,终于进入到开题的主菜了,原来埋伏的很深的 schedule 办法才是真正做调度的办法,其余都是前置解决和筹备数据。

因为篇幅问题,schedule 办法会放到下篇再持续分析,咱们先聚焦本篇的一些细节点。

问题深剖

不过到这里篇幅也曾经比拟长了,积攒了不少问题。咱们针对在 Runtime 中出镜率最高的两个元素进行分析:

  1. m0 是什么,作用是?
  2. g0 是什么,作用是?

m0

m0 是 Go Runtime 所创立的第一个零碎线程,一个 Go 过程只有一个 m0,也叫主线程。

从多个方面来看:

  • 数据结构:m0 和其余创立的 m 没有任何区别。
  • 创立过程:m0 是过程在启动时应该汇编间接复制给 m0 的,其余后续的 m 则都是 Go Runtime 内自行创立的。
  • 变量申明:m0 和惯例 m 一样,m0 的定义就是 var m0 m,没什么特别之处。

g0

g 个别分为三种,别离是:

  • 执行用户工作的叫做 g。
  • 执行 runtime.main 的 main goroutine。
  • 执行调度工作的叫 g0。。

g0 比拟非凡,每一个 m 都只有一个 g0(仅此只有一个 g0),且每个 m 都只会绑定一个 g0。在 g0 的赋值上也是通过汇编赋值的,其余后续所创立的都是惯例的 g。

从多个方面来看:

  • 数据结构:g0 和其余创立的 g 在数据结构上是一样的,然而存在栈的差异。在 g0 上的栈调配的是零碎栈,在 Linux 上栈大小默认固定 8MB,不能扩缩容。而惯例的 g 起始只有 2KB,可扩容。
  • 运行状态:g0 和惯例的 g 不一样,没有那么多种运行状态,也不会被调度程序抢占,调度自身就是在 g0 上运行的。
  • 变量申明:g0 和惯例 g,g0 的定义就是 var g0 g,没什么特别之处。

小结

在本章节中咱们解说了 Go 调度器初始化的一个过程,别离波及:

  • runtime.mstart。
  • runtime.mstart1。

基于此也理解到了在调度器初始化过程中,须要筹备什么,初始化什么。另外针对调度过程中最常提到的 m0、g0 的概念咱们进行了梳理和阐明。

总结

在明天这篇文章中,咱们具体的介绍了 Go 语言的疏导启动过程中的所有流程和初始化动作。

同时针对调度器的初始化进行了初步剖析,具体介绍了 m0、g0 的用处和区别。
在下一篇文章中咱们将进一步对真正调度的 schedule 办法进行详解,这块也是个硬骨头了。

若有任何疑难欢送评论区反馈和交换,最好的关系是相互成就 ,各位的 点赞 就是煎鱼创作的最大能源,感激反对。

文章继续更新,能够微信搜【脑子进煎鱼了】浏览,回复【000】有我筹备的一线大厂面试算法题解和材料;本文 GitHub github.com/eddycjy/blog 已收录,欢送 Star 催更。

正文完
 0