在大多数的编程语言中,main函数都是用户程序的入口函数,go中也是如此。那么main.main是整个程序的入口吗, 肯定不是,因为go程序依赖于runtime,在程序的初始阶段需要初始化运行时,之后才会运行到用户的main函数,那么main.main是在哪里被调用的呢?接下来就从go程序的入口,再到go的GMP模型进行一个探究。
注意:本文使用的go sdk的版本为go1.20
1.go程序的入口
1 首先,编写一个简单的go程序,并将其进行编译,在此使用linux系统:
package main
import "fmt"
func main() {
fmt.Println("hello,world")
}
编译:-N -l 用于阻止编译时进行优化和内联
go build -gcflags "-N -l" main.go
2 然后使用gdb来调试go程序:
首先,使用gdb加载支持调试go语言的脚本文件:
在shell中执行gdb命令,然后执行source /usr/local/go/src/runtime/runtime-gdb.py
➜ RemoteWorking git:(master) ✗ gdb
(gdb) source /usr/local/go/src/runtime/runtime-gdb.py
3 调试程序:
gdb main

使用info files来查看文件
可以看到程序的入口为0x45c020, 在该处打上端点,可以看到入口为_rt0_amd64_linux的函数它位于src/runtime/rt0_liunx_amd64.s的汇编文件中:
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)
而该函数又调用了_rt0_amd64,在asm_amd64.s文件中:
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
JMP runtime·rt0_go(SB)
进而又跳转到了rt0_go中:
TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
...
MOVL 24(SP), AX // copy argc
MOVL AX, 0(SP)
MOVQ 32(SP), AX // copy argv
MOVQ AX, 8(SP)
CALL runtime·args(SB)
CALL runtime·osinit(SB)
CALL runtime·schedinit(SB)
// create a new goroutine to start program
MOVQ $runtime·mainPC(SB), AX // entry
PUSHQ AX
CALL runtime·newproc(SB)
POPQ AX
// start this M
CALL runtime·mstart(SB)
CALL runtime·abort(SB) // mstart should never return
RET
...
// mainPC is a function value for runtime.main, to be passed to newproc.
// The reference to runtime.main is made via ABIInternal, since the
// actual function (not the ABI0 wrapper) is needed by newproc.
DATA runtime·mainPC+0(SB)/8,$runtime·main<ABIInternal>(SB)
GLOBL runtime·mainPC(SB),RODATA,$8
在rt0_go中先是进行了一些初始化,比如runtime.osinit, runtime.schedinit。
然后将runtime.mainPC的地址放入AX寄存器中,然后调用了runtime.newproc,根据下面的注释可以知道,mainPC就是runtime.main,而newproc则是创建goroutine的函数。
我们通常在程序中使用 go func()来启动一个协程,这是在go语言提供的一个语法糖,在编译时它会被翻译为newproc的调用。因此,下面的几行代码则是创建了runtime.main的goroutine,也就是主goroutine,主goroutine被创建后,只是被放入了当前p的本地队列,但是还没有得到运行。

接下来调用了runtime.mstart, 这个函数是除了sysmon线程以外的其它线程的入口函数,最终该函数会调用schedule函数,在schedule函数中调用findrunnable函数来获取一个可运行的goroutine,然后调用execute来执行,execute对goroutine对应的g结构体中的字段进行一些设置,然后调用gogo来切换协程栈,并切换协程,因此main goroutine将会被调度执行。
如下图所示:

2 GMP模型
GMP模型是go语言goroutine的调度系统,调度是将goroutine调度到线程上执行的过程,而操作系统的调度器则负责将线程调度到CPU上运行。
2.1 GM模型
go语言早期的调度模型为GM模型,G代表goroutine,而M代表一个线程,goroutine和线程的数量有多个,那么调度器的职责就是将m个goroutine调度到n个线程上来运行。待调度的goroutine处于一个全局的调度队列globrunq中,每个线程需要从globrunq中获取goroutine来执行,那么多个线程同时访问全局队列,为了保证线程间的同步,需要加锁,那么就会导致锁争用较大,从而降低系统的效率。

而且一个goroutine创建的goroutine也会被放入全局队列中,同时也需要加锁。这样也会造成程序的局部性较差,因为一个goroutine创建的另一个goroutine大概率不会在同一个线程上运行。
2.2 改进的GMP模型
为了改进之前的缺点:1 所有线程都从全局队列获取goroutine,造成锁争用强度大。2. 程序的局部性较差
go语言引入了GMP模型,G同样代表一个goroutine,M代表machine,也就是worker thread,p代表processor,包含了运行go代码所需的资源。
官方解释:
// Goroutine scheduler
// The scheduler's job is to distribute ready-to-run goroutines over worker threads.
//
// The main concepts are:
// G - goroutine.
// M - worker thread, or machine.
// P - processor, a resource that is required to execute Go code.
// M must have an associated P to execute Go code, however it can be
// blocked or in a syscall w/o an associated P.
线程是goroutine运行的载体,goroutine必须要在线程上运行。而一个线程想要运行goroutine,就需要和一个p进行关联,在每个p中都包含了一个本地runq,其中存放待运行的goroutine,线程可以从本地runq中无锁访问,减少了锁竞争的力度。本地runq的大小是有限的,最多可以存放256个goroutine。除此之外,还存在一个全局的globrunq,当创建goroutine时,优先放入相关联的p的本地runq,当本地runq满了之后,新创建的goroutine就会被添加到全局globrunq中。
p的数量:p代表了一个逻辑处理器,p的数量一般与CPU的核心数相同,代表了可以并行运行的goroutine的数量,可以通过runtime.GOMAXPROC来设置。m的数量:m表示一个线程,m的数量是不确定的,最大数量为10000个,但是正常情况下达不到这么大的数量。

