从源码剖析Go语言基于信号抢占式调度 (4)

atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle):nmspinning 表示正在窃取 G 的数量,npidle 表示空闲 P 的数量,判断是否存在空闲 P 和正在进行调度窃取 G 的 P;

pd.syscallwhen+10*1000*1000 > now:判断是否系统调用时间超过了 10ms ;

Go GC 栈扫描发送抢占信号

GC 相关的内容可以看这篇:《Go语言GC实现原理及源码分析 https://www.luozhiyun.com/archives/475》。Go 在 GC 时对 GC Root 进行标记的时候会扫描 G 的栈,扫描之前会调用 suspendG 挂起 G 的执行才进行扫描,扫描完毕之后再次调用 resumeG 恢复执行。

该函数在:runtime/mgcmark.go:

markroot

func markroot(gcw *gcWork, i uint32) { ... switch { ... // 扫描各个 G 的栈 default: // 获取需要扫描的 G var gp *g if baseStacks <= i && i < end { gp = allgs[i-baseStacks] } else { throw("markroot: bad index") } ... // 转交给g0进行扫描 systemstack(func() { ... // 挂起 G,让对应的 G 停止运行 stopped := suspendG(gp) if stopped.dead { gp.gcscandone = true return } if gp.gcscandone { throw("g already scanned") } // 扫描g的栈 scanstack(gp, gcw) gp.gcscandone = true // 恢复该 G 的执行 resumeG(stopped) }) } }

markroot 在扫描栈之前会切换到 G0 转交给g0进行扫描,然后调用 suspendG 会判断 G 的运行状态,如果该 G 处于 运行状态 _Grunning,那么会设置 preemptStop 为 true 并发送抢占信号。

该函数在:runtime/preempt.go:

suspendG

func suspendG(gp *g) suspendGState { ... const yieldDelay = 10 * 1000 var nextPreemptM int64 for i := 0; ; i++ { switch s := readgstatus(gp); s { ... case _Grunning: if gp.preemptStop && gp.preempt && gp.stackguard0 == stackPreempt && asyncM == gp.m && atomic.Load(&asyncM.preemptGen) == asyncGen { break } if !castogscanstatus(gp, _Grunning, _Gscanrunning) { break } // 设置抢占字段 gp.preemptStop = true gp.preempt = true gp.stackguard0 = stackPreempt asyncM2 := gp.m asyncGen2 := atomic.Load(&asyncM2.preemptGen) // asyncM 与 asyncGen 标记的是循环里 上次抢占的信息,用来校验不能重复抢占 needAsync := asyncM != asyncM2 || asyncGen != asyncGen2 asyncM = asyncM2 asyncGen = asyncGen2 casfrom_Gscanstatus(gp, _Gscanrunning, _Grunning) if preemptMSupported && debug.asyncpreemptoff == 0 && needAsync { now := nanotime() // 限制抢占的频率 if now >= nextPreemptM { nextPreemptM = now + yieldDelay/2 // 执行抢占信号发送 preemptM(asyncM) } } } ... } }

对于 suspendG 函数我只截取出了 G 在 _Grunning 状态下的处理情况。该状态下会将 preemptStop 设置为 true,也是唯一一个地方设置为 true 的地方。preemptStop 和抢占信号的执行有关,忘记的同学可以翻到上面的 asyncPreempt2 函数中。

Go GC StopTheWorld 抢占所有 P

Go GC STW 是通过 stopTheWorldWithSema 函数来执行的,该函数在 runtime/proc.go:

stopTheWorldWithSema

func stopTheWorldWithSema() { _g_ := getg() lock(&sched.lock) sched.stopwait = gomaxprocs // 标记 gcwaiting,调度时看见此标记会进入等待 atomic.Store(&sched.gcwaiting, 1) // 发送抢占信号 preemptall() // 暂停当前 P _g_.m.p.ptr().status = _Pgcstop // Pgcstop is only diagnostic. ... wait := sched.stopwait > 0 unlock(&sched.lock) if wait { for { // 等待 100 us if notetsleep(&sched.stopnote, 100*1000) { noteclear(&sched.stopnote) break } // 再次进行发送抢占信号 preemptall() } } ... }

stopTheWorldWithSema 函数会调用 preemptall 对所有的 P 发送抢占信号。

preemptall 函数的文件位置在 runtime/proc.go:

preemptall

func preemptall() bool { res := false // 遍历所有的 P for _, _p_ := range allp { if _p_.status != _Prunning { continue } // 对正在运行的 P 发送抢占信号 if preemptone(_p_) { res = true } } return res }

preemptall 调用的 preemptone 会将 P 对应的 M 中正在执行的 G 并标记为正在执行抢占;最后会调用 preemptM 向 M 发送抢占信号。

该函数的文件位置在 runtime/proc.go:

preemptone

func preemptone(_p_ *p) bool { // 获取 P 对应的 M mp := _p_.m.ptr() if mp == nil || mp == getg().m { return false } // 获取 M 正在执行的 G gp := mp.curg if gp == nil || gp == mp.g0 { return false } // 将 G 标记为抢占 gp.preempt = true // 在栈扩张的时候会检测是否被抢占 gp.stackguard0 = stackPreempt // 请求该 P 的异步抢占 if preemptMSupported && debug.asyncpreemptoff == 0 { _p_.preempt = true preemptM(mp) } return true }

stw_preempt

总结

到这里,我们完整的看了一下基于信号的抢占调度过程。总结一下具体的逻辑:

程序启动时,在注册 _SIGURG 信号的处理函数 runtime.doSigPreempt;

此时有一个 M1 通过 signalM 函数向 M2 发送中断信号 _SIGURG;

M2 收到信号,操作系统中断其执行代码,并切换到信号处理函数runtime.doSigPreempt;

M2 调用 runtime.asyncPreempt 修改执行的上下文,重新进入调度循环进而调度其他 G;

preempt

Reference

Linux用户抢占和内核抢占详解 https://blog.csdn.net/gatieme/article/details/51872618

sysmon 后台监控线程做了什么 https://www.bookstack.cn/read/qcrao-Go-Questions/goroutine 调度器-sysmon 后台监控线程做了什么.md

Go: Asynchronous Preemption https://medium.com/a-journey-with-go/go-asynchronous-preemption-b5194227371c

Unix信号 https://zh.wikipedia.org/wiki/Unix信号

Linux信号(signal)机制

Golang 大杀器之跟踪剖析 trace https://juejin.cn/post/6844903887757901831

详解Go语言调度循环源码实现 https://www.luozhiyun.com/archives/448

信号处理机制

luozhiyun很酷

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsswsx.html