微信搜索【脑补炸鱼】关注这条炸肝炸鱼。本文GitHubgithub.com/eddycjy/blog已收录,附有我的系列文章、资料和开源Go书籍。大家好,我是炸鱼。自古以来,应用都是从HelloWorld开始的,你我写的Go语言也是如此:世界,就是这么简单直接。但是这个时候,我不禁会想这个helloworld是怎么来的。是输出,它经历了什么过程。真的很好奇,今天我们就一起来探究一下Go程序的启动过程。涉及到GoRuntime调度器的启动,g0和m0是什么?车门被焊死,吸鱼之路正式开始。Gobootstrap阶段搜索入口首先编译上面提到的示例程序:$GOFLAGS="-ldflags=-compressdwarf=false"gobuild命令中指定了GOFLAGS参数,这是因为从Go1.11开始,为了减少二进制文件大小,调试信息将被压缩。这使得在MacOS上使用gdb时无法理解压缩的DWARF是什么意思(而我恰好在使用MacOS)。因此,在本次调试中需要将其关闭,然后使用gdb进行调试,以达到观察的目的:文件:`/Users/eddycjy/go-application/awesomeProject/awesomeProject',文件类型mach-o-x86-64。Entrypoint:0x1063c800x0000000001001000-0x00000000010a6acais.text...(gdb)b*0x106cxpoint801Bat:file/usr/local/Cellar/go/1.15/libexec/src/runtime/rt0_darwin_amd64.s,line8.通过Entry的调试点,可以看到真正的程序入口在runtime包中,不同的计算机架构指向的点不同。例如:src/runtime/rt0_darwin_amd64.s中的MacOS。Linux在src/runtime/rt0_linux_amd64.s中。最终指向rt0_darwin_amd64.s文件,文件名很直观:Breakpoint1at0x1063c80:file/usr/local/Cellar/go/1.15/libexec/src/runtime/rt0_darwin_amd64.s,line8.rt0代表runtime0一个缩写,指的是运行时的起源,超级爸爸:darwin代表目标操作系统(GOOS)。amd64代表目标操作系统架构(GOHOSTARCH)。同时,Go语言还支持更多的目标系统架构,如:AMD64、AMR、MIPS、WASM等:有兴趣的可以去src/runtime目录进一步查看,我会这里就不一一介绍了。入口方法在rt0_linux_amd64.s文件中。可以发现_rt0_amd64_darwinJMP跳转到_rt0_amd64方法:TEXT_rt0_amd64_darwin(SB),NOSPLIT,$-8JMP_rt0_amd64(SB)...然后跳转到runtime·rt0_go方法:TEXT_rt0_amd64(SB),NOSPLIT,$-8MOVQ0(SP),DI//argcLEAQ8(SP),SI//argvJMPruntime·rt0_go(SB)该方法将程序输入的argc和argv从内存中移入寄存器。栈指针(SP)的前两个值是argc和argv,分别对应参数个数和每个参数的值。主线程序参数准备好后,正式的初始化方法落在runtimert0_go方法中:TEXTruntimert0_go(SB),NOSPLIT,$0...CALLruntimecheck(SB)MOVL16(SP),AX//copyargcMOVLAX,0(SP)MOVQ24(SP),AX//复制argvMOVQAX,8(SP)CALLruntime·args(SB)CALLruntime·osinit(SB)CALLruntime·schedinit(SB)//创建一个新的goroutine启动程序MOVQ$runtime·mainPC(SB),AX//entryPUSHQAXPUSHQ$0//argsizeCALLruntime·newproc(SB)POPQAXPOPQAX//启动这个MCALLruntime·mstart(SB)...runtime.check:运行时类型检查,主要是验证编译器的翻译工作是否正确,是否存在“陷阱”。基本代码是在unsafe.Sizeof方法下检查int8是否等于1。runtime.args:系统参数传递,主要是将系统参数转换传递给程序使用。runtime.osinit:系统基本参数设置,主要获取CPU核心数和内存物理页大小。runtime.schedinit:初始化各种运行时组件,包括调度器、内存分配器、堆、栈、GC等大量的初始化工作。它会初始化p并将m0绑定到某个p。runtime.main:主要工作是运行主goroutine。runtime.rt0_go虽然指向$runtime.mainPC,但实际上指向的是runtime.main。runtime.newproc:新建一个goroutine,并绑定runtime.main方法(即应用中的入口main方法)。并放入m0绑定的p的本地队列中,用于后续调度。runtime.mstart:启动m,调度器开始循环调度。在runtime·rt0_go方法中,主要完成各种运行时检查,系统参数的设置和获取,初始化大量的Go基础组件。初始化完成后,主协程(maingoroutine)运行并放入等待队列(GMP模型),最后调度器开始进行循环调度。总结通过分析以上源码,可以得到如下Go应用程序bootstrap流程图:在Go语言中,实际运行的入口并不是用户日常编写的mainfunc,更不用说runtime.main了。方法,而是从rt0_*_amd64.s,最后一路JMP到runtime·rt0_go,然后在这个方法中完成Go本身需要完成的一系列大部分初始化动作。整体包括:运行时类型检查、系统参数传递、CPU核心获取和设置、运行时组件初始化(调度器、内存分配器、堆、栈、GC等)。运行主协程。广泛的默认行为,例如运行相应的GMP。涉及到很多调度器相关的知识。后续会继续分析runtime·rt0_go中的爱恨情仇,尤其是runtime.main和runtime.schedinit等调度方式,很有学习价值,感兴趣的朋友可以继续关注。Goschedulerinitialization在了解了Go程序是如何引导的之后,我们需要了解调度程序在GoRuntime中是如何流动的。runtime.mstart主要关注runtime.mstart方法:funcmstart(){//获取g0_g_:=getg()//判断栈边界osStack:=_g_.stack.lo==0ifosStack{size:=_g_.stack.hiifsize==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,scheduler循环调度mstart1()//退出线程ifmStackIsSystemAllocated(){osStack=true}mexit(osStack)}调用GMP模型中getg方法获取g,这里是g0。通过查看g的执行栈_g_.stack的边界来判断是否是系统栈(栈边界正好是lo,hi)。如果是,则根据系统栈初始化g执行栈的边界。调用mstart1方法启动系统线程m,进行scheduler循环调度。调用mexit方法退出系统线程m。从runtime.mstart1来看,其真正的逻辑在于mstart1方法。我们继续分析:funcmstart1(){//获取g并判断是否为g0_g_:=getg()if_g_!=_g_.m.g0{throw("badruntime·mstart")}//初始化m并记录调用者pc,spsave(getcallerpc(),getcallersp())asminit()minit()//设置信号处理程序if_g_.m==&m0{mstartm0()}//运行启动函数iffn:=_g_.m.mstartfn;fn!=nil{fn()}如果_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,方便后续调度阶段复用。如果确定当前g绑定的m为m0,则调用mstartm0方法设置信号处理器。此操作必须在minit方法之后进行,以便minit方法可以提前准备线程,以便它可以处理信号。如果g当前绑定的m有启动函数,就运行它。否则跳过。如果当前绑定g的m不是m0,则需要调用acquirep方法获取并绑定p,即m绑定p。调用schedule方法进行正式调度。折腾了一番,终于进入到开题正题了。原来隐藏的深层调度方法才是真正的调度方法,剩下的就是预处理和数据准备了。由于篇幅问题,schedule方法会放到下一篇继续分析。我们先关注一下这篇文章的一些细节。深入分析问题,不过这篇文章的篇幅已经比较长了,积累了很多问题。我们分析一下Runtime中最常见的两个元素:m0是什么,它的作用是什么?g0是什么,它有什么作用?m0m0是GoRuntime创建的第一个系统线程。一个Go进程只有一个m0,也叫主线程。从几个角度来看:数据结构:m0与其他任何创建的m都没有区别。创建过程:m0是进程启动时应该编译直接复制到m0的进程,后面的其他ms都是GoRuntime创建的。变量声明:m0和regularm一样,m0的定义是varm0m,没什么特别的。g0g一般分为三种,即:执行用户任务的称为g。执行runtime.main的主goroutine。执行调度任务的那个叫做g0。.g0比较特殊,每个m只有一个g0(onlyoneg0),每个m只能绑定一个g0。g0的赋值也是汇编赋值,后面的其余创建都是常规的g。从很多方面来说:数据结构:g0在数据结构上和另一个创建的g是一样的,只是在栈上有区别。g0上的堆栈分配系统堆栈。在Linux上,堆栈大小默认固定为8MB,无法扩展或收缩。常规的g开头只有2KB,可以扩展。运行状态:g0与常规g不同。没有那么多运行状态,也不会被调度器抢占。调度本身在g0上运行。变量声明:g0和regularg,g0的定义就是varg0g,没什么特别的。小结本章我们讲解了一个Go调度器初始化的过程,涉及到:runtime.mstart。运行时.mstart1。基于此,我也了解了调度器初始化过程中需要准备和初始化的内容。另外,我们对调度过程中提到频率最高的m0和g0的概念进行了梳理和解释。小结在今天的文章中,我们详细介绍了Go语言bootstrap过程中的所有过程和初始化动作。同时对调度器的初始化进行了初步分析,详细介绍了m0和g0的用途和区别。在下一篇文章中,我们将进一步详细讲解真正的调度调度方法,这也是一块硬骨头。如有任何问题,欢迎在评论区反馈交流。最好的关系是相互成就。您的好评是创作炸鱼最大的动力。感谢您的支持。文章持续更新中,微信搜索【脑补炸鱼】即可阅读,回复【000】一线大厂面试算法方案和资料我都准备好了;本文已收录在GitHubgithub.com/eddycjy/blog,欢迎Star提醒。
