Golang 启动流程-流程分析版

还记得当时我们学习 TP(ThinkPHP)时候想搞清楚框架运行流程,然后就进行各种断点和代码阅读,现在就想搞清楚Go程序的启动流程是什么?

启动流程分析

请注意这篇博客中会有很多汇编代码,你需要提前了解一下这方面的知识(Go 汇编器快速入门请参考这里)。让我们开始吧!

当前的Go版本为:go1.11.5 darwin/amd64

package main

import "fmt"

func main() {
    fmt.Println("Hell asm!")
}

然后,编译并链接:

go build

生成可运行程序

objdump -d 可运行程序 > asm.txt 

大概是一个 10w 多行的文件,在这里面寻找

针对不同的平台都有各自的特定的汇编文件,我这里通过rt0_darwin_amd64.s定位到runtime.rt0_go方法。先来看下asm_amd64.s的源码。文件很大,省略部分代码,留下初始化过程的重要步骤。

// 源码 src/runtime/rt0_darwin_amd64.s

__rt0_amd64_darwin:
 1052290:   e9 eb c6 ff ff  jmp -14613 <__rt0_amd64>

// 源码 src/runtime/asm_amd64.s
// _rt0_amd64是大多数amd64系统使用时的常用启动代码

 __rt0_amd64:
 ...
 104e989:   e9 02 00 00 00  jmp 2 <runtime.rt0_go>

runtime.rt0_go:

// 1.查询cpu信息
// 2.如果有cgo,初始化cgo; 调用setg_gcc(g0),然后更新stackguard。
// 3.设置tls 
 104ea42:   e8 09 3c 00 00  callq   15369 <runtime.settls>
// 4.src/runtime/stubs.go
 104ea63:   e8 18 1a 00 00  callq   6680 <runtime.abort>
// 5.做一些运算检测 src/runtime/runtime1.go
 104ea87:   e8 34 71 fe ff  callq   -102092 <runtime.check>
// 6.把二进制文件的绝对路径找出来 src/runtime/runtime1.go
 104ea9d:   e8 ce 6b fe ff  callq   -103474 <runtime.args>
// 7.获取CPU核数与内存页大小 src/runtime/os_darwin.go
 104eaa2:   e8 49 62 fd ff  callq   -171447 <runtime.osinit>
// 8\. 命令行参数、环境变量、gc、栈空间、内存管理、所有P实例、HASH算法等初始化 src/runtime/proc.go
 104eaa7:   e8 34 ae fd ff  callq   -152012 <runtime.schedinit>
// 9.新建一个goroutine,该goroutine绑定runtime.main,放在P的本地队列,等待调度 src/runtime/proc.go
 104eab6:   e8 65 17 fe ff  callq   -125083 <runtime.newproc>
// 10.启动M,开始调度goroutine src/runtime/proc.go
 104eabd:   e8 7e cc fd ff  callq   -144258 <runtime.mstart>

执行流程总结

按顺序总结下runtime.rt0_go里几件重要的事:

检查运行平台的CPU,设置好程序运行需要相关标志。

  • 1.TLS的初始化。
  • 2.runtime.args、runtime.osinit、runtime.schedinit 三个方法做好程序运行需要的各种变量与调度器。
  • 3.runtime.newproc 创建新的goroutine用于绑定用户写的main方法。
  • 4.runtime.mstart 开始goroutine的调度。

具体源码

下面接着针对上面几个runtime函数,粗略探索下干了什么事情。我们也只看一层代码,有兴趣的同学可以顺着这个顺序深入看下

runtime.args

就是把二进制文件的绝对路径找出来,并存在os.executablePath里。

func args(c int32, v **byte) {
    argc = c
    argv = v
    sysargs(c, v)
}

//go:linkname executablePath os.executablePath
var executablePath string

func sysargs(argc int32, argv **byte) {
    // skip over argv, envv and the first string will be the path
    n := argc + 1
    for argv_index(argv, n) != nil {
        n++
    }
    executablePath = gostringnocopy(argv_index(argv, n+1))

    // strip "executable_path=" prefix if available, it's added after OS X 10.11.
    const prefix = "executable_path="
    if len(executablePath) > len(prefix) && executablePath[:len(prefix)] == prefix {
        executablePath = executablePath[len(prefix):]
    }
}

runtime.osinit

获取CPU核数与内存页大小。按照本文的测试工程:

// BSD interface for threading.
func osinit() {
    // pthread_create delayed until end of goenvs so that we
    // can look at the environment first.

    ncpu = getncpu()
    physPageSize = getPageSize()
}

const (
    _CTL_HW      = 6
    _HW_NCPU     = 3
    _HW_PAGESIZE = 7
)

func getncpu() int32 {
    // Use sysctl to fetch hw.ncpu.
    mib := [2]uint32{_CTL_HW, _HW_NCPU}
    out := uint32(0)
    nout := unsafe.Sizeof(out)
    ret := sysctl(&mib[0], 2, (*byte)(unsafe.Pointer(&out)), &nout, nil, 0)
    if ret >= 0 && int32(out) > 0 {
        return int32(out)
    }
    return 1
}

func getPageSize() uintptr {
    // Use sysctl to fetch hw.pagesize.
    mib := [2]uint32{_CTL_HW, _HW_PAGESIZE}
    out := uint32(0)
    nout := unsafe.Sizeof(out)
    ret := sysctl(&mib[0], 2, (*byte)(unsafe.Pointer(&out)), &nout, nil, 0)
    if ret >= 0 && int32(out) > 0 {
        return uintptr(out)
    }
    return 0
}

runtime.schedinit

初始化程序运行需要环境

