go生命周期-从源码角度

好奇心

出于好奇,想了解go的执行生命周期,于是尝试跟着go的源码看看执行过程

go源码地址:GitHub - golang/go: The Go programming language

1.根据命令行编译文件,然后执行二进制文件

(1)go运行命令开始:#go run main.go

(2)入口在源码文件//src/cmd/go/main.go  ,找main()函数

// init() 函数,是go的隐式调用的,即会在包引入就执行。这里会在main()运行前初始化一些go的基本命令 
fun init(){
  base.Go.Commands = []*base.Command{ //go基本命令定义
    ...
    work.CmdBuild,
    run.CmdRun, //go run 命令定义
    ...
  }
}


fun main(){
    _ = go11tag  //
   flag.Usage = base.Usage
   flag.Parse() //从os.Args[1:]解析输入的命令行参数,从切片1开始
   log.SetFlags(0)
   args := flag.Args()//args[0]就是go后面跟的参数,我们这里是run
   if len(args) < 1 {
      base.Usage()
   }
   //如果是get或help的命令,
   if args[0] == "get" || args[0] == "help" {
      if !modload.WillBeEnabled() {
         // Replace module-aware get with GOPATH get if appropriate.
         *modget.CmdGet = *get.CmdGet
      }
   }
   cfg.CmdName = args[0] // for error messages
   //如果是get或help的命令,
   if args[0] == "help" {
      help.Help(os.Stdout, args[1:])
      return
   }
   //检查GOPATH的设置是否正确,不正确就退出了
   ...
   
//核心是BigCmdLoop这段循环执行命令
BigCmdLoop:
   for bigCmd := base.Go; ; {//go 基本命令结构体
      for _, cmd := range bigCmd.Commands {//go 可用命令列表
         //前面我们知道args[0]=run,args[1]=main.go
         if cmd.Name() != args[0] { //如果命令匹配不上就执行下一个
            continue
         }
         //如果有类似go mod help foo 更多的命令
         if len(cmd.Commands) > 0 {
            bigCmd = cmd
            args = args[1:] //把子命令截取,如:args[0]=help
            if len(args) == 0 {
               help.PrintUsage(os.Stderr, bigCmd)
               base.SetExitStatus(2)
               base.Exit()
            }
            if args[0] == "help" {
               // Accept 'go mod help' and 'go mod help foo' for 'go help mod' and 'go help mod foo'.
               help.Help(os.Stdout, append(strings.Split(cfg.CmdName, " "), args[1:]...))
               return
            }
            //错误提示信息"build", "install", "list", "mod tidy", etc.
            cfg.CmdName += " " + args[0]
            continue BigCmdLoop
         }
         if !cmd.Runnable() {
            continue
         }
         //把命令参数传过去,调用具体方法
         invoke(cmd, args)
         base.Exit()
         return
      }
      helpArg := ""
      if i := strings.LastIndex(cfg.CmdName, " "); i >= 0 {
         helpArg = " " + cfg.CmdName[:i]
      }
      fmt.Fprintf(os.Stderr, "go %s: unknown command\nRun 'go help%s' for usage.\n", cfg.CmdName, helpArg)
      base.SetExitStatus(2)
      base.Exit()
   }
}


func invoke(cmd *base.Command, args []string) {
  ...
  //如go run main.go这个执行会真正执行到对应的实现函数去
  cmd.Run(ctx, cmd, args)//CmdRun.Run = runRun,实际上会调runRun方法(go build,CmdBuild.Run = runBuild)
  span.Done()
}
invoke()方法会真正执行到src/cmd/go/internal/run/run.go里面的runRun()方法(如果是go build main.go会找到src/cmd/go/internal/work/build.go的runBuild)

(3)源码文件://src/cmd/go/internal/run/run.go  ,找runRun()方法

