• 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 二进制文件的结构,可以看到二进制文件中代码区数据区的内容,全局变量保存在数据区,函数保存在代码区

    1. $ readelf -s main | grep runtime.g0
    2. 1765: 000000000054b3a0 376 OBJECT GLOBAL DEFAULT 11 runtime.g0
    3. // _cgo_init 为全局变量
    4. $ readelf -s main | grep -i _cgo_init
    5. 2159: 000000000054aa88 8 OBJECT GLOBAL DEFAULT 11 _cgo_init

    二.运行

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

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

    三.程序启动流程分析

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

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

    main.go

    1. package main
    2. import "fmt"
    3. func main() {
    4. fmt.Println("hello world")
    5. }

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

    1. $ go build -gcflags "-N -l" -o main main.go
    2. $ gdb ./main
    3. (gdb) info files
    4. Symbols from "/home/gosoon/main".
    5. Local exec file:
    6. `/home/gosoon/main', file type elf64-x86-64.
    7. Entry point: 0x465860
    8. 0x0000000000401000 - 0x0000000000497893 is .text
    9. 0x0000000000498000 - 0x00000000004dbb65 is .rodata
    10. 0x00000000004dbd00 - 0x00000000004dc42c is .typelink
    11. 0x00000000004dc440 - 0x00000000004dc490 is .itablink
    12. 0x00000000004dc490 - 0x00000000004dc490 is .gosymtab
    13. 0x00000000004dc4a0 - 0x0000000000534b90 is .gopclntab
    14. 0x0000000000535000 - 0x0000000000535020 is .go.buildinfo
    15. 0x0000000000535020 - 0x00000000005432e4 is .noptrdata
    16. 0x0000000000543300 - 0x000000000054aa70 is .data
    17. 0x000000000054aa80 - 0x00000000005781f0 is .bss
    18. 0x0000000000578200 - 0x000000000057d510 is .noptrbss
    19. 0x0000000000400f9c - 0x0000000000401000 is .note.go.buildid
    20. (gdb) b *0x465860
    21. Breakpoint 1 at 0x465860: file /home/gosoon/golang/go/src/runtime/rt0_linux_amd64.s, line 8.
    22. (gdb) r
    23. Starting program: /home/gaofeilei/./main
    24. Breakpoint 1, _rt0_amd64_linux () at /home/gaofeilei/golang/go/src/runtime/rt0_linux_amd64.s:8
    25. 8 JMP _rt0_amd64(SB)
    26. (gdb) n
    27. _rt0_amd64 () at /home/gaofeilei/golang/go/src/runtime/asm_amd64.s:15
    28. 15 MOVQ 0(SP), DI // argc
    29. (gdb) n
    30. 16 LEAQ 8(SP), SI // argv
    31. (gdb) n
    32. 17 JMP runtime·rt0_go(SB)
    33. (gdb) n
    34. runtime.rt0_go () at /home/gaofeilei/golang/go/src/runtime/asm_amd64.s:91
    35. 91 MOVQ DI, AX // argc
    36. ......
    37. 231 CALL runtime·mstart(SB)
    38. (gdb) n
    39. hello world
    40. [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

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

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

    1. TEXT _rt0_amd64(SB),NOSPLIT,$-8
    2. // 处理 argc 和 argv 参数,argc 是指命令行输入参数的个数,argv 存储了所有的命令行参数
    3. MOVQ 0(SP), DI // argc
    4. // argv 为指针类型
    5. LEAQ 8(SP), SI // argv
    6. JMP 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 函数,此处会阻塞住直到进程退出

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

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

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

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

    1. // 执行CPUID指令,尝试获取CPU信息,探测 CPU 和 指令集的代码
    2. MOVL $0, AX
    3. CPUID
    4. MOVL AX, SI
    5. CMPL AX, $0
    6. JE nocpuinfo
    7. // Figure out how to serialize RDTSC.
    8. // On Intel processors LFENCE is enough. AMD requires MFENCE.
    9. // Don't know about the rest, so let's do MFENCE.
    10. CMPL BX, $0x756E6547 // "Genu"
    11. JNE notintel
    12. CMPL DX, $0x49656E69 // "ineI"
    13. JNE notintel
    14. CMPL CX, $0x6C65746E // "ntel"
    15. JNE notintel
    16. MOVB $1, runtime·isIntel(SB)
    17. MOVB $1, runtime·lfenceBeforeRdtsc(SB)
    18. notintel:
    19. // Load EAX=1 cpuid flags
    20. MOVL $1, AX
    21. CPUID
    22. MOVL AX, runtime·processorVersionInfo(SB)
    23. nocpuinfo:
    24. // cgo 初始化相关,_cgo_init 为全局变量
    25. MOVQ _cgo_init(SB), AX
    26. // 检查 AX 是否为 0
    27. TESTQ AX, AX
    28. // 跳转到 needtls
    29. JZ needtls
    30. // arg 1: g0, already in DI
    31. MOVQ $setg_gcc<>(SB), SI // arg 2: setg_gcc
    32. CALL AX
    33. // 如果开启了 CGO 特性,则会修改 g0 的部分字段
    34. MOVQ $runtime·g0(SB), CX
    35. MOVQ (g_stack+stack_lo)(CX), AX
    36. ADDQ $const__StackGuard, AX
    37. MOVQ AX, g_stackguard0(CX)
    38. 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

    1. // 下面开始初始化tls(thread local storage,线程本地存储),设置 m0 为线程私有变量,将 m0 绑定到主线程
    2. needtls:
    3. LEAQ runtime·m0+m_tls(SB), DI //DI = &m0.tls,取m0的tls成员的地址到DI寄存器
    4. // 调用 runtime·settls 函数设置线程本地存储,runtime·settls 函数的参数在 DI 寄存器中
    5. // 在 runtime·settls 函数中将 m0.tls[1] 的地址设置为 tls 的地址
    6. // runtime·settls 函数在 runtime/sys_linux_amd64.s#599
    7. CALL runtime·settls(SB)
    8. // 此处是在验证本地存储是否可以正常工作,确保值正确写入了 m0.tls,
    9. // 如果有问题则 abort 退出程序
    10. // get_tls 是宏,位于 runtime/go_tls.h
    11. get_tls(BX) // 将 tls 的地址放入 BX 中,即 BX = &m0.tls[1]
    12. MOVQ $0x123, g(BX) // BX = 0x123,即 m0.tls[0] = 0x123
    13. MOVQ runtime·m0+m_tls(SB), AX // AX = m0.tls[0]
    14. CMPQ AX, $0x123
    15. JEQ 2(PC) // 如果相等则向后跳转两条指令即到 ok 代码块
    16. 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 函数
    1. // 首先将 g0 地址保存在 tls 中,即 m0.tls[0] = &g0,然后将 m0 和 g0 绑定
    2. // 即 m0.g0 = g0, g0.m = m0
    3. ok:
    4. get_tls(BX) // 获取tls地址到BX寄存器,即 BX = m0.tls[0]
    5. LEAQ runtime·g0(SB), CX // CX = &g0
    6. MOVQ CX, g(BX) // m0.tls[0]=&g0
    7. LEAQ runtime·m0(SB), AX // AX = &m0
    8. MOVQ CX, m_g0(AX) // m0.g0 = g0
    9. MOVQ AX, g_m(CX) // g0.m = m0
    10. CLD // convention is D is always left cleared
    11. // check 函数检查了各种类型以及类型转换是否有问题,位于 runtime/runtime1.go#137
    12. CALL runtime·check(SB)
    13. // 将 argc 和 argv 移动到 SP+0 和 SP+8 的位置
    14. // 此处是为了将 argc 和 argv 作为 runtime·args 函数的参数
    15. MOVL 16(SP), AX
    16. MOVL AX, 0(SP)
    17. MOVQ 24(SP), AX
    18. MOVQ AX, 8(SP)
    19. // args 函数会从栈中读取参数和环境变量等进行处理
    20. // args 函数位于 runtime/runtime1.go#61
    21. CALL runtime·args(SB)
    22. // osinit 函数用来初始化 cpu 数量,函数位于 runtime/os_linux.go#301
    23. CALL runtime·osinit(SB)
    24. // schedinit 函数用来初始化调度器,函数位于 runtime/proc.go#654
    25. CALL runtime·schedinit(SB)
    26. // 创建第一个 goroutine 执行 runtime.main 函数。获取 runtime.main 的地址,调用 newproc 创建 g
    27. MOVQ $runtime·mainPC(SB), AX
    28. PUSHQ AX // runtime.main 作为 newproc 的第二个参数入栈
    29. PUSHQ $0 // newproc 的第一个参数入栈,该参数表示runtime.main函数需要的参数大小,runtime.main没有参数,所以这里是0
    30. // newproc 创建一个新的 goroutine 并放置到等待队列里,该 goroutine 会执行runtime.main 函数, 函数位于 runtime/proc.go#4250
    31. CALL runtime·newproc(SB)
    32. // 弹出栈顶的数据
    33. POPQ AX
    34. POPQ AX
    35. // mstart 函数会启动主线程进入调度循环,然后运行刚刚创建的 goroutine,mstart 会阻塞住,除非函数退出,mstart 函数位于 runtime/proc.go#1328
    36. CALL runtime·mstart(SB)
    37. CALL runtime·abort(SB) // mstart should never return
    38. RET
    39. // Prevent dead-code elimination of debugCallV2, which is
    40. // intended to be called by debuggers.
    41. MOVQ $runtime·debugCallV2<ABIInternal>(SB), AX
    42. RET

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

    总体启动流程大致如下: 

     

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

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

    check

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

    args

    args 函数同样runtime的runtime1.go

    1. var (
    2. argc int32 //参数个数
    3. argv **byte //入参
    4. )
    5. func args(c int32, v **byte) { //初始全局变量 argc,argv 并调用sysargs
    6. argc = c
    7. argv = v
    8. sysargs(c, v)
    9. }
    10. var executablePath string
    11. //获取执行程序路径 复制到全局变量executablePath
    12. func sysargs(argc int32, argv **byte) {
    13. 。。。
    14. }

    schedinit

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

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

    newproc

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

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

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

    1. func main() {
    2. g := getg()
    3. g.m.g0.racectx = 0
    4. //设置栈的最大值,按处理器位数,64位对应1G,32位对应250MB
    5. if goarch.PtrSize == 8 {
    6. maxstacksize = 1000000000
    7. } else {
    8. maxstacksize = 250000000
    9. }
    10. maxstackceiling = 2 * maxstacksize
    11. mainStarted = true //允许上面的newproc函数创建Ms
    12. ...
    13. //执行每runtime的init
    14. doInit(&runtime_inittask) // Must be before defer.
    15. ...
    16. gcenable() //开启gc
    17. //下面一大坨都是cgo相关
    18. main_init_done = make(chan bool)
    19. if iscgo {
    20. ...
    21. }
    22. doInit(&main_inittask) //执行package main的init
    23. ...
    24. fn := main_main // 执行package main中主函数
    25. fn()
    26. ...
    27. exit(0) //退出进程
    28. }

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

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

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

    1. func doInit(t *initTask) {
    2. switch t.state {
    3. case 2: // 完成退出
    4. return
    5. case 1: // 异常panic
    6. throw("recursive call during initialization - linker skew")
    7. default: // 遍历执行
    8. t.state = 1 // 先设置状态到执行中
    9. //向下递归
    10. for i := uintptr(0); i < t.ndeps; i++ {
    11. p := add(unsafe.Pointer(t), (3+i)*goarch.PtrSize)
    12. t2 := *(**initTask)(p)
    13. doInit(t2)
    14. }
    15. //当前模块没init则设置状态到完成,返回
    16. if t.nfns == 0 {
    17. t.state = 2 // initialization done
    18. return
    19. }
    20. ... //执行当前模块的init,完成后设置状态2 返回
    21. t.state = 2
    22. }
    23. }

    mstart

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

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

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

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

    源码如下:

    1. // file:runtime/proc.go
    2. func schedule() {
    3. _g_ := getg()
    4. ...
    5. top:
    6. pp := _g_.m.p.ptr()
    7. //每 61 次从全局运行队列中获取可运行的协程
    8. if gp == nil {
    9. if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
    10. lock(&sched.lock)
    11. gp = globrunqget(_g_.m.p.ptr(), 1)
    12. unlock(&sched.lock)
    13. }
    14. }
    15. if gp == nil {
    16. //从当前 P 的运行队列中获取可运行
    17. gp, inheritTime = runqget(_g_.m.p.ptr())
    18. }
    19. if gp == nil {
    20. //当前P或者全局队列中获取可运行协程
    21. //尝试从其它P中steal任务来处理
    22. //如果获取不到,就阻塞
    23. gp, inheritTime = findrunnable() // blocks until work is available
    24. }
    25. //执行协程
    26. execute(gp, inheritTime)
    27. }

    好了,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 函数
    1. // The main goroutine.
    2. func main() {
    3. g := getg()
    4. ...
    5. // 执行栈最大限制:1GB(64位系统)或者 250MB(32位系统)
    6. if sys.PtrSize == 8 {
    7. maxstacksize = 1000000000
    8. } else {
    9. maxstacksize = 250000000
    10. }
    11. ...
    12. // 启动系统后台监控(定期垃圾回收、抢占调度等等)
    13. systemstack(func() {
    14. newm(sysmon, nil)
    15. })
    16. ...
    17. // 让goroute独占当前线程,
    18. // runtime.lockOSThread的用法详见http://xiaorui.cc/archives/5320
    19. lockOSThread()
    20. ...
    21. // runtime包内部的init函数执行
    22. runtime_init() // must be before defer
    23. // Defer unlock so that runtime.Goexit during init does the unlock too.
    24. needUnlock := true
    25. defer func() {
    26. if needUnlock {
    27. unlockOSThread()
    28. }
    29. }()
    30. // 启动GC
    31. gcenable()
    32. ...
    33. // 用户包的init执行
    34. main_init()
    35. ...
    36. needUnlock = false
    37. unlockOSThread()
    38. ...
    39. // 执行用户的main主函数
    40. main_main()
    41. ...
    42. // 退出
    43. exit(0)
    44. for {
    45. var x *int32
    46. *x = 0
    47. }
    48. }

    到这里,用户定义的 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 程序启动流程分析

  • 相关阅读:
    Revit二次开发环境Addin和Lookup配置快速上手教程
    【数据结构与算法】stack栈的介绍和实现、使用栈来实现中缀表达式求值
    VUE开发记录
    面试经典150题——Day24
    UE5 - UI Material Lab 学习笔记
    计算机毕业设计ssm+vue 菜谱制作小程序
    你真的知道自己多大了吗?
    【小程序】多种功能实现步骤及获取数据/接口
    全网最牛自动化测试框架系列之pytest(4)-测试用例执行顺序
    怎样提高服务器安全性?45.248.11.x
  • 原文地址:https://blog.csdn.net/zhoupenghui168/article/details/136452141