golang程序启动流程详解

环境

go1.16.5 linux/amd64

用例

package mainimport "fmt"func main() {fmt.Println(42)
}

编译

-gcflags “-N -l”: 关闭优化和内联,方便调试跟踪

$ go build -gcflags "-N -l" -o hello hello.go

gdb跟踪执行流程

$ gdb hello$ source /usr/lib/go/src/runtime/runtime-gdb.py # 加载Go运行时支持

预备知识:

1. GMP调度模型
  • Golang的调度器模型是"GMP"模型,P作为逻辑cpu的抽象,解决了竞争全局队列等问题.
  • M是操作系统线程,M必须关联到某个P上,从P上获取工作goroutine
  • 一个P可能有多个M,当某个M阻塞时.

2. runtime/proc.go中定义了一些重要的全局符号,下面分析启动流程会涉及这些符号:
var (m0           m // 第一个mg0           g // 第一个goroutinemcache0      *mcache // m0的cacheraceprocctx0 uintptr // 用于竞争检测
)
  • g0: 主线程上的第一个协程g0, g0拥有这个线程的系统栈,这个栈很大.g0还有创建新协程的职责,当我们调用go func创建新协程都会在g0的栈上执行.
  • m0: 第一个工作线程,主线程
  • mcache0: m0的cache
3. tls线程私有存储

每个线程的私有存储空间,golang主要用其来设置每个m当前正在运行的goroutine,这样可以快速获取到当前上下文的goroutine. 类似于linux内核中的current宏.

4. sched全局结构

golang使用一个全局schedt结构来控制全局调度(runtime2.go),里面主要的信息如全局运行队列,所有m,所有p的状态信息,系统监控sysmon等