var CmdRun = &base.Command{
  UsageLine: "go run [build flags] [-exec xprog] package [arguments...]",
  Short:     "compile and run Go program",
  Long: `Run compiles and runs the named main Go package...`,//Run编译并运行命名的主Go包...
}
func init() {
   
   CmdRun.Run = runRun // break init loop
   work.AddBuildFlags(CmdRun, work.DefaultBuildFlags)
   CmdRun.Flag.Var((*base.StringsFlag)(&work.ExecCmd), "exec", "")
}
//go run main.go最终会执行到这里
func runRun(ctx context.Context, cmd *base.Command, args []string) {
  //看run时有没有指定包,如go run cmd@version,有则导入包
  if shouldUseOutsideModuleMode(args) {
     // Set global module flags for 'go run cmd@version'.
     // This must be done before modload.Init, but we need to call work.BuildInit
     // before loading packages, since it affects package locations, e.g.,
     // for -race and -msan.
     modload.ForceUseModules = true
     modload.RootMode = modload.NoRoot
     modload.AllowMissingModuleImports()
     modload.Init()
  }
  //运行之前,先编译,根据机器将源码编译成对应可执行文件,大概过程
  //1.词法分析(编译器会将扫描到的词法单位(Lexemes)归类到常量、保留字、运算符等标记(Tokens)中)
  //2.语法分析(接收上一阶段生成的Tokens序列,基于特定编程语言的规则生成抽象语法树AST)
  //3.语义分析(语义分析阶段用来检查代码的语义一致性)
  //4.中间码生成(中间代码介是于高级语言和机器语言之间,具有跨平台特性)
  //5.代码优化(改进中间代码,删除不必要的代码)
  //6.机器码生成(基于中间码生成汇编代码,汇编器根据汇编代码生成目标文件,目标文件经过链接器处理最终生成可执行文件)
  //基本所有的高级语言到机器码的输出都需要编译这个过程
  work.BuildInit()
  var b work.Builder
  //初始化
  b.Init()
  ...
  //链接到可执行文件
  a1 := b.LinkAction(work.ModeBuild, work.ModeBuild, p)
  //执行,buildRunProgram:是执行二进制文件
  a := &work.Action{Mode: "go run", Func: buildRunProgram, Args: cmdArgs, Deps: []*work.Action{a1}}
  b.Do(ctx, a)
}
 可以看的run之前,是先进行打包的:work.BuildInit(),打包完就是进行链接执行了。

(4)打包work.BuildInit()

         (4-1)词法分析:编译器会将扫描到的词法单位(Lexemes)归类到常量、保留字、运算符等标记(Tokens)中

         (4-2)语法分析:接收上一阶段生成的Tokens序列,基于特定编程语言的规则生成抽象语法树AST

         (4-3)语义分析:语义分析阶段用来检查代码的语义一致性

         (4-4)中间码生成:中间代码介是于高级语言和机器语言之间,具有跨平台特性

         (4-5)代码优化:改进中间代码,删除不必要的代码)

         (4-6)机器码生成:基于中间码生成汇编代码,汇编器根据汇编代码生成目标文件,目标文件经过链接器处理最终生成可执行文件

(5)链接到可执行文件

     a1 := b.LinkAction(work.ModeBuild, work.ModeBuild, p)

(6)执行二进制文件

          buildRunProgram:是执行二进制文件

     a := &work.Action{Mode: "go run", Func: buildRunProgram, Args: cmdArgs, Deps: []*work.Action{a1}}

 到这里才开始执行二进制文件,才真正跑起main这个二进制文件。类似我们在服务器上启动:./main

(7)小结图解

2.引导程序执行,程序执行真正入口

(1)引导程序入口

go引导阶段,真正的程序入口在 src/runtime 包中,不同的计算机架构指向不同.如arm64的linux,对应的机器码就会找对应的入口

//src/runtime/rt0_linux_amd64.s

看名字很直观:

rt0:表示runtime0,

liunx:表示操作系统,

amd64:支持AMD64指令集,指处理器架构

平时我们打包程序:gf build main.go -n main -a amd64 -s linux -p  ,生成的机器码就是对应amd64指令集的。当我们执行时,操作系统就会引导程序来

(2)引导程序开始引导执行

   (2-1)src/runtime/rt0_linux_amd64.s 这里的入口开始执行。

#include "textflag.h"
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
   JMP    _rt0_amd64(SB)
TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0
   JMP    _rt0_amd64_lib(SB)
_rt0_amd64(SB)会找到//src/runtime/asm_amd64.s的TEXT _rt0_amd64(SB),NOSPLIT,$-8 方法

  (2-2)//src/runtime/asm_amd64.s ,引导执行的核心方法

TEXT _rt0_amd64(SB),NOSPLIT,$-8
   MOVQ   0(SP), DI  // argc
   LEAQ   8(SP), SI  // argv
   JMP    runtime·rt0_go(SB)
   