// The bootstrap sequence is:
//
//  call osinit
//  call schedinit
//  make & queue new G
//  call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {
    // raceinit must be the first call to race detector.
    // In particular, it must be done before mallocinit below calls racemapshadow.
    // 获取g实例
    _g_ := getg()
    if raceenabled {
        _g_.racectx, raceprocctx0 = raceinit()
    }

    sched.maxmcount = 10000 // 设置全局线程数上限

    tracebackinit() // 初始化一系列函数所在的PC计数器,用于traceback
    moduledataverify() // 验证链接器符号的正确性
    stackinit() // 栈的初始化
    mallocinit() // 内存分配器初始化
    mcommoninit(_g_.m)
    cpuinit()       // must run before alginit
    alginit()       // maps must not be used before this call
    modulesinit()   // provides activeModules
    typelinksinit() // uses maps, activeModules
    itabsinit()     // uses activeModules

    msigsave(_g_.m)
    initSigmask = _g_.m.sigmask

    goargs() // 获取命令行参数
    goenvs() // 获取所有的环境变量
    parsedebugvars() // GODEBUG 设置
    gcinit() // gc初始化

    sched.lastpoll = uint64(nanotime())
    procs := ncpu // P个数检查
    if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 { // 设置 GOMAXPROCS 参数
        procs = n
    }
    if procresize(procs) != nil { // 所有P的初始化
        throw("unknown runnable goroutine during bootstrap")
    }

    // For cgocheck > 1, we turn on the write barrier at all times
    // and check all pointer writes. We can't do this until after
    // procresize because the write barrier needs a P.
    if debug.cgocheck > 1 {
        writeBarrier.cgo = true
        writeBarrier.enabled = true
        for _, p := range allp {
            p.wbBuf.reset()
        }
    }

    if buildVersion == "" {
        // Condition should never trigger. This code just serves
        // to ensure runtime·buildVersion is kept in the resulting binary.
        buildVersion = "unknown"
    }
}

runtime.newproc

newproc() 比较简单,只是获取参数的起始地址与相关寄存器。真正干活的是newproc1()。

runtime.newproc1()

newproc1() 就比较长了,这儿概括下它做了的事情:

  • 从TLS拿到当前运行的G实例,并且使绑定到当前线程的M实例不可抢占。
  • 从M实例上取到P实例,如果P实例本地上有free goroutine就拿过去,没有就到全局调度器那儿偷一些过来。这两个地方都没有,就按照最低栈大小2K new一个G实例(即goroutine)。
  • 然后设置好G实例上的各种寄存器的信息,SP、PC等。
  • 将G实例的状态变更为Grunnable,放到P实例的本地可运行队列里等待调度执行,若队列满了,就把一半的G移到全局调度器下。
  • 释放M实例的不可抢占状态。返回新的G实例。

如果是程序刚启动,经由runtime.rt0_go调用newproc1时,实质干的事情就是创建一个G,把runtime.main(也包含main.main)放进去。在执行mstart时,触发调度。所以main实际是在一个新的G里运行的,而不是g0。

// Create a new g running fn with siz bytes of arguments.
// Put it on the queue of g's waiting to run.
// The compiler turns a go statement into a call to this.
// Cannot split the stack because it assumes that the arguments
// are available sequentially after &fn; they would not be
// copied if a stack split occurred.
//go:nosplit
func newproc(siz int32, fn *funcval) {
    argp := add(unsafe.Pointer(&fn), sys.PtrSize)
    gp := getg()
    pc := getcallerpc()
    systemstack(func() {
        newproc1(fn, (*uint8)(argp), siz, gp, pc)
    })
}

// Create a new g running fn with narg bytes of arguments starting
// at argp. callerpc is the address of the go statement that created
// this. The new g is put on the queue of g's waiting to run.
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr)

runtime.mstart

启动M


// Called to start an M.
//
// This must not split the stack because we may not even have stack
// bounds set up yet.
//
// May run during STW (because it doesn't have a P yet), so write
// barriers are not allowed.
//
//go:nosplit
//go:nowritebarrierrec
func mstart() {
    _g_ := getg()

    osStack := _g_.stack.lo == 0
    if osStack {
        // Initialize stack bounds from system stack.
        // Cgo may have left stack size in stack.hi.
        // minit may update the stack bounds.
        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
    }
    // Initialize stack guards so that we can start calling
    // both Go and C functions with stack growth prologues.
    _g_.stackguard0 = _g_.stack.lo + _StackGuard
    _g_.stackguard1 = _g_.stackguard0
    mstart1()

    // Exit this thread.
    if GOOS == "windows" || GOOS == "solaris" || GOOS == "plan9" || GOOS == "darwin" {
        // Window, Solaris, Darwin and Plan 9 always system-allocate
        // the stack, but put it in _g_.stack before mstart,
        // so the logic above hasn't set osStack yet.
        osStack = true
    }
    mexit(osStack)
}

func mstart1() {
    _g_ := getg()

    if _g_ != _g_.m.g0 {
        throw("bad runtime·mstart")
    }

    // Record the caller for use as the top of stack in mcall and
    // for terminating the thread.
    // We're never coming back to mstart1 after we call schedule,
    // so other calls can reuse the current frame.
    save(getcallerpc(), getcallersp())
    asminit()
    minit() // 初始化新的 M

    // Install signal handlers; after minit so that minit can
    // prepare the thread to be able to handle the signals.
    if _g_.m == &m0 {
        mstartm0()
    }

    if fn := _g_.m.mstartfn; fn != nil {
        fn()
    }

    if _g_.m.helpgc != 0 {
        _g_.m.helpgc = 0
        stopm()
    } else if _g_.m != &m0 {
        acquirep(_g_.m.nextp.ptr())
        _g_.m.nextp = 0
    }
    schedule()
}

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容