从 Go 1.14 开始,通过使用信号,Go 语言实现了调度和 GC 过程中的真“抢占“。
抢占流程由抢占的发起方向被抢占线程发送 SIGURG 信号。
当被抢占线程收到信号后,进入 SIGURG 的处理流程,将 asyncPreempt 的调用强制插入到用户当前执行的代码位置。
本节会对该过程进行详尽分析。
抢占发起的时机
抢占会在下列时机发生:
- STW 期间
- 在 P 上执行 safe point 函数期间
- sysmon 后台监控期间
- gc pacer 分配新的 dedicated worker 期间
- panic 崩溃期间
除了栈扫描,所有触发抢占最终都会去执行 preemptone 函数。栈扫描流程比较特殊:
从这些流程里,我们挑出三个来一探究竟。
STW 抢占
上图是现在 Go 语言的 GC 流程图,在两个 STW 阶段都需要将正在执行的线程上的 running 状态的 goroutine 停下来。
func stopTheWorldWithSema() {
.....
preemptall()
.....
// 等待剩余的 P 主动停下
if wait {
for {
// wait for 100us, then try to re-preempt in case of any races
// 等待 100us,然后重新尝试抢占
if notetsleep(&sched.stopnote, 100*1000) {
noteclear(&sched.stopnote)
break
}
preemptall()
}
}
GC 栈扫描
goroutine 的栈是 GC 扫描期间的根,所有 markroot 中需要将用户的 goroutine 停下来,主要是 running 状态:
func markroot(gcw *gcWork, i uint32) {
// Note: if you add a case here, please also update heapdump.go:dumproots.
switch {
......
default:
// the rest is scanning goroutine stacks
var gp *g
......
// scanstack must be done on the system stack in case
// we're trying to scan our own stack.
systemstack(func() {
stopped := suspendG(gp)
scanstack(gp, gcw)
resumeG(stopped)
})
}
}
suspendG 中会调用 preemptM -> signalM 对正在执行的 goroutine 所在的线程发送抢占信号。
sysmon 后台监控
func sysmon() {
idle := 0 // how many cycles in succession we had not wokeup somebody
for {
......
// retake P's blocked in syscalls
// and preempt long running G's
if retake(now) != 0 {
idle = 0
} else {
idle++
}
}
}
执行 syscall 太久的,需要将 P 从 M 上剥离;运行用户代码太久的,需要抢占停止该 goroutine 执行。这里我们只看抢占 goroutine 的部分:
const forcePreemptNS = 10 * 1000 * 1000 // 10ms
func retake(now int64) uint32 {
......
for i := 0; i < len(allp); i++ {
_p_ := allp[i]
s := _p_.status
if s == _Prunning || s == _Psy