2.3 相关数据结构
2.3.1 runtime.g
goroutine在runtime中表示为一个g结构体:
type g struct {
stack stack // offset known to runtime/cgo
stackguard0 uintptr // offset known to liblink
...
m *m // current m; offset known to arm liblink
sched gobuf
...
atomicstatus atomic.Uint32
goid uint64
preempt bool // preemption signal, duplicates stackguard0 = stackpreempt
}
type stack struct {
lo uintptr
hi uintptr
}
type gobuf struct {
sp uintptr
pc uintptr
g guintptr
...
}
省略了一些不太关心的字段,其中的一些字段的含义如下:
| 字段 | 用途 |
|---|---|
stack |
goroutine的栈空间,表示栈空间的一个界限 |
stackguard0 |
栈的上限,它的值一般是stack.lo+StackGuard,用于判断是否需要栈增长。由于goroutine的栈在初始化只有2K,并且是可以动态增长的,因此在函数调用时会判断栈空间是否够用,如果不够用会进行扩容。同时该字段可能会被设置为StackPreempt来表示抢占当前goroutine。 |
m |
关联到当前正在运行goroutine的m |
sched |
保存gouroutine的执行上下文,比如栈指针sp,程序计数器pc |
atomicstatus |
一个原子变量,表示goroutine的当前状态 |
goid |
goroutine的id,goroutine的id由p进行分配,p会从全局缓存处取一批id缓存起来 |
preempt |
抢占标识,为true时,调度器会在合适的时机触发一次抢占 |
goroutine是一个有栈协程,stack字段用于描述协程的栈,goroutine的初始栈大小为2K,并且是从堆中分配的,是可以动态增长的。
sched用来存储goroutine执行的上下文,它与goroutine切换的底层实现相关,其中sp标识stack pointer,pc为program counter,g用来反向关联到当前g。
g的状态:
atomicstatus字段表示goroutine的状态,goroutine有多种状态:
| 状态 | 含义 |
|---|---|
| _Gidle | 当前goroutine刚被分配,还没有被初始化 |
| _Grunnable | 当前goroutine处于待运行状态,他可能处于p的本地runq或者globrunq中,当前并没有在运行用户代码,它的栈也不归自己所有。 |
| _Grunning | 当前goroutine正在运行用户代码,有关联的M和P。不会处于任何runq中,栈归该goroutine所有。 |
| _Gsyscall | 当前goroutine正在执行系统调用,并没有在执行用户代码,拥有栈,而且被分配了M。 |
| _Gwaiting | 当前goroutine处于阻塞状态,即不再runq中,也没有得到运行。它肯定被记录在某个地方,比如chan的阻塞队列、mutex的阻塞队列中。 |
| _Gdead | 当前goroutine没有在使用,可能存在一个free list中或者刚刚被初始化。 |
| _Gcopystack | 当前goroutine的栈正在被移动,没有在执行用户代码也不在一个runq中。 |
2.3.2 runtime.m
GMP中的M代表一个工作线程,在runtime中使用m结构体来表示:
type m struct {
g0 *g // goroutine with scheduling stack
gsignal *g // signal-handling g
curg *g // current running goroutine
p puintptr // attached p for executing go code (nil if not executing go code)
id int64
preemptoff string // if != "", keep curg running on this m
locks int32
spinning bool // m is out of work and is actively looking for work
mOS
}
省略了其中一些不太关心的字段,其中一些字段的含义如下:
| 字段 | 用途 |
|---|---|
g0 |
每个工作线程都拥有一个g0,它的栈比普通线程的栈要大,是分配在线程栈上的。g0主要用来运行调度器代码,当需要调度新的协程运行时,就会切换到g0栈上来运行调度程序。 |
gsignal |
用来处理操作系统信号的goroutine |
curg |
指向当前正在运行的g |
p |
关联到的p |
id |
线程的唯一ID |
preemptoff |
不为空时表示要关闭对curg的抢占,字符串的内容给出了相关的原因 |
locks |
当前M持有锁的数量 |
spining |
表示当前线程处于自旋状态 |
mOS |
平台相关的线程 |
2.3.3 runtime.p
GMP中的p代表processor,其中包含了一系列用于运行goroutine的资源,比如本地runq、堆内存缓存、栈内存缓存、goroutine id缓存等,在runtime中使用p结构体表示:
type p struct {
id int32
status uint32 // one of pidle/prunning/...
schedtick uint32 // incremented on every scheduler call
syscalltick uint32 // incremented on every system call
sysmontick sysmontick // last tick observed by sysmon
m muintptr // back-link to associated m (nil if idle)
// Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.
goidcache uint64
goidcacheend uint64
// Queue of runnable goroutines. Accessed without lock.
runqhead uint32
runqtail uint32
runq [256]guintptr
runnext guintptr
// Available G's (status == Gdead)
gFree struct {
gList
n int32
}
// preempt is set to indicate that this P should be enter the
// scheduler ASAP (regardless of what G is running on it).
preempt bool
}
其中省略了一些不太关心的字段,一些字段的含义如下:
| 字段 | 用途 |
|---|---|
id |
p的唯一ID,等于在allp数组中的下标 |
status |
表示p的状态 |
schedtick |
记录了调度发生的次数,每调度一次goroutine并且不继承时间片的情况下,将该字段加1 |
syscalltick |
记录发生系统调用的次数 |
sysmontick |
被监控线程用来存储上一次检查时的调度器时钟滴答,用以实现时间片算法 |
m |
当前关联的m |
goidcache、goidcacheend |
goroutine id缓存,会从全局缓存中申请一批来减少锁争用 |
runqhead、runqtail、runq |
本地goroutine运行队列,使用一个数组和一头一尾组成一个环形队列 |
runnext |
如果不为nil,则指向一个被当前G准备好的就绪的G,接下来会继承当前G的时间片开始运行。 |
gFree |
用来缓存已经推出的g,方便下次申请时复用 |
preempt |
该字段用于支持异步抢占机制 |
p的状态:
| 状态 | 含义 |
|---|---|
| _Pidle | 当前p处于空闲状态,没有被用于执行用户代码或调度。p处于idle list中,它的本地runq是空的 |
| _Prunning | 当前p与一个m进行关联并且被用于执行用户代码或者调度 |
| _Psyscall | 当前p没有在运行用于代码,它与系统调用中的M有亲和关系,但不属于它,并且可能被另一个M窃取。这类似于_Pidle,但使用轻量级转换并维护M亲和关系。 |
| _Pgcstop | 当前p因为STW而停止 |
| _Pdead | 停用状态,因为GOMAXPROC可用收缩,会造成多余的p被停用。一旦GOMAXPROC重新增长,那么停用的p会被重新启用。 |
2.3.4 runtime.schedt
还有另一个和调度相关的数据结构需要关注,就是runtime.schedt,其中包含了调度的一些全局数据,schedt类型的实例只会存在一个:
var (
allm *m // 所有m组成一个链表
gomaxprocs int32 // 对应与GOMAXPROC
ncpu int32 // CPU核心数
sched schedt // 调度器相关的数据结构
allpLock mutex // 保护allp的锁
allp []*p // 所有的p
)
schedt结构如下:
其中全局runq就存在与schedt结构中
type schedt struct {
goidgen atomic.Uint64
midle muintptr // idle m's waiting for work
nmidle int32 // number of idle m's waiting for work
mnext int64 // number of m's that have been created and next M ID
maxmcount int32 // maximum number of m's allowed (or die)
nmsys int32 // number of system m's not counted for deadlock
nmfreed int64 // cumulative number of freed m's
ngsys atomic.Int32 // number of system goroutines
pidle puintptr // idle p's
npidle atomic.Int32
nmspinning atomic.Int32 // See "Worker thread parking/unparking" comment in proc.go.
// Global runnable queue.
runq gQueue
runqsize int32
// Global cache of dead G's.
gFree struct {
lock mutex
stack gList // Gs with stacks
noStack gList // Gs without stacks
n int32
}
}
| 字段 | 用途 |
|---|---|
goidgen |
全局goid的分配器,以保证goid的唯一性。P中的goidcache就是从这里批量获取的。 |
midle |
空闲M链表的链表头 |
nmidle |
空闲M的数量 |
mnext |
记录共创建了多少个M,同时也被用于下一个M的ID |
maxmcount |
允许创建的M的最大数量 |
nmsys |
系统M的数量 |
nmfreed |
统计已经释放的M的数量 |
ngsys |
系统goroutine的数量 |
pidle |
空闲P链表的表头 |
npidle |
空闲P的数量 |
nmspining |
处于自旋状态的M的数量 |
qunq、runqsize |
全局就绪goroutine队列,需要加锁访问 |
gFree |
用来缓存已经退出的g |
2.4 g0、m0
在每

本文详细探讨了Go语言中的程序执行流程,从main函数开始,讲解了Go程序如何通过runtime进行初始化,然后进入调度器的GMP模型,包括G(goroutine)、M(工作线程)和P(处理器)的概念和交互。文章还深入解析了goroutine的创建、调度循环、抢占式调度以及系统监控线程sysmon的角色,展示了Go语言并发执行的内部机制。
最低0.47元/天 解锁文章

被折叠的 条评论
为什么被折叠?



