Golang 程序启动原理详解

一.编译

go源代码首先要通过 go build 编译可执行文件,然后去机器上直接执行的,在 linux 平台上为 ELF 格式的可执行文件,linux 能直接执行这个文件,而编译阶段会经过编译器、汇编器、链接器三个过程最终生成可执行文件

  • 编译器:*.go 源码通过 go 编译器生成为 *.s 的 plan9 汇编代码,Go 编译器入口是 compile/internal/gc/main.go 文件的 main 函数
  • 汇编器:通过 go 汇编器将编译器生成的 *.s 汇编语言转换为机器代码,并写出最终的目标程序 *.o 文件,src/cmd/internal/obj 包实现了go汇编器
  • 链接器:汇编器生成的一个个 *.o 目标文件通过链接处理得到最终的可执行程序,src/cmd/link/internal/ld 包实现了链接器

查看 ELF 二进制文件结构:

        可以通过 readelf 命令查看 ELF 二进制文件的结构,可以看到二进制文件中代码区数据区的内容,全局变量保存在数据区,函数保存在代码区

$ readelf -s main | grep runtime.g01765: 000000000054b3a0   376 OBJECT  GLOBAL DEFAULT   11 runtime.g0// _cgo_init 为全局变量
$ readelf -s main | grep -i _cgo_init2159: 000000000054aa88     8 OBJECT  GLOBAL DEFAULT   11 _cgo_init

二.运行

经上述几个步骤生成可执行文件后,二进制文件在被操作系统加载起来运行时会经过如下几个阶段:

  1. 从磁盘上把可执行程序读入内存
  2. 创建进程和主线程
  3. 为主线程分配栈空间
  4. 把由用户在命令行输入的参数拷贝到主线程的栈;
  5. 把主线程放入操作系统的运行队列等待被调度执起来运行

三.程序启动流程分析

1.通过gdb调试分析程序启动流程

通过一个简单的go程序单步调试来分析其启动过程的流程

main.go

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

编译该程序并使用 gdb 进行调试,使用gdb调试时首先在程序入口处设置一个断点,然后进行单步调试即可看到该程序启动过程中的代码执行流程

