好奇心
出于好奇,想了解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