写在开头
非原创,知识搬运工/整合工,仅用于自己学习,本文主要介绍GMP和几个核心结构体
往期回顾
带着问题去阅读
1.runtime
复习一下上一章节的内容![](https://img.haomeiwen.com/i20602965/e34e134a98fb0626.png)
GO程序执行由两层组成,分别是用户程序和运行时runtime,runtime直接和内核打交道,实际上go的runtime包含4个模块:
- GO Scheduler,负责调度管理协程(GMP)
- netpoll,网络轮询
- memory 内存分配
- garbage 垃圾回收
1.1 GMP模型
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.
这是runtime.proc.go对GMP的描述
在同一时间点上,一个处理器CPU的一个核只能执行一个线程任务,我们把P看作是一个虚拟的核,它是个抽象的概念,那么核上运行的线程就是M,协程G要依赖线程才能执行。
我们前面学过现实中线程核协程是M:N的关系,即可以有M个线程,每个线程上有N个协程,但是线程是给协程分配资源,最后是CPU核执行的。但是在GMP模型中,最后的执行者是线程M,这需要区分,处理器核P只是用来存放可以被运行的G,M从P中获取G,协程才得以执行(最初是没有P这个概念,直接从全局队列获取G)。
GO程序启动后,会给每一个CPU核分配一个P
runtime.NumCPU()
该函数返回当前进程可以用到的核数量,不是物理核心数,
分配好P后,再给每一个P分配一个M,但是这些M仍由操作系统调度器OS Scheduler调度(G才是用户调度的),G在M上执行,M在CPU核(这里指实际的核,不是P)上调度,G也是在M上调度的
1.2 运行队列
我们前面提到了两个队列,一个是全局队列GRQ(现在还有),另一个是私有队列LRQ(P上私有),全局队列上的G还没有分配到具体的P![](https://img.haomeiwen.com/i20602965/a6b31f245ee1b510.png)
1.3 协程的状态
协程和线程类似,有三种状态:
- Waiting 等待状态,如g被阻塞等待某个时间发生,典型的如等待网络数据
- Runable 就绪状态,只要给M就能直接运行
- Executing 运行状态,正在M上执行指令
1.4协程的调度时机
在四种情况下,G可能发生调度,也不一定会发生(有机会被调度)
- 使用关键词go 创建一个新G,调度器会考虑调度
- GC 前一节学过,GC垃圾回收是程序开始会创建的一个G,也要在M上运行,触发GC意味着要调度,处理堆上的内存
- 系统调用 当G进行系统调用时会被阻塞等待,还记得协程调度器的核心目的吗?榨干线程M,那自然不允许它浪费,调走该G
- 内存同步访问 如atomic,mutex,channel操作都会阻塞G,本质上也是系统调用
1.5同步/异步系统调用
这里指G进行系统调用的类型:
-
同步,G被阻塞,M也被阻塞,那么就要从P上调度下来,但是该G仍依附于原来的M(G的执行是在M上),之后一个新的M会被调用到原来的P上去执行其他G。一旦系统调用完之前的G加入到P的私有队列LRQ中,原来的G被“雪藏”(休眠),之后等时机调用
image.png
-
异步,G未阻塞,M也未阻塞(如多路复用),G的请求被netpoll网络轮询接手,G不再依附M,依附于netpoll,等待结束再返回LRQ
image.png
异步情况,是将I/O任务变成CPU任务
2.GMP数据结构
GO调度器三个核心基础数据结构(这也验证了前文的说法,协程就是一个特殊的数据结构),下面都是1.20版本
2.1 G struct
- G结构体,这个是简化版
type g struct {
stack stack // offset known to runtime/cgo
stackguard0 uintptr // offset known to liblink
stackguard1 uintptr // offset known to liblink
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
m *m // current m; offset known to arm liblink
sched gobuf
timer *timer
gopc uintptr
- stack,看到stack我们就明白了,协程应该和线程一样有自己的栈,这里初始化的栈空间在2K左右,表示一个用户代码的执行流,即用户栈,线程拥有的栈叫做系统栈,stack很简单,栈顶和栈底
type stack struct {
lo uintptr
hi uintptr
}
typedef unsigned long long int uint64;
typedef uint64 uintptr;
- stackguard0和stackgruad1都是描述栈的,问题来了G是可以有千万个的,内存会不会很快用光?初始内存设为2k就是不让栈拥有太大的内存空间,这个内存也会根据G的调用链动态增加的
- _panic和_defer 是等待被执行的函数链表,defer很熟悉了吧
- m,G在M上执行,当然要绑定一个m
- timer ,定时器
- gopc是创建该g的指令地址
- sched等是G运行时的现场数据
type gobuf struct {
sp uintptr
pc uintptr
g guintptr
ctxt unsafe.Pointer
ret uintptr
lr uintptr
bp uintptr // for framepointer-enabled architectures
}
G的运行不光需要栈,还需要SP和PC等寄存器
G的状态当g被调离CPU时,调度器会把CPU寄存器的值保存在g的gobuf类型对象成员中
被调度运行时,把gobuf保存的变量恢复到寄存器中
![](https://img.haomeiwen.com/i20602965/0510deed8107f311.png)
2.2 M struct
M结构体,也省略了很多
type m struct {
g0 *g // goroutine with scheduling stack
curg *g // current running goroutine
p puintptr // attached p for executing go code (nil if not executing go code)
nextp puintptr
oldp puintptr // the p that was attached before executing a syscall
id int64
- curg表示正在运行的G
- p表示m绑定的p,m和p的关系是1:1
- g0 是一个特殊的G
m的状态有两种
![](https://img.haomeiwen.com/i20602965/6d2a65c55962666e.png)
- 自旋:找G去工作,M会从与他绑定的P获取G,也会从netpoll获取G(异步系统调用),还会从其他P中偷取G
- 非自旋:没工作睡大觉,之后会进入休眠
2.3 特殊的g0
我们在前一章学习了调度器的第二个核心思想,要限制同时运行的线程数N,N为CPU核数(当前进程可获取的核数),m执行G,那么m如何调度G?这个调度功能委托给g0,它是在创建每一个线程M时,都会自动创建的第一个协程G,代号为0![](https://img.haomeiwen.com/i20602965/e970e5603b374a81.png)
并且调度不是它唯一的工作,它有着固定且更大的栈空间
-
G的创建,使用关键词go 创建协程实际是委托给g0创建
image.png
我们看到新的G放在队列头部,优先级更高,这也解释了为什么创建协程有机会触发调度
- defer函数分配
- 垃圾回收
- 栈增长
运筹帷幄,总览全局,GMP的G明明应该是g0的g呀
2.4 P结构体
type p struct {
m muintptr // back-link to associated m (nil if idle)
// Queue of runnable goroutines. Accessed without lock.
runqhead uint32
runqtail uint32
runq [256]guintptr
schedtick uint32
P的状态
![](https://img.haomeiwen.com/i20602965/fafff94e35399b01.png)
- Pgcstop:程序刚初始化时,都为该状态,初始化完成后变为Pidle,发生GC也会变成该状态
- Pidle
- Pruning: M需要运行时,绑定的P为该状态
- psyscall:G调用系统调用阻塞,设置P状态,
`#参考
1.GMP是什么
2.GO scheduler
3.程序员面试笔记
4.特殊的g0
网友评论