...
TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
   // copy arguments forward on an even stack
   MOVQ   DI, AX    // argc
   MOVQ   SI, BX    // argv
   SUBQ   $(5*8), SP    // 3args 2auto
   ANDQ   $~15, SP
   MOVQ   AX, 24(SP)
   MOVQ   BX, 32(SP)
   // create istack out of the given (operating system) stack.
   // _cgo_init may update stackguard.
   //创建最初的G g0
   MOVQ   $runtime·g0(SB), DI
   LEAQ   (-64*1024+104)(SP), BX
   MOVQ   BX, g_stackguard0(DI)
   MOVQ   BX, g_stackguard1(DI)
   MOVQ   BX, (g_stack+stack_lo)(DI)
   MOVQ   SP, (g_stack+stack_hi)(DI)
  // find out information about the processor we're on
  MOVL   $0, AX
  CPUID
  CMPL   AX, $0
  JE nocpuinfo   
...
  
ok:
   // set the per-goroutine and per-mach "registers"
   //创建最初的线程 m0 和 goroutine g0,并把两者进行关联(g0.m = m0)
   get_tls(BX) //本地线程存储 (Thread Local Storage, TLS)
   LEAQ   runtime·g0(SB), CX
   MOVQ   CX, g(BX)
   LEAQ   runtime·m0(SB), AX
   // save m->g0 = g0
   MOVQ   CX, m_g0(AX)
   // save m0 to g0->m
   MOVQ   AX, g_m(CX)
  ...
  CALL    runtime·args(SB)//系统参数传递,主要是将系统参数转换传递给程序使用
  CALL   runtime·osinit(SB)//系统基本参数设置,主要是获取 CPU 核心数和内存物理页大小
  //各种运行时组件初始化工作.如调度器、内存分配器、堆、栈、GC 等。
  CALL   runtime·schedinit(SB)
  // create a new goroutine to start program
  //创建一个新的goroutine来启动程序,main()函数入口
  MOVQ   $runtime·mainPC(SB), AX       // entry
  PUSHQ  AX
  //创建一个运行fn的新goroutine。(GMP中的G)
  //把它放在等待运行的g队列中(goroutine调度队列,GMP中的p)
  //编译器将go语句转换为对this的调用。
  //会进行 p 的初始化,并将 m0 和某一个 p 进行绑定
  CALL   runtime·newproc(SB)
  POPQ   AX
  // start this M ,开启一个内核线程(GMP中的M),调用mstart0,调mstart1,调度器开始进行循环调度
  CALL   runtime·mstart(SB)
  CALL   runtime·abort(SB)  // mstart should never return

 

(2-3)m0和g0创建

   get_tls(BX)  //本地线程存储 (Thread Local Storage, TLS)
   LEAQ   runtime·g0(SB), CX
   MOVQ   CX, g(BX)
   LEAQ   runtime·m0(SB), AX
   // save m->g0 = g0
   MOVQ   CX, m_g0(AX)
   // save m0 to g0->m

     创建最初的线程 m0 和 goroutine g0,并把两者进行关联(g0.m = m0), g0 和 m0 是一组全局变量。

     m0 : m0 是 Go 启动时所创建的第一个系统线程,一个 Go 进程只有一个 m0,也叫主线程。

     g0:是m结构上的g指针,负责普通G在M上的调度切换。每个M都有自己的g0,即m0->g0,m1->g0...

type m struct {
   g0      *g // goroutine with scheduling stack
     ...

}
type g struct {
 stack       stack // offset known to runtime/cgo
 ...
 m         *m // current m; offset known to arm liblink
 sched     gobuf
...

}

   (2-4) 关于GMP:

     G-Goroutine:Go协程,是参与调度与执行的最小单位。普通G是执行用户任务的协程G。g0比较特殊,是执行调度普通G执行任务的。

     M-Machine:系统内核级别线程,m0是主线程,一个 Go 进程只有一个 m0,后续M由 Go Runtime 内自行创建。主线程m0在启动完初始化工作之后,会和普通M工作一样。

     P-Processor:是逻辑处理器,P关联了的本地可运行G的队列(也称为LRQ),最多可存放256个G

(2-5)g0,常规g,m0,常规M