var (allm       *mgomaxprocs int32ncpu       int32forcegc    forcegcstatesched      schedtnewprocs   int32// allpLock protects P-less reads and size changes of allp, idlepMask,// and timerpMask, and all writes to allp.allpLock mutex// len(allp) == gomaxprocs; may change at safe points, otherwise// immutable.allp []*p

程序入口函数:

  • 为g0分配栈空间
runtime.asm_amd64.s:89TEXT runtime·rt0_go<ABIInternal>(SB),NOSPLIT,$0// copy arguments forward on an even stackMOVQ    DI, AX      // x64上使用rdi,rsi传递入参, di:argc si:argvMOVQ   SI, BX      // argvSUBQ $(4*8+7), SP       // 开辟栈空间,用于存放argc, argv和两个局部变量ANDQ  $~15, SP  //与~15& 保障SP 16字节对齐MOVQ   AX, 16(SP) // 存储argc, argvMOVQ  BX, 24(SP) // MOVQ  $runtime·g0(SB), DI // 将g0存储到DI寄存器LEAQ  (-64*1024+104)(SP), BX // 为g0开辟64kb栈空间MOVQ BX, g_stackguard0(DI)  // 将栈底地址保存到g0->stackguard0MOVQ    BX, g_stackguard1(DI)MOVQ   BX, (g_stack+stack_lo)(DI) // 将栈底保存到g0->stack->loMOVQ    SP, (g_stack+stack_hi)(DI) // 将栈顶保存到g0->stack->hi// 下面是g0的结构:
type g struct {// Stack parameters.// stack describes the actual stack memory: [stack.lo, stack.hi).// stackguard0 is the stack pointer compared in the Go stack growth prologue.// It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.// stackguard1 is the stack pointer compared in the C stack growth prologue.// It is stack.lo+StackGuard on g0 and gsignal stacks.// It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).stack       stack   // offset known to runtime/cgostackguard0 uintptr // offset known to liblinkstackguard1 uintptr // offset known to liblink
  • 获取cpu相关信息
 // find out information about the processor we're onMOVL   $0, AX  // 获取CPUID信息CPUIDMOVL   AX, SI // 我本机获取到的cpuid为0xdCMPL  AX, $0 //判断是否获取到了cpuid,成功JE nocpuinfo// 判断cpu的型号,并设置标志,如是否是intel.// 主要是需要确定RDTSC的获取方式,即cpu时间戳计数器CMPL    BX, $0x756E6547  // "Genu" 正式版 oJNE   notintelCMPL    DX, $0x49656E69  // "ineI"JNE notintelCMPL    CX, $0x6C65746E  // "ntel"JNE notintelMOVB    $1, runtime·isIntel(SB) //is inelMOVB   $1, runtime·lfenceBeforeRdtsc(SB) //...
  • 初始化tls,设置m->g0, g0->m,初始化sched信息
    MOVQ _cgo_init(SB), AX // 查看是否有_cgo_init,如果有则需要调用,我们的例子中没有_cgo_initTESTQ AX, AXJZ    needtls //设置tls...LEAQ  runtime·m0+m_tls(SB), DI //获取m0中的tls结构CALL runtime·settls(SB) // 调用sys_linux_amd64.s:658来设置tls, linux上设置tls主要是通过arch_pcrtl实现,设置当前线程的FS信息.// store through it, to make sure it worksget_tls(BX)  //下面代码主要测试tls是否正确工作.MOVQ   $0x123, g(BX)MOVQ   runtime·m0+m_tls(SB), AXCMPQ   AX, $0x123JEQ 2(PC)CALL runtime·abort(SB)...// set the per-goroutine and per-mach "registers"get_tls(BX)LEAQ  runtime·g0(SB), CX // 将g0保存到tls中MOVQ    CX, g(BX) // save g0 to tlsLEAQ runtime·m0(SB), AX // ax -->m0// save m->g0 = g0MOVQ CX, m_g0(AX) //将g0保存到m0中// save m0 to g0->mMOVQ  AX, g_m(CX) // 将m0设置到g0中CLD             // convention is D is always left clearedCALL   runtime·check(SB) //runtime1.go:137 检查一些cas和原子操作工作是否正确MOVL  16(SP), AX      // 获取之前保存到栈中的argc, argvMOVL AX, 0(SP)MOVQ   24(SP), AX      // copy argvMOVQ    AX, 8(SP)CALL   runtime·args(SB) //runtime1.go:61 设置argc, argv到全局变量runtime1.argc, runtime1.argvCALL runtime·osinit(SB) //301 os初始化,根据cpu亲和性获取可用cpu个数,获取大页信息CALL runtime·schedinit(SB) //600 sched初始化,这是一个go函数,先来看一下。
type m struct {g0      *g     // goroutine with scheduling stack...tls           [6]uintptr   // thread-local storage (for x86 extern register)
sched初始化

sched内容比较多,我们详细来看一下:

 _g_ := getg()  // 获取当前的goroutine, 之前已经保存在tls中了,getg就是从tls中获取if raceenabled {_g_.racectx, raceprocctx0 = raceinit()}sched.maxmcount = 10000 //设置最大m线程个数为10000// The world starts stopped.worldStopped()stackinit() // 栈缓存初始化,golang运行时需要分配栈时优先使用缓存mallocinit() // 内存管理初始化fastrandinit() // must run before mcommoninit, 快速随机数初始化mcommoninit(_g_.m, -1) // m初始化并将其放到全局allm链表中cpuinit()       // must run before alginit, cpu初始化alginit()       // maps must not be used before this callmodulesinit()   // provides activeModulestypelinksinit() // uses maps, activeModulesitabsinit()     // uses activeModulessigsave(&_g_.m.sigmask) // 保存当前信号掩码到minitSigmask = _g_.m.sigmaskgoargs()goenvs()parsedebugvars()gcinit()  // 初始化gclock(&sched.lock)sched.lastpoll = uint64(nanotime())procs := ncpuif n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 { //环境变量是否设置了GOMAXPROCSprocs = n}if procresize(procs) != nil {  // 重新调整p的数量.throw("unknown runnable goroutine during bootstrap")}unlock(&sched.lock)// World is effectively started now, as P's can run.worldStarted()...
sched初始化就完成了,主要就是一些全局信息,包括内存,栈缓存,P的个数,gc等.

再回到汇编:

  • 设置g0主协程入口函数runtime.mainPC,调用newproc创建协程
 CALL    runtime·schedinit(SB) //600// create a new goroutine to start programMOVQ   $runtime·mainPC(SB), AX     // 新goroutine的入口函数PUSHQ AX          // 压入栈中下面传递给newprocPUSHQ    $0          // arg sizeCALL runtime·newproc(SB) // 创建新的p,这也是一个go函数,重点分析一下.POPQ  AXPOPQ  AX// start this MCALL   runtime·mstart(SB) //mstart loopCALL    runtime·abort(SB)   // mstart should never returnRET
newproc:
  • 创建主协程并将其放到p的本地队列中,systemstack函数表示在系统栈上执行goroutine的创建操作
 argp := add(unsafe.Pointer(&fn), sys.PtrSize) // 获取argpgp := getg() // 获取当前goroutinepc := getcallerpc()systemstack(func() {  // 调用systemstack来执行newg := newproc1(fn, argp, siz, gp, pc)_p_ := getg().m.p.ptr()runqput(_p_, newg, true)if mainStarted {wakep()}})
systemstack
TEXT runtime·systemstack(SB), NOSPLIT, $0-8MOVQ  fn+0(FP), DI   // DI = fn, 将要执行的函数指针放到rdi.get_tls(CX)  // 获取当前goroutineMOVQ   g(CX), AX   // AX = g, g0MOVQ  g_m(AX), BX // BX = m, m0CMPQ  AX, m_gsignal(BX) //判断当前goroutine是否是用于处理信号的goroutineJEQ noswitchMOVQ    m_g0(BX), DX    // DX = g0CMPQ AX, DX // 判断当前goroutine是否是当前栈的使用者JEQ    noswitch // 如果是则不需要切换栈, 这里明显是,因此直接跳转到noswitchCMPQ   AX, m_curg(BX)JNE   badnoswitch:// already on m stack; tail call the function// Using a tail call here cleans up tracebacks since we won't stop// at an intermediate systemstack.MOVQ  DI, DXMOVQ  0(DI), DI // di是之前传递给systemstack的fnJMP  DI // 执行fn
 systemstack(func() {newg := newproc1(fn, argp, siz, gp, pc) //创建新goroutine执行fn_p_ := getg().m.p.ptr()runqput(_p_, newg, true)if mainStarted {wakep()}})
newproc1:

newproc1的作用是为执行函数分配新的goroutine

func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) *g {_g_ := getg() // 获取当前gif fn == nil {_g_.m.throwing = -1 // do not dump full stacksthrow("go of nil func value")}acquirem() // 锁定m,禁止抢占siz := nargsiz = (siz + 7) &^ 7_p_ := _g_.m.p.ptr() // 获取当前的pnewg := gfget(_p_) // 查找是否有缓存的goroutine,这些goroutine是dead状态的,可以直接使用的.如果本地没有还会从全局查找,最后都没有才会真的申请新的goroutine if newg == nil { // 当前没有可重复使用的缓存gorutinenewg = malg(_StackMin) // 申请新的goroutinecasgstatus(newg, _Gidle, _Gdead) // 初始状态为Gdead.allgadd(newg) // 将newg加入全局allg}/*为newg 准备栈和参数*/totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frametotalSize += -totalSize & (sys.SpAlign - 1)                  // align to spAlignsp := newg.stack.hi - totalSizespArg := spif usesLR {// caller's LR*(*uintptr)(unsafe.Pointer(sp)) = 0prepGoExitFrame(sp)spArg += sys.MinFrameSize}.../*设置newg的sp, pc, g, startpc等 信息*/newg.sched.sp = spnewg.stktopsp = spnewg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same functionnewg.sched.g = guintptr(unsafe.Pointer(newg))gostartcallfn(&newg.sched, fn)newg.gopc = callerpcnewg.ancestors = saveAncestors(callergp)newg.startpc = fn.fncasgstatus(newg, _Gdead, _Grunnable) // 修改newg状态为runnableif _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) // 设置goroutie id._p_.goidcache++...

创建好新的goroutine后,继续:

 systemstack(func() {newg := newproc1(fn, argp, siz, gp, pc) //创建新goroutine执行fn_p_ := getg().m.p.ptr() // 获取新routine的p.runqput(_p_, newg, true) // 将新routine放入运行队列. 首先尝试放入本地队列,如果本地队列满则放入全局队列.本地队列最大256.if mainStarted {wakep()}})
新goroutine创建完成,再启动一个m,这个m目前是主线程,即m0
 CALL    runtime·newproc(SB)POPQ AXPOPQ  AX// start this MCALL   runtime·mstart(SB) //调用mstart启动mCALL    runtime·abort(SB)   // mstart should never returnRET
初始化m0,设置线程id
func minit() {minitSignals() // 初始化信号处理,设置信号处理栈和掩码// Cgo-created threads and the bootstrap m are missing a// procid. We need this for asynchronous preemption and it's// useful in debuggers.getg().m.procid = uint64(gettid()) //设置m的procid,即线程id
}
  • m0,g0都初始化完成后就开始执行主协程,这时通过汇编代码gogo执行主协程
TEXT runtime·gogo(SB), NOSPLIT, $16-8MOVQ    buf+0(FP), BX      // gobufMOVQ    gobuf_g(BX), DXMOVQ 0(DX), CX       // make sure g != nilget_tls(CX)MOVQ   DX, g(CX)MOVQ   gobuf_sp(BX), SP    // restore SPMOVQ   gobuf_ret(BX), AXMOVQ   gobuf_ctxt(BX), DXMOVQ  gobuf_bp(BX), BPMOVQ    $0, gobuf_sp(BX)    // clear to help garbage collectorMOVQ  $0, gobuf_ret(BX)MOVQ   $0, gobuf_ctxt(BX)MOVQ  $0, gobuf_bp(BX)MOVQ    gobuf_pc(BX), BX // 执行之前的runtime.mainPC,即主协程入口JMP   BX
  • 执行主协程入口proc.go: main

主协程会启动sysmon线程进行监控,然后执行package main里我们实现的main函数

...mainStarted = true // 设置main开始标志,这样才允许新协程启动新的M.if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon// For runtime_syscall_doAllThreadsSyscall, we// register sysmon is not ready for the world to be// stopped.atomic.Store(&sched.sysmonStarting, 1)systemstack(func() {newm(sysmon, nil, -1) // 启动sysmon})...fn := main_main // 执行package main中主函数fn()

上面就是一个go程序的启动流程,总结一下:

我们再来分析一下调用go func创建协程的流程

  • go func关键字会被编译器转换为runtime.newproc调用创建新协程
  • 新协程加入当前p的本地队列
  • 如果本地队列已满,则批量将一半的goroutine放入全局队列
  • 之前主协程已经设置了mainStarted标志,因此会调用wakeup尝试唤醒更多空闲的p来工作

golang程序启动流程详解相关推荐

  1. U-Boot启动流程详解

    参考:U-Boot顶层目录链接脚本文件(u-boot.lds)介绍 作者:一只青木呀 发布时间: 2020-10-23 13:52:23 网址:https://blog.csdn.net/weixin ...

  2. 【正点原子Linux连载】第三十二章 U-Boot启动流程详解 -摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0

    1)实验平台:正点原子阿尔法Linux开发板 2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434 2)全套实验源码+手册+视频下载地址: ...

  3. android zygote启动流程,Android zygote启动流程详解

    对zygote的理解 在Android系统中,zygote是一个native进程,是所有应用进程的父进程.而zygote则是Linux系统用户空间的第一个进程--init进程,通过fork的方式创建并 ...

  4. Delta3d框架学习--程序启动过程详解

    一个Delta3d程序启动过程详解 一.初始化一个dtGame::GameApplication的实例,dtGame::GameApplication* app = new dtGame::GameA ...

  5. 【Autosar 启动流程详解】

    Autosar 启动流程详解 1. vLinkGen_Template.lsl 2. BrsHwStartup.c 3.BrsMainStartup.c 4.BrsMain.c 链接文件: 1. vL ...

  6. 【线上沙龙直播报名】App 启动流程详解及其优化

    点击上方"公众号"可以订阅哦 [美团点评技术沙龙Online]是美团点评技术团队推出的线上分享课程,每月2-3期,采用目前最火热的线上直播形式,邀请美团点评技术专家,面向互联网技术 ...

  7. Springboot启动流程详解

    SpringMVC请求流程详解 SpringMVC框架是一个基于请求驱动的Web框架,并且使用了'前端控制器'模型来进行设计,再根据'请求映射规则'分发给相应的页面控制器进行处理. (一)整体流程 每 ...

  8. Android App启动流程详解

    前言:在之前的文章中已经写了apk的打包流程.安装流程,今天就是梳理一下apk系列的最后的流程--app启动流程.经过今天的梳理以后咱们就可以对apk包是怎么编译生成的.apk是怎么被安装到安卓手机的 ...

  9. S5PV210 Uboot开发与移植03:Uboot启动流程详解

    目录 1. start.S解析 1.1 uboot入口分析 1.2 头文件包含 1.2.1 config.h 1.2.2 version.h 1.2.3 asm/proc/domain.h 1.2.4 ...

最新文章

  1. PHP与MySQL连接菜鸟教程_PHP 连接 MySQL - PHP 教程 - 菜鸟学堂-脚本之家
  2. Vue.js slots: 为什么你需要它们?
  3. 用iPhone薅Google羊毛:相册可无限存储高清照片,只要一步设置就搞定
  4. Windows 2003 server 服务器集群实例
  5. 10.1——为什么方法不能用static修饰
  6. php企业号自定义菜单,用php实现微信企业号自定义菜单遇到问题,请大神指点!...
  7. oracle 实现 drop table if exists
  8. 既稳又狂!黑鲨游戏手机2官宣发布时间:3月18日北京见
  9. c++数据结构队列栈尸体_数据结构-栈与队列(二)
  10. jboss7.1.0配置数据库(mysql)
  11. Install R language on Linux RHEL5 or RHEL6
  12. Linux驱动的地址空间和硬件地址空间说明——摘自华清远见嵌入式园地 .
  13. 小飞鱼通达二开 致远OA A8+ 设计工作流实例初体验(图文)
  14. Git:SSL错误导致失败的解决办法
  15. WebServie 基础
  16. 文件夹加密超级大师V16.85官方版
  17. es keyword和text的区别以及联想词实现方案
  18. GitHub生成TOC目录
  19. 奥沙利文第三次夺得了世锦赛冠军
  20. Mysql出现问题:ERROR 1149 ( 42000 (ER_SYNTAX_ERROR)): You have an error in your SQL syntax; check th解决方案

热门文章

  1. java单链表通讯录_[源码和文档分享]C++实现的基于链表的通讯录管理系统
  2. 吐槽一下,英伟达Nvidia官网下载驱动及相关资源,下载完全是打不开的文件
  3. fl水果软件第三方插件FL Studio20.9
  4. FFmpeg从入门到出家(FLV文件结构解析)
  5. 用Python和Tableau对母婴商品销量进行数据分析(附Python源码及Tableau文件)
  6. Revit开发之快捷键相关类
  7. 报价、订货、付款方式、通关、保险、提单、结汇等问题解析
  8. 在JS里面获取浏览器sessionId
  9. 营销qq会话在线聊天代码(也可以匿名)
  10. 网络扫描与网络侦察一