美文网首页
Golang 启动流程-流程分析版

Golang 启动流程-流程分析版

作者: iceinto | 来源:发表于2022-01-07 10:31 被阅读0次

    还记得当时我们学习 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()
    }
    
    

    参考资料

    相关文章

      网友评论

          本文标题:Golang 启动流程-流程分析版

          本文链接:https://www.haomeiwen.com/subject/wjblcrtx.html