$ go build -gcflags "-N -l" -o main main.go$ gdb ./main(gdb) info files
Symbols from "/home/gosoon/main".
Local exec file:`/home/gosoon/main', file type elf64-x86-64.Entry point: 0x4658600x0000000000401000 - 0x0000000000497893 is .text0x0000000000498000 - 0x00000000004dbb65 is .rodata0x00000000004dbd00 - 0x00000000004dc42c is .typelink0x00000000004dc440 - 0x00000000004dc490 is .itablink0x00000000004dc490 - 0x00000000004dc490 is .gosymtab0x00000000004dc4a0 - 0x0000000000534b90 is .gopclntab0x0000000000535000 - 0x0000000000535020 is .go.buildinfo0x0000000000535020 - 0x00000000005432e4 is .noptrdata0x0000000000543300 - 0x000000000054aa70 is .data0x000000000054aa80 - 0x00000000005781f0 is .bss0x0000000000578200 - 0x000000000057d510 is .noptrbss0x0000000000400f9c - 0x0000000000401000 is .note.go.buildid
(gdb) b *0x465860
Breakpoint 1 at 0x465860: file /home/gosoon/golang/go/src/runtime/rt0_linux_amd64.s, line 8.
(gdb) r
Starting program: /home/gaofeilei/./mainBreakpoint 1, _rt0_amd64_linux () at /home/gaofeilei/golang/go/src/runtime/rt0_linux_amd64.s:8
8       JMP _rt0_amd64(SB)
(gdb) n
_rt0_amd64 () at /home/gaofeilei/golang/go/src/runtime/asm_amd64.s:15
15      MOVQ    0(SP), DI   // argc
(gdb) n
16      LEAQ    8(SP), SI   // argv
(gdb) n
17      JMP runtime·rt0_go(SB)
(gdb) n
runtime.rt0_go () at /home/gaofeilei/golang/go/src/runtime/asm_amd64.s:91
91      MOVQ    DI, AX      // argc
......
231     CALL    runtime·mstart(SB)
(gdb) n
hello world
[Inferior 1 (process 39563) exited normally]

通过单步调试可以看到程序入口函数在 runtime/rt0_linux_amd64.s 文件中的第 8 行,最终会执行 CALL runtime·mstart(SB) 指令后输出 “hello world” ,然后程序就退出了,启动流程中的函数调用如下所示:

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

这里解释一下文件名:

  • rt0 : runtime0 表示起始运行时
  • linux: 操作系统 我这里是linux系统
  • amd64 : 操作系统架构,对应(GOHOSTARCH)
  • 启动文件位于GOROOT/src/runtime目录下,那同理可以看到其他系统的启动文件

2.golang 启动流程分析 

看一下这个启动文件干了嘛

src/runtime/rt0_linux_amd64.s

#include "textflag.h"TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8JMP _rt0_amd64(SB)TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0JMP _rt0_amd64_lib(SB)

首先执行的第8行即 JMP _rt0_amd64,此处在 amd64 平台下运行,_rt0_amd64 函数所在的文件为 src/runtime/asm_amd64.s

TEXT _rt0_amd64(SB),NOSPLIT,$-8// 处理 argc 和 argv 参数,argc 是指命令行输入参数的个数,argv 存储了所有的命令行参数MOVQ    0(SP), DI   // argc// argv 为指针类型LEAQ    8(SP), SI   // argvJMP runtime·rt0_go(SB)

 _rt0_amd64 函数中将 argc 和 argv 两个参数保存到 DI 和 SI 寄存器跳转到了rt0_go 函数rt0_go 函数的主要作用如下:     

  • 1、将 argc、argv 参数拷贝到主线程栈上;
  • 2、初始化全局变量 g0,为 g0 在主线程栈上分配大约 64K 栈空间,并设置 g0 的stackguard0,stackguard1,stack 三个字段;
  • 3、执行 CPUID 指令,探测 CPU 信息
  • 4、执行 nocpuinfo 代码块判断是否需要初始化 cgo
  • 5、执行 needtls 代码块,初始化 tls 和 m0;
  • 6、执行 ok 代码块,首先将 m0 和 g0 绑定,然后:
    • 调用 runtime.args 函数处理进程参数和环境变量
    • 调用 runtime.osinit 函数初始化 cpu 数量
    • 调用 runtime.schedinit 初始化调度器
    • 调用 runtime.newproc 创建第一个 goroutine 执行 main 函数
    • 调用 runtime.mstart 启动主线程,主线程会执行第一个 goroutine 来运行 main 函数,此处会阻塞住直到进程退出

参数拷贝,初始化全局变量代码大致如下:

TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0// 处理命令行参数的代码MOVQ    DI, AX      // AX = argcMOVQ    SI, BX      // BX = argv// 将栈扩大39字节,此处为什么扩大39字节暂时还没有搞清楚SUBQ    $(4*8+7), SPANDQ    $~15, SP    // 调整为 16 字节对齐MOVQ    AX, 16(SP)  //argc放在SP + 16字节处MOVQ    BX, 24(SP)  //argv放在SP + 24字节处// 开始初始化 g0,runtime·g0 是一个全局变量,变量在 src/runtime/proc.go 中定义,全局变量会保存在进程内存空间的数据区,下文会介绍查看 elf 二进制文件中的代码数据和全局变量的方法// g0 的栈是从进程栈内存区进行分配的,g0 占用了大约 64k 大小。MOVQ    $runtime·g0(SB), DI    // g0 的地址放入 DI 寄存器LEAQ    (-64*1024+104)(SP), BX // BX = SP - 64*1024 + 104// 开始初始化 g0 对象的 stackguard0,stackguard1,stack 这三个字段MOVQ    BX, g_stackguard0(DI) // g0.stackguard0 = SP - 64*1024 + 104MOVQ    BX, g_stackguard1(DI) // g0.stackguard1 = SP - 64*1024 + 104MOVQ    BX, (g_stack+stack_lo)(DI) // g0.stack.lo = SP - 64*1024 + 104MOVQ    SP, (g_stack+stack_hi)(DI) // g0.stack.hi = SP

rt0_go 可分为两个部分,第一部分是系统参数获取和运行时检查,第二部分是go程序启动的核心, 这里就是整个go代码的起点,,这里只详细介绍第二部分,执行完以上指令后,进程内存空间布局如下所示:

然后开始执行获取 cpu 信息的指令以及与 cgo 初始化相关的指定:

    // 执行CPUID指令,尝试获取CPU信息,探测 CPU 和 指令集的代码MOVL    $0, AXCPUIDMOVL    AX, SICMPL    AX, $0JE  nocpuinfo// Figure out how to serialize RDTSC.// On Intel processors LFENCE is enough. AMD requires MFENCE.// Don't know about the rest, so let's do MFENCE.CMPL    BX, $0x756E6547  // "Genu"JNE notintelCMPL    DX, $0x49656E69  // "ineI"JNE notintelCMPL    CX, $0x6C65746E  // "ntel"JNE notintelMOVB    $1, runtime·isIntel(SB)MOVB    $1, runtime·lfenceBeforeRdtsc(SB)
notintel:// Load EAX=1 cpuid flagsMOVL    $1, AXCPUIDMOVL    AX, runtime·processorVersionInfo(SB)nocpuinfo:// cgo 初始化相关,_cgo_init 为全局变量MOVQ    _cgo_init(SB), AX// 检查 AX 是否为 0TESTQ   AX, AX// 跳转到 needtlsJZ  needtls// arg 1: g0, already in DIMOVQ    $setg_gcc<>(SB), SI // arg 2: setg_gccCALL    AX// 如果开启了 CGO 特性,则会修改 g0 的部分字段MOVQ    $runtime·g0(SB), CXMOVQ    (g_stack+stack_lo)(CX), AXADDQ    $const__StackGuard, AXMOVQ    AX, g_stackguard0(CX)MOVQ    AX, g_stackguard1(CX)

下面执行 needtls 代码块,初始化 tls 和 m0,tls 为线程本地存储,在 golang 程序运行过程中,每个 m 都需要和一个工作线程关联,那么工作线程如何知道其关联的 m,此时就会用到线程本地存储,线程本地存储就是线程私有的全局变量,通过线程本地存储可以为每个线程初始化一个私有的全局变量 m,然后就可以在每个工作线程中都使用相同的全局变量名来访问不同的 m 结构体对象。后面会分析到其实每个工作线程 m 在刚刚被创建出来进入调度循环之前就利用线程本地存储机制为该工作线程实现了一个指向 m 结构体实例对象的私有全局变量。

在后面代码分析中,会经常看到调用 getg 函数,getg 函数会从线程本地存储中获取当前正在运行的 g,这里获取出来的 m 关联的 g0。

tls 地址会写到 m0 中,而 m0 会和 g0 绑定,所以可以直接从 tls 中获取到 g0

// 下面开始初始化tls(thread local storage,线程本地存储),设置 m0 为线程私有变量,将 m0 绑定到主线程
needtls:LEAQ    runtime·m0+m_tls(SB), DI  //DI = &m0.tls,取m0的tls成员的地址到DI寄存器// 调用 runtime·settls 函数设置线程本地存储,runtime·settls 函数的参数在 DI 寄存器中// 在 runtime·settls 函数中将 m0.tls[1] 的地址设置为 tls 的地址// runtime·settls 函数在 runtime/sys_linux_amd64.s#599CALL    runtime·settls(SB)// 此处是在验证本地存储是否可以正常工作,确保值正确写入了 m0.tls,// 如果有问题则 abort 退出程序// get_tls 是宏,位于 runtime/go_tls.hget_tls(BX)                      // 将 tls 的地址放入 BX 中,即 BX = &m0.tls[1]MOVQ    $0x123, g(BX)  // BX = 0x123,即 m0.tls[0] = 0x123MOVQ    runtime·m0+m_tls(SB), AX    // AX = m0.tls[0]CMPQ    AX, $0x123JEQ 2(PC)                                   // 如果相等则向后跳转两条指令即到 ok 代码块CALL    runtime·abort(SB)   // 使用 INT 指令执行中断

然后继续执行ok 代码块,主要逻辑为:

  • 将 m0 和 g0 进行绑定,启动主线程
  • 调用 runtime.osinit 函数用来初始化 cpu 数量,调度器初始化时需要知道当前系统有多少个CPU核
  • 调用 runtime.schedinit 函数会初始化m0和p对象,还设置了全局变量 sched 的 maxmcount 成员为10000,限制最多可以创建10000个操作系统线程出来工作
  • 调用 runtime.newproc 为main函数创建 goroutine
  • 调用 runtime.mstart 启动主线程,执行 main 函数
// 首先将 g0 地址保存在 tls 中,即 m0.tls[0] = &g0,然后将 m0 和 g0 绑定
// 即 m0.g0 = g0, g0.m = m0
ok:get_tls(BX)                             // 获取tls地址到BX寄存器,即 BX = m0.tls[0]LEAQ    runtime·g0(SB), CX  // CX = &g0MOVQ    CX, g(BX)                 // m0.tls[0]=&g0LEAQ    runtime·m0(SB), AX  // AX = &m0MOVQ    CX, m_g0(AX)  // m0.g0 = g0MOVQ    AX, g_m(CX)   // g0.m = m0CLD             // convention is D is always left cleared// check 函数检查了各种类型以及类型转换是否有问题,位于 runtime/runtime1.go#137 中CALL    runtime·check(SB)// 将 argc 和 argv 移动到 SP+0 和 SP+8 的位置// 此处是为了将 argc 和 argv 作为 runtime·args 函数的参数MOVL    16(SP), AXMOVL    AX, 0(SP)MOVQ    24(SP), AXMOVQ    AX, 8(SP)// args 函数会从栈中读取参数和环境变量等进行处理// args 函数位于 runtime/runtime1.go#61CALL    runtime·args(SB)// osinit 函数用来初始化 cpu 数量,函数位于 runtime/os_linux.go#301CALL    runtime·osinit(SB)// schedinit 函数用来初始化调度器,函数位于 runtime/proc.go#654CALL    runtime·schedinit(SB)// 创建第一个 goroutine 执行 runtime.main 函数。获取 runtime.main 的地址,调用 newproc 创建 gMOVQ    $runtime·mainPC(SB), AXPUSHQ   AX            // runtime.main 作为 newproc 的第二个参数入栈PUSHQ   $0            // newproc 的第一个参数入栈,该参数表示runtime.main函数需要的参数大小,runtime.main没有参数,所以这里是0// newproc 创建一个新的 goroutine 并放置到等待队列里,该 goroutine 会执行runtime.main 函数, 函数位于 runtime/proc.go#4250CALL    runtime·newproc(SB)// 弹出栈顶的数据POPQ    AXPOPQ    AX// mstart 函数会启动主线程进入调度循环,然后运行刚刚创建的 goroutine,mstart 会阻塞住,除非函数退出,mstart 函数位于 runtime/proc.go#1328CALL    runtime·mstart(SB)CALL    runtime·abort(SB)   // mstart should never returnRET// Prevent dead-code elimination of debugCallV2, which is// intended to be called by debuggers.MOVQ    $runtime·debugCallV2<ABIInternal>(SB), AXRET

此时进程内存空间布局如下所示:

总体启动流程大致如下: 

 

 3.runtime中某些核心方法讲解

上面的启动流程中运用了runtime包里面的一下方法,这里拿出来分析一下

check

check函数位于runtime的runtime1.go中,主要是检查一些标识

args

args 函数同样runtime的runtime1.go

var (argc int32   //参数个数argv **byte  //入参
)func args(c int32, v **byte) { //初始全局变量 argc,argv 并调用sysargsargc = cargv = vsysargs(c, v)
}var executablePath string  //获取执行程序路径 复制到全局变量executablePath
func sysargs(argc int32, argv **byte) {。。。
}

schedinit

schedinit位于runtime的proc.go文件中,它的功能是进行各种运行时额所有核心组件初始化工作,这包括调度器内存分配器回收器的初始化 

func schedinit() {//lockInit  锁相关的初始化 暂时忽略//获取当前的g 之前已经保存在tls中了,getg就是从tls中获取//大致的关系是fs -> tls[1] -> g() -> tls[0] -> g0 -> g0.m0 = &m0 -> m0.g0 = &g0//从fs段寄存器出发 找到 m0.tls[1] ,地址-8后得到 tls[0] 而 tls[0]正好指向g0获取到_g_ := getg()if raceenabled { //如果启用了race 则进行raceinit的初始化,默认false_g_.racectx, raceprocctx0 = raceinit()}//默认m(线程)的最大值是10000个,面试经常问sched.maxmcount = 10000// The world starts stopped.worldStopped() //用于lock rank,可忽略moduledataverify() //验证链接器符号,可忽略//初始栈,就是初始 stackLarge,stackpool 两个全局变量。对这哥俩感兴趣的可以看上篇博文 内存管理//注意这里还没有给栈分配内存stackinit()//内存分配初始化。就是计算内存大小,初始化mheap,mcache0 等操作mallocinit()//初始化CPU相关的参数//读取环境变量GODEBUG,并调用 internal/cpu.Initializecpuinit()      // must run before alginit//map使用必须调用,算法相关alginit()      // maps, hash, fastrand must not be used before this call//随机数相关fastrandinit() // must run before mcommoninit//初始化m,调用atomicstorep将m0放入全局变量allm//并且将allm挂到m的alllink上mcommoninit(_g_.m, -1)//模块初始化,将所有模块的moduledata的gc标志初始化,并将moduledata放入全局变量modulesSlice中modulesinit()   // provides activeModules//type这种别名相关的,消除重复映射typelinksinit() // uses maps, activeModules//接口相关,将每个模块的itab 放入全局变量itabTable.entries中,方便动态派发//itab粗糙的理解 = 接口类型+具体实现类型,方便动态类型的查找。itabsinit()     // uses activeModules//初始化methodValueCallFrameObjs栈对象 stkobjinit()    // must run before GC starts//将当前线程信号保存到m.sigmask中,一并设置到全局变量initSigmasksigsave(&_g_.m.sigmask)initSigmask = _g_.m.sigmask...goargs() //入参全局变量argslice初始化goenvs() //环境全局变量envs初始化parsedebugvars() //初始化debug包变量,并根据环境变量GODEBUG解析dbgvars的一系列配置gcinit()  //gc相关lock(&sched.lock)//sched.lastpoll 设置调度器初始化轮训时间sched.lastpoll = uint64(nanotime())//设置当前cpu个数,在 osinit() 函数里已经获取到。如果环境变量GOMAXPROCS设置了CPU个数,直使用设置个数。procs := ncpuif n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {procs = n}//调整cpu 数量if procresize(procs) != nil {throw("unknown runnable goroutine during bootstrap")}unlock(&sched.lock)...// World is effectively started now, as P's can run.worldStarted()}

newproc

在runtime的proc.go文件中,负责根据主 goroutine(即 main)入口地址创建可被运行时调度的执行单元,这里的main还不是用户的main函数,是 runtime.main

//创建一个新的g,绑定main函数,并且加入到队列中等待执行
func newproc(fn *funcval) {gp := getg()         //获取当前gpc := getcallerpc()  //获取程序计数器systemstack(func() {//创建新的g并绑定fn,也就是mainnewg := newproc1(fn, gp, pc)_p_ := getg().m.p.ptr()//推入p的队列中runqput(_p_, newg, true)//是否启动M开始执行//默认为false,在下面的main函数中设置mainStarted=true,所以第一次到这里是不会执行的。if mainStarted {wakep()}})
}
  • 这里相当于将runtime·main推到p的队列中
  • golang中go statement入口就是newproc,即go func(){}实际上newproc(func(){})的调用

main函数,同样在这个文件里:

func main() {g := getg()g.m.g0.racectx = 0//设置栈的最大值,按处理器位数,64位对应1G,32位对应250MBif goarch.PtrSize == 8 {maxstacksize = 1000000000} else {maxstacksize = 250000000}maxstackceiling = 2 * maxstacksizemainStarted = true //允许上面的newproc函数创建Ms...//执行每runtime的initdoInit(&runtime_inittask) // Must be before defer....gcenable() //开启gc//下面一大坨都是cgo相关main_init_done = make(chan bool)if iscgo {...}doInit(&main_inittask) //执行package main的init...fn := main_main // 执行package main中主函数fn()...exit(0)  //退出进程
}

这里有一个doInit函数:它会执行每个模块中的init函数,init函数对应结构体如下:

type initTask struct {state uintptr //状态标识 0:未执行, 1:执行中, 2:已完成 ndeps uintptr //当前模块的其他依赖nfns  uintptr //模块里面的几个init函数
}

看这个结构就可以指定,所有的init函数会根据模块的依赖关系形成一个有向无环图,执行的过程就是对这个图进行深度优先遍历,遍历函数doInit如下:

func doInit(t *initTask) {switch t.state {case 2: // 完成退出returncase 1: // 异常panicthrow("recursive call during initialization - linker skew")default: // 遍历执行t.state = 1 // 先设置状态到执行中//向下递归for i := uintptr(0); i < t.ndeps; i++ {p := add(unsafe.Pointer(t), (3+i)*goarch.PtrSize)t2 := *(**initTask)(p)doInit(t2)}//当前模块没init则设置状态到完成,返回if t.nfns == 0 {t.state = 2 // initialization donereturn}... //执行当前模块的init,完成后设置状态2 返回t.state = 2}
}

mstart

mstart函数汇编中指向mstart0,proc.go文件里,它的功能是开始启动调度器的调度循环,以此来启动线程,启动调度系统. 执行队列中入口方法是runtime.main 的 G

TEXT runtime·rt0_go(SB),NOSPLIT,$0(...)// 调度器初始化CALL    runtime·schedinit(SB)// 创建一个新的 goroutine 来启动程序MOVQ    $runtime·mainPC(SB), AXPUSHQ   AXPUSHQ   $0          // 参数大小CALL    runtime·newproc(SB)POPQ    AXPOPQ    AX// 启动这个 M,mstart 应该永不返回CALL    runtime·mstart(SB)(...)RET
func mstart0() {...mstart1() // 启动m//退出当前线程if mStackIsSystemAllocated() {osStack = true}//执行完所有的 Goroutine 后,清理并退出m,不会执行到这里mexit(osStack)
}func mstart1() {...asminit()minit() //初始化新的m,在新线程上调用...schedule() //开始调度,找到一个`runnable`状态的goroutine并执行
}

其中,schedule 是整个 golang 程序的运行核心,所有的协程都是通过它来开始运行的,schedule 的主要工作逻辑如下:

  • 每隔 61次调度轮回从全局队列找,避免全局队列中的g被饿死
  • 从 p.runnext 获取 g,从 p的本地队列中获取
  • 调用 findrunnable 找 g,找不到的话就将 m 休眠,等待唤醒
  • 当找到一个 g 后,就会调用 execute 去执行 g

源码如下:

// file:runtime/proc.go
func schedule() {_g_ := getg()...
top:pp := _g_.m.p.ptr()//每 61 次从全局运行队列中获取可运行的协程if gp == nil {if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {lock(&sched.lock)gp = globrunqget(_g_.m.p.ptr(), 1)unlock(&sched.lock)}}if gp == nil {//从当前 P 的运行队列中获取可运行gp, inheritTime = runqget(_g_.m.p.ptr())}if gp == nil {//当前P或者全局队列中获取可运行协程//尝试从其它P中steal任务来处理//如果获取不到,就阻塞gp, inheritTime = findrunnable() // blocks until work is available}//执行协程execute(gp, inheritTime) 
}

好了,runtime几个核心的方法讲解了,接下来看看main函数的真正运行

 4.main函数的运行

通过上面的步骤以及runtime函数的讲解,知道了整个golang的调度系统,以及设置了runtime.main函数作为入口,所以对于主协程的调度,就会进入这个入口进行执行,通过runtime 运行自己写的main函数,其实runtime.main 在执行main包中的main之前,还是做了一些不少其他工作,包括:

  • 启动系统后台监控sysmon 线程:新建一个线程来执行sysmon它的工作是系统后台监控(定期垃圾回收和调度抢占)
  • 执行 runtime init 函数:runtime 包中也有不少的 init 函数,会在这个时机运行
  • 启动 gc 清扫的 goroutine
  • 执行 main init 函数,包括用户定义的所有的 init 函数
  • 执行用户 main 函数
// The main goroutine.
func main() {g := getg()...// 执行栈最大限制:1GB(64位系统)或者 250MB(32位系统)if sys.PtrSize == 8 {maxstacksize = 1000000000} else {maxstacksize = 250000000}...// 启动系统后台监控(定期垃圾回收、抢占调度等等)systemstack(func() {newm(sysmon, nil)})...// 让goroute独占当前线程, // runtime.lockOSThread的用法详见http://xiaorui.cc/archives/5320lockOSThread()...// runtime包内部的init函数执行runtime_init() // must be before defer// Defer unlock so that runtime.Goexit during init does the unlock too.needUnlock := truedefer func() {if needUnlock {unlockOSThread()}}()// 启动GCgcenable()...// 用户包的init执行main_init()...needUnlock = falseunlockOSThread()...// 执行用户的main主函数main_main()...// 退出exit(0)for {var x *int32*x = 0}
}

到这里,用户定义的 main 函数能被执行到,就可以输出用户的程序了

5.总结

Golang 程序的运行入口是runtime定义的一个汇编函数_rt0_amd64,这个函数核心有三个:

  • 1.通过 runtime 中的 osinit、schedinit 等函数对 golang 运行时进行关键的初始化,在这里将看到 GMP 的初始化,与调度逻辑
  • 2.创建一个主协程,并指明 runtime.main 函数是其入口函数,因为操作系统加载的时候只创建好了主线程,协程这种东西还是得用户态的 golang 自己来管理,golang 在这里创建出了自己的第一个协程
  • 3.调用 runtime·mstart 真正开启调度器进行运行

当调度器开始执行后,其中主协程会进入 runtime.main 函数中运行,在这个函数中进行初始化后,最后真正进入用户的 main 中运行

参考:

        [go学习笔记.第二章] 3.go语言快速开发入门

        Golang 程序启动过程

        Golang 程序启动流程分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/269362.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

23.基于springboot + vue实现的前后端分离-在线旅游网站系统(项目 + 论文PPT)

项目介绍 本旅游网站系统采用的数据库是MYSQL &#xff0c;使用 JSP 技术开发&#xff0c;在设计过程中&#xff0c;充分保证了系统代码的良好可读性、实用性、易扩展性、通用性、便于后期维护、操作方便以及页面简洁等特点。 技术选型 后端: SpringBoot Mybatis 数据库 : MyS…

视频生成模型Sora的全面解析:从AI绘画、ViT到ViViT、DiT、VDT、NaViT、VideoPoet

前言 真没想到&#xff0c;距离视频生成上一轮的集中爆发(详见《Sora之前的视频生成发展史&#xff1a;从Gen2、Emu Video到PixelDance、SVD、Pika 1.0》)才过去三个月&#xff0c;没想OpenAI一出手&#xff0c;该领域又直接变天了 自打2.16日OpenAI发布sora以来(其开发团队包…

第十七天-反爬与反反爬-验证码识别

目录 反爬虫介绍 基于身份识别反爬和解决思路 Headers反爬-使用User-agent Headers反爬-使用coookie字段 Headers反爬-使用Referer字段 基于参数反爬 验证码反爬 1.验证码介绍 2.验证码分类&#xff1a; 3.验证码作用 4.处理方案 5.图片识别引擎:ocr 6.使用打码平…

AWTK 开源串口屏开发(11) - 天气预报

# AWTK 开源串口屏开发 - 天气预报 天气预报是一个很常用的功能&#xff0c;在很多设备上都有这个功能。实现天气预报的功能&#xff0c;不能说很难但是也绝不简单&#xff0c;首先需要从网上获取数据&#xff0c;再解析数据&#xff0c;最后更新到界面上。 在 AWTK 串口屏中…

如何在jupyter notebook 中下载第三方库

在anconda 中找到&#xff1a; Anaconda Prompt 进入页面后的样式&#xff1a; 在黑色框中输入&#xff1a; 下载第三方库的命令 第三方库&#xff1a; 三种输入方式 标准保证正确 pip instsall 包名 -i 镜像源地址 pip install pip 是 Python 包管理工具&#xff0c;…

牛客练习赛122

D:圆 正着求删除的最小代价不好做&#xff0c;采用逆向思维&#xff0c;求选择一些不相交的线段使得构成一个圆的代价尽量大&#xff0c;最后答案就是所有线段权值之和减去最大代价。 那么如何求这个最大代价呢&#xff1f;显然区间DP 老套路&#xff1a;破环成链&#xff0…

Java实现手机库存管理

一、实验任务 编写一个程序&#xff0c;模拟库存管理系统。该系统主要包括系统首页、商品入库、商品显示和删除商品功能。每个功能的具体要求如下&#xff1a; 1.系统的首页&#xff1a;用于显示系统所有的操作&#xff0c;并且可以选择使用某一个功能。 2.商品入库功能&…

Java 数据结构篇-深入了解排序算法(动态图 + 实现七种基本排序算法)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 实现冒泡排序 2.0 实现选择排序 2.1 选择排序的改良升级 3.0 实现堆排序 4.0 实现插入排序 5.0 实现希尔排序 6.0 实现归并排序 6.1 递归实现归并排序 6.2 使用…

用FPGA CORDIC IP核实现信号的相位检测,计算相位角

用FPGA CORDIC IP核实现信号的相位检测 1.matlab仿真 波形仿真代码&#xff1a; 代码功能&#xff1a;生成一个点频信号s&#xff0c;求出s的实部和虚部&#xff1b;并且结算相位角atan2。画出图形&#xff0c;并且将Q和I数据写入文件中。 %代码功能&#xff1a;生成一个点…

双链表——“数据结构与算法”

各位CSDN的uu们你们好呀&#xff0c;今天&#xff0c;小雅兰又回来了&#xff0c;到了好久没有更新的数据结构与算法专栏&#xff0c;最近确实发现自己有很多不足&#xff0c;需要学习的内容也有很多&#xff0c;所以之后更新文章可能不会像之前那种一天一篇或者一天两篇啦&…

红帆OA 多处 SQL注入漏洞复现

0x01 产品简介 红帆iOffice.net从最早满足医院行政办公需求(传统OA),到目前融合了卫生主管部门的管理规范和众多行业特色应用,是目前唯一定位于解决医院综合业务管理的软件,是最符合医院行业特点的医院综合业务管理平台,是成功案例最多的医院综合业务管理软件。 0x02 漏…

网络安全: Kali Linux 使用 docker-compose 部署 openvas

目录 一、实验 1.环境 2.Kali Linux 安装docker与docker-compose 3.Kali Linux 使用docker-compose方式部署 openvas 4. KaliLinux 使用openvas 二、问题 1. 信息安全漏洞库 2.信息安全漏洞共享平台 3.Windows 更新指南与查询 4.CVE 查询 5.docker-compose 如何修改o…

哪些型号的高速主轴适合PCB分板机

在选择适合PCB分板机的高速主轴时&#xff0c;SycoTec品牌提供了丰富的型号选择&#xff0c;主要型号包括4025 HY、4033 AC&#xff08;电动换刀&#xff09;、4033 AC-ESD、4033 DC-T和4041 HY-ESD等。 那么如何选择合适的PCB分板机高速主轴型号呢&#xff1f;在选择适合PCB分…

LZO索引文件失效说明

在hive中创建lzo文件和索引时&#xff0c;进行查询时会出现问题.hive的默认输入格式是开启小文件合并的&#xff0c;会把索引也合并进来。所以要关闭hive小文件合并功能&#xff01;

day03_Vue_Element

文章目录 01.Ajax1.1 Ajax 概述1.2 同步异步1.3 原生Ajax 2. Axios2.1 Axios的基本使用2.2 Axios快速入门2.3请求方法的别名2.4 案例 3 前后台分离开发3.1 前后台分离开发介绍 04 YAPI4.1 YAPI介绍4.2 接口文档管理 05 前端工程化5.1 前端工程化介绍5.2 前端工程化入门5.2.1 环…

小程序学习

1、小程序体验 2、注册账号 小程序 (qq.com) 3、开发工具下载 下载 / 稳定版更新日志 (qq.com) 4、目录结构 "navigationBarBackgroundColor": "#00b26a" 配置头部背景色 4、wxml模板介绍 5、wxss 6、js文件 7、宿主环境 1、通信主体 2、运行机制 3、…

网工学习 DHCP配置-接口模式

网工学习 DHCP配置-接口模式 学习DHCP总是看到&#xff0c;接口模式、全局模式、中继模式。理解起来也不困难&#xff0c;但是自己动手操作起来全是问号。跟着老师视频配置啥问题没有&#xff0c;自己组建网络环境配置就是不通&#xff0c;悲催。今天总结一下我学习接口模式的…

动手学深度学习—循环神经网络RNN详解

循环神经网络 循环神经网络的步骤&#xff1a; 处理数据 将数据按照批量大小和时间步数进行处理&#xff0c;最后得到迭代器&#xff0c;即每一个迭代的大小是批量大小时间步数&#xff0c;迭代次数根据整个数据的大小决定&#xff0c;最后得出处理的数据&#xff08;参照第三…

基于SpringBoot的物业管理系统

** &#x1f345;点赞收藏关注 → 私信领取本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345;** 一 、设计说明 1.1 研究…

Elasticsearch:使用 Streamlit、语义搜索和命名实体提取开发 Elastic Search 应用程序

作者&#xff1a;Camille Corti-Georgiou 介绍 一切都是一个搜索问题。 我在 Elastic 工作的第一周就听到有人说过这句话&#xff0c;从那时起&#xff0c;这句话就永久地印在了我的脑海中。 这篇博客的目的并不是我出色的同事对我所做的相关陈述进行分析&#xff0c;但我首先…