m0和普通m:

      m0是主线程,一个 Go 进程只有一个 m0,后续M由 Go Runtime 内自行创建。主线程m0在启动完初始化工作之后,会和普通M工作一样。

g0和普通g:

g0 比较特殊,每一个 m 都只有一个 g0(仅此只有一个 g0),且每个 m 都只会绑定一个 g0。

A.数据结构一样

B.存在栈不同:g0栈分配的是系统栈,linux上栈默认大小是固定8M,不能扩缩。常规g起始只有2KB,可扩缩

C.运行状态不同:g0没那么多运行状态,也不会被调度程序抢占,调度本身就是在g0上运行的

(3) CALL runtime·schedinit(SB)各种运行时组件初始化工作

  (3-1)src/runtime/proc.go,启动核心的方法初始化如调度器、内存分配器、堆、栈、GC 等

func schedinit() {
  ...
  sched.maxmcount = 10000 //初始化机器线程数M最大为10000
  ...
  stackinit()//goroutine 执行栈初始化
  mallocinit()//内存分配器初始化
  initPageTrace(godebug)//内存页初始化
  ...
  modulesinit()   // 系统线程的部分初始化工作
  ...
  gcinit()//垃圾回收初始化
  ...
  procs := ncpu //P(go逻辑处理器)数量,gogetenv("GOMAXPROCS")
  procs := ncpu
  //GOMAXPROCS的数量一般和cup的核数相同,因为M和P是一一绑定的
  if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
   procs = n
  }
  ...
  procresize(procs)
  ...
}

(4) CALLruntime·newproc(SB)会进行 p 的初始化,并将 m0 和某一个 p 进行绑定

  (4-1)src/runtime/proc.go,这里GMP中的三个关键结构就出现了

//创建一个运行fn的新goroutine。
func newproc(fn *funcval) {
   gp := getg()
   pc := getcallerpc()
   systemstack(func() {
      newg := newproc1(fn, gp, pc)
      pp := getg().m.p.ptr()
      runqput(pp, newg, true)
      if mainStarted {
         wakep()
      }
   })
}

(5) CALL runtime·mstart(SB)启动循环调度

  (5-1)src/runtime/proc.go,这里start this M ,开启一个内核线程(GMP中的M),调用mstart0,调mstart1,调度器开始进行循环调度

// mstart is the entry-point for new Ms.
// It is written in assembly, uses ABI0, is marked TOPFRAME, and calls mstart0.
func mstart()
//创建一个运行fn的新goroutine。
func mstart0() {
   gp := getg()
   osStack := gp.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.
      //
      // Note: these bounds may not be very accurate.
      // We set hi to &size, but there are things above
      // it. The 1024 is supposed to compensate this,
      // but is somewhat arbitrary.
      size := gp.stack.hi
      if size == 0 {
         size = 8192 * sys.StackGuardMultiplier
      }
      gp.stack.hi = uintptr(noescape(unsafe.Pointer(&size)))
      gp.stack.lo = gp.stack.hi - size + 1024
   }
   // Initialize stack guard so that we can start calling regular
   // Go code.
   gp.stackguard0 = gp.stack.lo + _StackGuard
   // This is the g0, so we can also call go:systemstack
   // functions, which check stackguard1.
   gp.stackguard1 = gp.stackguard0
   mstart1()
   // Exit this thread.
   if mStackIsSystemAllocated() {
      // Windows, Solaris, illumos, Darwin, AIX and Plan 9 always system-allocate
      // the stack, but put it in gp.stack before mstart,
      // so the logic above hasn't set osStack yet.
      osStack = true
   }
   //退出线程
   mexit(osStack)
}
//保存g0的调度信息
// 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.
func mstart1() {
  ...
  schedule()
}
//调度器:找到一个可运行的goroutine并执行它。
func schedule() {
  ...
  execute(gp, inheritTime)
}
//调度gp在当前M上运行。
func execute(gp *g, inheritTime bool) {
  ...
}

(6)小结图解

到这里初步了解go执行过程,后续还有很多个点需要再细致了解,中间参考了很多优秀的文章。

参考资料:

知乎:mingguangtu的深入分析Go1.18 GMP调度器底层原理:深入分析Go1.18 GMP调度器底层原理 - 知乎

欧长坤创作的go语言原本 :Changkun Ou | Go 语言原本

tink.的GMP模型:https://go.cyub.vip/gmp/gmp-model.html#id11

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值