本文基于小徐先生的编程世界学习GMP模式
1.概念梳理
1.1进程
每个进程都有自己的独立内存空间,拥有自己独立的地址空间、独立的堆和栈,既不共享堆,亦不共享栈。一个程序至少有一个进程,一个进程至少有一个线程。进程切换只发生在内核态。
1.2线程
线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,是由操作系统调度,是操作系统调度(CPU调度)执行的最小单位。对于进程和线程,都是有内核进行调度,有 CPU 时间片的概念, 进行抢占式调度。内核由系统内核进行调度, 系统为了实现并发,会不断地切换线程执行, 由此会带来线程的上下文切换,可充分利用多核,实现并行.
通常语义中的线程,指的是内核级线程,核心点如下:
(1)是操作系统最小调度单元;
(2)创建、销毁、调度交由内核完成,cpu 需完成用户态与内核态间的切换;
(3)可充分利用多核,实现并行.
1.3协程
协程线程一样共享堆,不共享栈,协程是由程序员在协程的代码中显示调度。协程(用户态线程)是对内核透明的, 也就是系统完全不知道有协程的存在, 完全由用户自己的程序进行调度。在栈大小分配方便,且每个协程占用的默认占用内存很小,只有 2kb ,而线程需要 8mb,相较于线程,因为协程是对内核透明的,所以栈空间大小可以按需增大减小。
协程,又称为用户级线程,核心点如下:
(1)与线程存在映射关系,为 M:1;
(2)创建、销毁、调度在用户态完成,对内核透明,所以更轻;
(3)从属同一个内核级线程,无法并行;一个协程阻塞会导致从属同一线程的所有协程无法执行.
1.4Goroutine
Goroutine,经 Golang 优化后的特殊“协程”,核心点如下:
(1)与线程存在映射关系,为 M:N;
(2)创建、销毁、调度在用户态完成,对内核透明,足够轻便;
(3)可利用多个线程,实现并行;
(4)通过调度器的斡旋,实现和线程间的动态绑定和灵活调度;
(5)栈空间大小可动态扩缩,因地制宜.
实际上,“灵活调度” 一词概括得实在过于简要,Golang 在调度 goroutine 时,针对“如何减少加锁行为”,“如何避免资源不均”等问题都给出了精彩的解决方案,这一切都得益于经典的 “gmp” 模型,而这些,就留待第 2 节展开介绍.
1.5对比
三个模型的各项能力对比如下:
2.GMP模型
2.1介绍GMP
实际上,“灵活调度” 一词概括得实在过于简要,Golang 在调度 goroutine 时,针对“如何减少加锁行为”,“如何避免资源不均”等问题都给出了精彩的解决方案,这一切都得益于经典的 “gmp” 模型,而这些,就留待第 2 节展开介绍.
GMP 宏观模型如上图所示,下面对其要点和细节进行逐一介绍:
(1)M 是线程的抽象;G 是 goroutine;P 是承上启下的调度器;
(2)M调度G前,需要和P绑定;
(3)全局有多个M和多个P,但同时并行的G的最大数量等于P的数量;
(4)G的存放队列有三类:P的本地队列;全局队列;和wait队列(图中未展示,为io阻塞就绪态goroutine队列);
(5)M调度G时,优先取P本地队列,其次取全局队列,最后取wait队列;这样的好处是,取本地队列时,可以接近于无锁化,减少全局锁竞争;
(6)为防止不同P的闲忙差异过大,设立work-stealing机制,本地队列为空的P可以尝试从其他P本地队列偷取一半的G补充到自身队列.
2.2概念
2.2.1g
(1)g 即goroutine,是 golang 中对协程的抽象;
(2)g 有自己的运行栈、状态、以及执行的任务函数(用户通过 go func 指定);
(3)g 需要绑定到 p 才能执行,在 g 的视角中,p 就是它的 cpu.
2.2.2p
(1)p 即 processor,是 golang 中的调度器;
(2)p 是 gmp 的中枢,借由 p 承上启下,实现 g 和 m 之间的动态有机结合;
(3)对 g 而言,p 是其 cpu,g 只有被 p 调度,才得以执行;
(4)对 m 而言,p 是其执行代理,为其提供必要信息的同时(可执行的 g、内存分配情况等),并隐藏了繁杂的调度细节;
(5)p 的数量决定了 g 最大并行数量,可由用户通过 GOMAXPROCS 进行设定(超过 CPU 核数时无意义).
问题:为什么 GOMAXPROCS 不能超过 CPU 核数?
这是因为:
- 操作系统层面,一个 CPU 核心同一时间只能执行一个线程(OS 线程)。
- 在 Golang 运行时中,每个 P 都会被绑定到一个 OS 线程上。
- 因此,GOMAXPROCS 设置的 P 的数量,不能超过 CPU 核心数,否则就会出现某些 P 没有 OS 线程可以运行,导致资源浪费和性能下降。
2.2.3m
(1)m 即 machine,是 golang 中对线程的抽象;
(2)m 不直接执行 g,而是先和 p 绑定,由其实现代理;
(3)借由 p 的存在,m 无需和 g 绑死,也无需记录 g 的状态信息,因此 g 在全生命周期中可以实现跨 m 执行.
3.源码走读.数据结构
gmp 数据结构定义为 runtime/runtime2.go 文件中
G
type g struct {
// ...
m *m
// ...
sched gobuf
// ...
}
type gobuf struct {
sp uintptr
pc uintptr
ret uintptr
bp uintptr // for framepointer-enabled architectures
}
G,协程。通常在代码里用 go 关键字执行一个方法,那么就等于起了一个G。
数据结构解析:
(1)m:在 p 的代理,负责执行当前 g 的 m;
(2)sched.sp:保存 CPU 的 rsp 寄存器的值,指向函数调用栈栈顶;
(3)sched.pc:保存 CPU 的 rip 寄存器的值,指向程序下一条执行指令的地址;
(4)sched.ret:保存系统调用的返回值;
(5)sched.bp:保存 CPU 的 rbp 寄存器的值,存储函数栈帧的起始位置.
生命周期:
(1)_Gidle 值为 0,为协程开始创建时的状态,此时尚未初始化完成;
(2)_Grunnable 值 为 1,协程在待执行队列中,等待被执行;
(3)_Grunning 值为 2,协程正在执行,同一时刻一个 p 中只有一个 g 处于此状态;
(4)_Gsyscall 值为 3,协程正在执行系统调用;
(5)_Gwaiting 值为 4,协程处于挂起态,需要等待被唤醒. gc、channel 通信或者锁操作时经常会进入这种状态;
(6)_Gdead 值为 6,协程刚初始化完成或者已经被销毁,会处于此状态;
(7)_Gcopystack 值为 8,协程正在栈扩容流程中;
(8)_Greempted 值为 9,协程被抢占后的状态.
M
type m struct {
g0 *g // goroutine with scheduling stack
// ...
tls [tlsSlots]uintptr // thread-local storage (for x86 extern register)
// ...
}
数据结构解析:
(1)g0:一类特殊的调度协程,不用于执行用户函数,负责执行 g 之间的切换调度. 与 m 的关系为 1:1;
(2)tls:thread-local storage,线程本地存储,存储内容只对当前线程可见. 线程本地存储的是 m.tls 的地址,m.tls[0] 存储的是当前运行的 g,因此线程可以通过 g 找到当前的 m、p、g0 等信息.
P
type p struct {
// ...
runqhead uint32
runqtail uint32
runq [256]guintptr
runnext guintptr
// ...
}
数据结构解析:
(1)runq:本地 goroutine 队列,最大长度为 256.
(2)runqhead:队列头部;
(3)runqtail:队列尾部;
(4)runnext:下一个可执行的 goroutine.
schedf(全局,有锁)
type schedt struct {
// ...
lock mutex
// ...
runq gQueue
runqsize int32
// ...
}
sched 是全局 goroutine 队列的封装:
(1)lock:一把操作全局队列时使用的锁;
(2)runq:全局 goroutine 队列;
(3)runqsize:全局 goroutine 队列的容量.
4.源码走读.动态调度流程
4.1m的两种g转换
goroutine 的类型可分为两类:
I 负责调度普通 g 的 g0,执行固定的调度流程,与 m 的关系为一对一;
II 负责执行用户函数的普通 g.
m 通过 p 调度执行的 goroutine 永远在普通 g 和 g0 之间进行切换,当 g0 找到可执行的 g 时,会调用 gogo 方法,调度 g 执行用户定义的任务;当 g 需要主动让渡或被动调度时,会触发 mcall 方法,将执行权重新交还给 g0.
func gogo(buf *gobuf) // …
func mcall(fn func(*g))
4.2p的四种调度类型
狭义的调度:由 g0 按照特定策略找到下一个可执行 g 的过程
广义的调度:调度器 p 实现从执行一个 g 切换到另一个 g 的过程
(1)主动调度:一种用户主动执行让渡的方式,主要方式是,用户在执行代码中调用了 runtime.Gosched 方法,此时当前 g 会当让出执行权,主动进行队列等待下次被调度执行.
func Gosched() {
checkTimeouts()//这个函数会检查当前 Goroutine 是否有超时等待的情况,如果有,会将这些 Goroutine 从睡眠状态唤醒。
mcall(gosched_m)//mcall() 函数是 Go 运行时中的一个底层函数,它的作用是切换当前 M(OS 线程)所执行的 G(Goroutine)。gosched_m() 是 mcall() 的回调函数,它负责实现 Goroutine 的调度逻辑。
}
gosched_m() :保存当前 Goroutine 的执行上下文(寄存器、栈等)。将当前 Goroutine 放入 runnext 队列。从 G 队列中取出下一个待执行的 Goroutine。恢复新 Goroutine 的执行上下文,切换到新的 Goroutine 继续执行。
(2)被动调度:因当前不满足某种执行条件,g 可能会陷入阻塞态无法被调度,直到关注的条件达成后,g才从阻塞中被唤醒,重新进入可执行队列等待被调度.
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
// ...
mcall(park_m)
}
func goready(gp *g, traceskip int) {
systemstack(func() {
ready(gp, traceskip, true)
})
}
(3)正常调度:g 中的执行任务已完成,g0 会将当前 g 置为死亡状态,发起新一轮调度.
(4)抢占调度:倘若 g 执行系统调用超过指定的时长,且全局的 p 资源比较紧缺,此时将 p 和 g 解绑,抢占出来用于其他 g 的调度. 等 g 完成系统调用后,会重新进入可执行队列中等待被调度.
值得一提的是,前 3 种调度方式都由 m 下的 g0 完成,唯独抢占调度不同.
因为发起系统调用时需要打破用户态的边界进入内核态,此时 m 也会因系统调用而陷入僵直,无法主动完成抢占调度的行为.
因此,在 Golang 进程会有一个全局监控协程 monitor g 的存在,这个 g 会越过 p 直接与一个 m 进行绑定,不断轮询对所有 p 的执行状况进行监控. 倘若发现满足抢占调度的条件,则会从第三方的角度出手干预,主动发起该动作.
(1)以 g0 -> g -> g0 的一轮循环为例进行串联;
(2)g0 执行 schedule() 函数,寻找到用于执行的 g;
(3)g0 执行 execute() 方法,更新当前 g、p 的状态信息,并调用 gogo() 方法,将执行权交给 g;
(4)g 因主动让渡( gosche_m() )、被动调度( park_m() )、正常结束( goexit0() )等原因,调用 m_call 函数,执行权重新回到 g0 手中;
(5)g0 执行 schedule() 函数,开启新一轮循环.
4.3schedule
func schedule() {
// ...
gp, inheritTime, tryWakeP := findRunnable() // blocks until work is available
// ...
execute(gp, inheritTime)
}
4.3.1findRunnabl
func findRunnable() (gp *g, inheritTime, tryWakeP bool) {
_g_ := getg()//获取当前 Goroutine 的 g 结构体指针。
top:
_p_ := _g_.m.p.ptr()//用来获取当前 Goroutine 所在的 P(Processor)的指针。
// ...
if _p_.schedtick%61 == 0 && sched.runqsize > 0 {//经过了一个调度周期61,
lock(&sched.lock)//检查全局运行队列是否有待运行的 Goroutine。如果全局队列不为空,那么就需要从中取出 Goroutine 并添加到当前 P 的本地队列中,
gp = globrunqget(_p_, 1)//从全局运行队列中取出1个 Goroutine,并赋值给变量 gp。
unlock(&sched.lock)
if gp != nil {
return gp, false, false
}
}
// ...
if gp, inheritTime := runqget(_p_); gp != nil {
return gp, inheritTime, false//从当前 P 的本地运行队列中取出一个 Goroutine 的。
}
// ...
if sched.runqsize != 0 {
lock(&sched.lock)
gp := globrunqget(_p_, 0)//取出多个 Goroutine
unlock(&sched.lock)
if gp != nil {
return gp, false, false
}
}
//当系统中存在网络相关的 Goroutine 时,会优先尝试从网络轮询队列中取出已就绪的 Goroutine,避免这些 Goroutine 长时间阻塞在网络 I/O 操作上
// 网络轮询已经初始化完成。有 Goroutine 正在等待网络事件。近一次网络轮询是否有发生。
if netpollinited() && atomic.Load(&netpollWaiters) > 0 && atomic.Load64(&sched.lastpoll) != 0 {
if list := netpoll(0); !list.empty() { // non-blocking一次非阻塞的网络轮询。如果有已就绪的 Goroutine,会被放到一个链表中返回。
gp := list.pop()
injectglist(&list)
需要注意的是,刚获取网络协程时,g 的状态是处于 waiting 的,因此需要先更新为 runnable 状态.
casgstatus(gp, _Gwaiting, _Grunnable)
return gp, false, false
}
}
// ...
procs := uint32(gomaxprocs)
if _g_.m.spinning || 2*atomic.Load(&sched.nmspinning) < procs-atomic.Load(&sched.npidle) {
if !_g_.m.spinning {
_g_.m.spinning = true
atomic.Xadd(&sched.nmspinning, 1)
}
尝试从其他 P 上偷取 Goroutine。
gp, inheritTime, tnow, w, newWork := stealWork(now)
now = tnow
if gp != nil {
// Successfully stole.
return gp, inheritTime, false
}
说明可能有新的定时器或 GC 工作需要处理,于是跳转回 top 重新检查一遍。
if newWork {
// There may be new timer or GC work; restart to
// discover.
goto top
}
如果没有成功偷取到 Goroutine,但获得了一个较早的定时器事件
就将其记录下来,以便之后进行网络轮询。
if w != 0 && (pollUntil == 0 || w < pollUntil) {
// Earlier timer to wait for.
pollUntil = w
}
}
//
4.3.2globrunqget
func globrunqget(_p_ *p, max int32) *g {
if sched.runqsize == 0 {
return nil
}
n := sched.runqsize/gomaxprocs + 1
if n > sched.runqsize {
n = sched.runqsize
}
if max > 0 && n > max {
n = max
}
if n > int32(len(_p_.runq))/2 {
n = int32(len(_p_.runq)) / 2
}
sched.runqsize -= n
gp := sched.runq.pop()
n--
for ; n > 0; n-- {
将一个 g 由全局队列转移到 p 本地队列的执行逻辑位于 runqput 方法中:
gp1 := sched.runq.pop()
runqput(_p_, gp1, false)
}
return gp
func runqput(_p_ *p, gp *g, next bool) {
// ...
retry:
I 取得 p 本地队列队首的索引,同时对本地队列加锁:
h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with consumers
t := _p_.runqtail
II 倘若 p 的局部队列未满,则成功转移 g,将 p 的对尾索引 runqtail 值加 1 并解锁队列.
if t-h < uint32(len(_p_.runq)) {
_p_.runq[t%uint32(len(_p_.runq))].set(gp)
atomic.StoreRel(&_p_.runqtail, t+1) // store-release, makes the item available for consumption
return
}
III 倘若发现本地队列 runq 已经满了,
则会返回来将本地队列中一半的 g 放回全局队列中,帮助当前 p 缓解执行压力,这部分内容位于 runqputslow 方法中.
if runqputslow(_p_, gp, h, t) {
return
}
// the queue is not full, now the put above must succeed
goto retry
func runqputslow(_p_ *p, gp *g, h, t uint32) bool {
var batch [len(_p_.runq)/2 + 1]*g
// First, grab a batch from local queue.
n := t - h
n = n / 2
// ...
for i := uint32(0); i < n; i++ {
batch[i] = _p_.runq[(h+i)%uint32(len(_p_.runq))].ptr()
}
if !atomic.CasRel(&_p_.runqhead, h, h+n) { // cas-release, commits consume
return false
}
batch[n] = gp
// Link the goroutines.
for i := uint32(0); i < n; i++ {
batch[i].schedlink.set(batch[i+1])
}
var q gQueue
q.head.set(batch[0])
q.tail.set(batch[n])
// Now put the batch on global queue.
lock(&sched.lock)
globrunqputbatch(&q, int32(n+1))
unlock(&sched.lock)
return true
4.3.3 runqget
func runqget(_p_ *p) (gp *g, inheritTime bool) {
倘若当前 p 的 runnext 非空,直接获取即可:
if next != 0 && _p_.runnext.cas(next, 0) {
return next.ptr(), true
}
for {
II 加锁从 p 的本地队列中获取 g.
需要注意,虽然本地队列是属于 p 独有的,但是由于 work-stealing 机制的存在,其他 p 可能会前来执行窃取动作,因此操作仍需加锁.
但是,由于窃取动作发生的频率不会太高,因此当前 p 取得锁的成功率是很高的,因此可以说p 的本地队列是接近于无锁化,但没有达到真正意义的无锁.
h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with other consumers
t := _p_.runqtail
III 倘若本地队列为空,直接终止并返回;
if t == h {
return nil, false
}
gp := _p_.runq[h%uint32(len(_p_.runq))].ptr()
if atomic.CasRel(&_p_.runqhead, h, h+1) { // cas-release, commits consume
IV 倘若本地队列存在 g,则取得队首的 g,解锁并返回.
return gp, false
}
}
4.3.4stealWork
func stealWork(now int64) (gp *g, inheritTime bool, rnow, pollUntil int64, newWork bool) {
pp := getg().m.p.ptr()
ranTimer := false
偷取操作至多会遍历全局的 p 队列 4 次,过程中只要找到可窃取的 p 则会立即返回.
const stealTries = 4
for i := 0; i < stealTries; i++ {
stealTimersOrRunNextG := i == stealTries-1
为保证窃取行为的公平性,遍历的起点是随机的. 窃取动作的核心逻辑位于 runqgrab 方法当中:
for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() {
// ...
}
}
return nil, false, now, pollUntil, ranTime
func runqgrab(_p_ *p, batch *[256]guintptr, batchHead uint32, stealRunNextG bool) uint32 {
for {
I 每次对一个 p 尝试窃取前,会对其局部队列加锁;
h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with other consumers
t := atomic.LoadAcq(&_p_.runqtail) // load-acquire, synchronize with the producer
II 尝试偷取其现有的一半 g,并且返回实际偷取的数量.
n := t - h
n = n - n/2
if n == 0 {
if stealRunNextG {
// Try to steal from _p_.runnext.
if next := _p_.runnext; next != 0 {
if _p_.status == _Prunning {
if GOOS != "windows" && GOOS != "openbsd" && GOOS != "netbsd" {
usleep(3)
} else {
osyield()
}
}
if !_p_.runnext.cas(next, 0) {
continue
}
batch[batchHead%uint32(len(batch))] = next
return 1
}
}
return 0
}
if n > uint32(len(_p_.runq)/2) { // read inconsistent h and t
continue
}
for i := uint32(0); i < n; i++ {
g := _p_.runq[(h+i)%uint32(len(_p_.runq))]
batch[(batchHead+i)%uint32(len(batch))] = g
}
if atomic.CasRel(&_p_.runqhead, h, h+n) { // cas-release, commits consume
return n
}
}
}
4.3.5excute
func execute(gp *g, inheritTime bool) {
_g_ := getg()
建立 g 与 m 之间的绑定关系;
_g_.m.curg = gp
gp.m = _g_.m
更新 g 的状态信息
casgstatus(gp, _Grunnable, _Grunning)
gp.waitsince = 0
gp.preempt = false
gp.stackguard0 = gp.stack.lo + _StackGuard
if !inheritTime {
更新 p 的总调度次数;
_g_.m.p.ptr().schedtick++
}
调用 gogo 方法,执行 goroutine 中的任务.
gogo(&gp.sched)
4.4gosched_m主动调度
g 执行主动让渡时,会调用 mcall 方法将执行权归还给 g0,并由 g0 调用 gosched_m 方法,位于 runtime/proc.go 文件中
func Gosched() {
// ...
mcall(gosched_m)
}
func gosched_m(gp *g) {
goschedImpl(gp)
}
func goschedImpl(gp *g) {
status := readgstatus(gp)
if status&^_Gscan != _Grunning {
dumpgstatus(gp)
throw("bad g status")
}
(1)将当前 g 的状态由执行中切换为待执行 _Grunnable:
casgstatus(gp, _Grunning, _Grunnable)
(2)调用 dropg() 方法,将当前的 m 和 g 解绑;
dropg()
(3)将 g 添加到全局队列当中
lock(&sched.lock)
globrunqput(gp)
unlock(&sched.lock)
(4)开启新一轮的调度:
schedule()
## 4.6park_m 与 ready被动调度
g 需要被动调度时,会调用 mcall 方法切换至 g0,并调用 park_m 方法将 g 置为阻塞态,执行流程位于 runtime/proc.go 的 gopark 方法当中
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
// ...
mcall(park_m)
}
func park_m(gp *g) {
_g_ := getg()
(1)将当前 g 的状态由 running 改为 waiting;
casgstatus(gp, _Grunning, _Gwaiting)
(2)将 g 与 m 解绑;//上层channel等来实现与ready的成对调用
dropg()
(3)执行新一轮的调度 schedule.
// ...
schedule()
当因被动调度陷入阻塞态的 g 需要被唤醒时,会由其他协程执行 goready 方法将 g 重新置为可执行的状态,方法位于 runtime/proc.go .
被动调度如果需要唤醒,则会其他 g 负责将 g 的状态由 waiting 改为 runnable,然后会将其添加到唤醒者的 p 的本地队列中:
func goready(gp *g, traceskip int) {
systemstack(func() {
ready(gp, traceskip, true)
})
}
func ready(gp *g, traceskip int, next bool) {
// ...
_g_ := getg()
// ...
(1)先将 g 的状态从阻塞态改为可执行的状态;
casgstatus(gp, _Gwaiting, _Grunnable)
2)调用 runqput 将当前 g 添加到唤醒者 p 的本地队列中,如果队列满了,会连带 g 一起将一半的元素转移到全局队列.
runqput(_g_.m.p.ptr(), gp, next)
// ...
}
4.7 goexit0正常调度
当 g 执行完成时,会先执行 mcall 方法切换至 g0,然后调用 goexit0 方法,内容为 runtime/proc.go:
func goexit1() {
// ...
mcall(goexit0)
}
func goexit0(gp *g) {
_g_ := getg()
_p_ := _g_.m.p.ptr()
(1)将 g 状态置为 dead;
casgstatus(gp, _Grunning, _Gdead)
// ...
gp.m = nil
// ...
(2)解绑 g 和 m;
dropg()
(3)开启新一轮的调度.
// ...
schedule()
4.8retake抢占调度
抢占调度的执行者不是 g0,而是一个全局的 monitor g,代码位于 runtime/proc.go 的 retake 方法中
func retake(now int64) uint32 {
n := 0
(1)加锁后,遍历全局的 p 队列,寻找需要被抢占的目标:
lock(&allpLock)
for i := 0; i < len(allp); i++ {
_p_ := allp[i]
if _p_ == nil {
// This can happen if procresize has grown
// allp but not yet created new Ps.
continue
}
pd := &_p_.sysmontick
// ...
if s == _Psyscall {
// ...
(2)倘若某个 p 同时满足下述条件,则会进行抢占调度:
I 执行系统调用超过 10 ms;
II p 本地队列有等待执行的 g;
III 或者当前没有空闲的 p 和 m.
if runqempty(_p_) && atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 && pd.syscallwhen+10*1000*1000 > now {
continue
}
unlock(&allpLock)
先将当前 p 的状态更新为 idle
if atomic.Cas(&_p_.status, s, _Pidle) {
n++
_p_.syscalltick++
handoffp(_p_)步入 handoffp 方法中,判断是否需要为 p 寻找接管的 m(因为其原本绑定的 m 正在执行系统调用)
}
incidlelocked(1)
lock(&allpLock)
}
}
unlock(&allpLock)
return uint32(n)
}
四个条件满足其一时,则需要为 p 获取新的 m:
func handoffp(_p_ *p) {
I 当前 p 本地队列还有待执行的 g;
if !runqempty(_p_) || sched.runqsize != 0 {
startm(_p_, false)
return
}
II 全局繁忙(没有空闲的 p 和 m,全局 g 队列为空)
if atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) == 0 && atomic.Cas(&sched.nmspinning, 0, 1) {
startm(_p_, true)
return
}
lock(&sched.lock)
// ...
if sched.runqsize != 0 {
unlock(&sched.lock)
startm(_p_, false)
return
}
// If this is the last running P and nobody is polling network,
// need to wakeup another M to poll network.
III 需要处理网络 socket 读写请求
if sched.npidle == uint32(gomaxprocs-1) && atomic.Load64(&sched.lastpoll) != 0 {
unlock(&sched.lock)
startm(_p_, false)
return
}
// ...
为 p 获取新的 m:
会先尝试获取已有的空闲的 m,若不存在,则会创建一个新的 m.
func startm(_p_ *p, spinning bool) {
mp := acquirem()
lock(&sched.lock)
// ...
nmp := mget()
if nmp == nil {
id := mReserveID()
unlock(&sched.lock)
var fn func()
// ...
newm(fn, _p_, id)
// ...
return
}
unlock(&sched.lock)
// ...
}
4.9 reentersyscall 和 exitsyscall系统调用
视角切换回发生系统调用前,与 g 绑定的原 m 当中.在 m 需要执行系统调用前,会先执行位于 runtime/proc.go 的 reentersyscall 的方法:
func reentersyscall(pc, sp uintptr) {
(1)此时执行权同样位于 m 的 g0 手中;
_g_ := getg()
// ...
(2)保存当前 g 的执行环境;
save(pc, sp)
_g_.syscallsp = sp
_g_.syscallpc = pc
(3)将 g 和 p 的状态更新为 syscall;
casgstatus(_g_, _Grunning, _Gsyscall)
// ...
(4)解除 p 和 当前 m 之间的绑定,因为 m 即将进入系统调用而导致短暂不可用;
pp := _g_.m.p.ptr()
pp.m = 0
(5)将 p 添加到 当前 m 的 oldP 容器当中,后续 m 恢复后,会优先寻找旧的 p 重新建立绑定关系.
_g_.m.oldp.set(pp)
_g_.m.p = 0
atomic.Store(&pp.status, _Psyscall)
// ...
当 m 完成了内核态的系统调用之后,此时会步入位于 runtime/proc.go 的 exitsyscall 函数中,尝试寻找 p 重新开始运作:
func exitsyscall() {
_g_ := getg()
// ...
(1)方法执行之初,此时的执行权是普通 g.倘若此前设置的 oldp 仍然可用,则重新和 oldP 绑定,
将当前 g 重新置为 running 状态,然后开始执行后续的用户函数;
if exitsyscallfast(oldp) {
// ...
casgstatus(_g_, _Gsyscall, _Grunning)
// ...
return
}
// ...
(2)old 绑定失败,则调用 mcall 方法切换到 m 的 g0,并执行 exitsyscall0 方法:
mcall(exitsyscall0)
// ...
}
func exitsyscall0(gp *g) {
(3)将 g 由系统调用状态切换为可运行态,并解绑 g 和 m 的关系:
casgstatus(gp, _Gsyscall, _Grunnable)
dropg()
(4)从全局 p 队列获取可用的 p,如果获取到了,则执行 g:
lock(&sched.lock)
var _p_ *p
if schedEnabled(gp) {
_p_, _ = pidleget(0)
}
var locked bool
(5)如若无 p 可用,则将 g 添加到全局队列,当前 m 陷入沉睡. 直到被唤醒后才会继续发起调度.
if _p_ == nil {
globrunqput(gp)
}
unlock(&sched.lock)
if _p_ != nil {
acquirep(_p_)
execute(gp, false) // Never returns.
}
// ...
stopm()
schedule() // Never returns.
}