golang程序启动流程详解
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程序启动流程详解相关推荐
- U-Boot启动流程详解
参考:U-Boot顶层目录链接脚本文件(u-boot.lds)介绍 作者:一只青木呀 发布时间: 2020-10-23 13:52:23 网址:https://blog.csdn.net/weixin ...
- 【正点原子Linux连载】第三十二章 U-Boot启动流程详解 -摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0
1)实验平台:正点原子阿尔法Linux开发板 2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434 2)全套实验源码+手册+视频下载地址: ...
- android zygote启动流程,Android zygote启动流程详解
对zygote的理解 在Android系统中,zygote是一个native进程,是所有应用进程的父进程.而zygote则是Linux系统用户空间的第一个进程--init进程,通过fork的方式创建并 ...
- Delta3d框架学习--程序启动过程详解
一个Delta3d程序启动过程详解 一.初始化一个dtGame::GameApplication的实例,dtGame::GameApplication* app = new dtGame::GameA ...
- 【Autosar 启动流程详解】
Autosar 启动流程详解 1. vLinkGen_Template.lsl 2. BrsHwStartup.c 3.BrsMainStartup.c 4.BrsMain.c 链接文件: 1. vL ...
- 【线上沙龙直播报名】App 启动流程详解及其优化
点击上方"公众号"可以订阅哦 [美团点评技术沙龙Online]是美团点评技术团队推出的线上分享课程,每月2-3期,采用目前最火热的线上直播形式,邀请美团点评技术专家,面向互联网技术 ...
- Springboot启动流程详解
SpringMVC请求流程详解 SpringMVC框架是一个基于请求驱动的Web框架,并且使用了'前端控制器'模型来进行设计,再根据'请求映射规则'分发给相应的页面控制器进行处理. (一)整体流程 每 ...
- Android App启动流程详解
前言:在之前的文章中已经写了apk的打包流程.安装流程,今天就是梳理一下apk系列的最后的流程--app启动流程.经过今天的梳理以后咱们就可以对apk包是怎么编译生成的.apk是怎么被安装到安卓手机的 ...
- 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 ...
最新文章
- PHP与MySQL连接菜鸟教程_PHP 连接 MySQL - PHP 教程 - 菜鸟学堂-脚本之家
- Vue.js slots: 为什么你需要它们?
- 用iPhone薅Google羊毛:相册可无限存储高清照片,只要一步设置就搞定
- Windows 2003 server 服务器集群实例
- 10.1——为什么方法不能用static修饰
- php企业号自定义菜单,用php实现微信企业号自定义菜单遇到问题,请大神指点!...
- oracle 实现 drop table if exists
- 既稳又狂!黑鲨游戏手机2官宣发布时间:3月18日北京见
- c++数据结构队列栈尸体_数据结构-栈与队列(二)
- jboss7.1.0配置数据库(mysql)
- Install R language on Linux RHEL5 or RHEL6
- Linux驱动的地址空间和硬件地址空间说明——摘自华清远见嵌入式园地 .
- 小飞鱼通达二开 致远OA A8+ 设计工作流实例初体验(图文)
- Git:SSL错误导致失败的解决办法
- WebServie 基础
- 文件夹加密超级大师V16.85官方版
- es keyword和text的区别以及联想词实现方案
- GitHub生成TOC目录
- 奥沙利文第三次夺得了世锦赛冠军
- Mysql出现问题:ERROR 1149 ( 42000 (ER_SYNTAX_ERROR)): You have an error in your SQL syntax; check th解决方案
热门文章
- java单链表通讯录_[源码和文档分享]C++实现的基于链表的通讯录管理系统
- 吐槽一下,英伟达Nvidia官网下载驱动及相关资源,下载完全是打不开的文件
- fl水果软件第三方插件FL Studio20.9
- FFmpeg从入门到出家(FLV文件结构解析)
- 用Python和Tableau对母婴商品销量进行数据分析(附Python源码及Tableau文件)
- Revit开发之快捷键相关类
- 报价、订货、付款方式、通关、保险、提单、结汇等问题解析
- 在JS里面获取浏览器sessionId
- 营销qq会话在线聊天代码(也可以匿名)
- 网络扫描与网络侦察一