go 应用启动过程分析

1. 启动过程函数调用流程

rt0_linux_amd64.s -->_rt0_amd64 --> rt0_go-->runtime·settls -->runtime·check-->runtime·args-->runtime·osinit-->runtime·schedinit-->runtime·newproc-->runtime·mstart

2. asm_amd64.s. runtime·rt0_go 关键部分源码分析

(版本:go 1.17.3 ,第191 行)

ok:
    // set the per-goroutine and per-mach "registers"
    get_tls(BX)
    LEAQ    runtime·g0(SB), CX
    MOVQ    CX, g(BX)
    LEAQ    runtime·m0(SB), AX

     // 实现m0与g0 相关绑定
    // save m->g0 = g0
    MOVQ    CX, m_g0(AX)
    // save m0 to g0->m
    MOVQ    AX, g_m(CX)

    CLD             // convention is D is always left cleared
    CALL    runtime·check(SB)

    MOVL    16(SP), AX      // copy argc
    MOVL    AX, 0(SP)
    MOVQ    24(SP), AX      // copy argv
    MOVQ    AX, 8(SP)
// check 函数检查了各种类型以及类型转换是否有问题,位于 runtime/runtime1.go#137 中
    CALL    runtime·args(SB)
// osinit 函数用来初始化 cpu 数量(CPU 核心数与内存物理页大小),函数位于 runtime/os_linux.go#301
    CALL    runtime·osinit(SB)
// schedinit 函数用来初始化调度器(进行各种运行时组件初始化工作,这包括我们的调度器与内存分配器、回收器的初始化),函数位于 runtime/proc.go#654
    CALL    runtime·schedinit(SB)

    // create a new goroutine to start program

//runtime.mainPC在数据段中被定义为runtime.main的入口地址
    MOVQ    $runtime·mainPC(SB), AX     // entry
    PUSHQ   AX //  runtime.main 作为 newproc 的第二个参数入栈
    PUSHQ   $0          // arg size // newproc 的第一个参数入栈,该参数表示runtime.main函数需要的参数大小,runtime.main没有参数,所以这里是0

    // newproc 创建一个新的 goroutine 并放置到等待队列里,该 goroutine 会执行runtime.main 函数, 函数位于 runtime/proc.go#4250
//负责根据主 goroutine (即main)入口地址创建可被运行时调度的执行单元
    CALL    runtime·newproc(SB)
    POPQ    AX
    POPQ    AX

    // start this M
    // mstart 函数会启动主线程进入调度循环,然后运行刚刚创建的 goroutine,mstart 会阻塞住,除非函数退出,mstart 函数位于 runtime/proc.go#1328
    CALL    runtime·mstart(SB)

    CALL    runtime·abort(SB)   // mstart should never return
    RET

    // Prevent dead-code elimination of debugCallV2, which is
    // intended to be called by debuggers.
    MOVQ    $runtime·debugCallV2(SB), AX
    RET

runtime.mainPC

DATA    runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOBL   runtime·mainPC(SB),RODATA,$8

3. schedinit

// src/runtime/proc.go
func schedinit() {
    _g_ := getg()
    (...)

    // 栈、内存分配器、调度器相关初始化
    sched.maxmcount = 10000 // 限制最大系统线程数量
    stackinit()         // 初始化执行栈
    mallocinit()        // 初始化内存分配器
    mcommoninit(_g_.m)  // 初始化当前系统线程
    (...)

    gcinit()    // 垃圾回收器初始化
    (...)

    // 创建 P
    // 通过 CPU 核心数和 GOMAXPROCS 环境变量确定 P 的数量
    procs := ncpu
    if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
        procs = n
    }
    procresize(procs)
    (...)
}

stackinit()goroutine 执行栈初始化
mallocinit()内存分配器初始化
mcommoninit()系统线程的部分初始化工作
gcinit()垃圾回收器初始化
procresize()根据 CPU 核心数,初始化系统线程的本地缓存

4. mstart

有mstart 执行 runtime .main()

// The go:noinline is to guarantee the getcallerpc/getcallersp below are safe,
// so that we can set up g0.sched to return to the call of mstart1 above.
//go:noinline
func mstart1() {
    _g_ := getg()

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

    // Set up m.g0.sched as a label returning to just
    // after the mstart1 call in mstart0 above, for use by goexit0 and mcall.
    // We're never coming back to mstart1 after we call schedule,
    // so other calls can reuse the current frame.
    // And goexit0 does a gogo that needs to return from mstart1
    // and let mstart0 exit the thread.
    _g_.sched.g = guintptr(unsafe.Pointer(_g_))
    _g_.sched.pc = getcallerpc()
    _g_.sched.sp = getcallersp()

    asminit()
    minit()

    // 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 != &m0 {
        acquirep(_g_.m.nextp.ptr())
        _g_.m.nextp = 0
    }
    schedule()
}

5. runtime.main

(proc.go 145行)

// The main goroutine.
func main() {
    g := getg()
       ....
    doInit(&main_inittask)

    // Disable init tracing after main init done to avoid overhead
    // of collecting statistics in malloc and newproc
    inittrace.active = false

    close(main_init_done)

    needUnlock = false
    unlockOSThread()

    if isarchive || islibrary {
        // A program compiled with -buildmode=c-archive or c-shared
        // has a main, but it is not executed.
        return
    }
    //main.mian函数调用 (应用程序的main函数)
    fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
    fn()
    if raceenabled {
        racefini()
    }

    // Make racy client program work: if panicking on
    // another goroutine at the same time as main returns,
    // let the other goroutine finish printing the panic trace.
    // Once it does, it will exit. See issues 3934 and 20018.
    if atomic.Load(&runningPanicDefers) != 0 {
        // Running deferred functions should not take long.
        for c := 0; c < 1000; c++ {
            if atomic.Load(&runningPanicDefers) == 0 {
                break
            }
            Gosched()
        }
    }
    if atomic.Load(&panicking) != 0 {
        gopark(nil, nil, waitReasonPanicWait, traceEvGoStop, 1)
    }

    exit(0)
    for {
        var x *int32
        *x = 0
    }
}

参考:
https://www.jianshu.com/p/59b2e46c8708
https://www.kancloud.cn/cfun_good/golang/2033482

你可能感兴趣的:(go 应用启动过程分析)