开启系统监控
#
这个是main goroutine执行的主函数,此函数之后会调用用户定义main函数,我们在初始化一章已经提过
main goroutine执行的主函数
它的里面可以看到调用了newm函数生成新M,且新M将执行sysmon函数
func main() {
//...
if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
systemstack(func() {
newm(sysmon, nil)
})
}
//...
}
sysmon
#
可以看到sysmon里面是一个for循环,它会一直运行,因为这个M没有关联P,所以不允许写屏障
逻辑比较简单,延时睡眠,调用retake
// Always runs without a P, so write barriers are not allowed.
//
//go:nowritebarrierrec
func sysmon() {
lock(&sched.lock)
sched.nmsys++ //增加记录系统线程的值的个数
checkdead()
unlock(&sched.lock)
lasttrace := int64(0)
idle := 0 // how many cycles in succession we had not wokeup somebody
delay := uint32(0)
for {
if idle == 0 { // start with 20us sleep...
delay = 20
} else if idle > 50 { // start doubling the sleep after 1ms...
delay *= 2
}
if delay > 10*1000 { // up to 10ms
delay = 10 * 1000
}
usleep(delay)
//...
// retake P's blocked in syscalls
// and preempt long running G's
// 抢占被系统调用阻塞的P和抢占长期运行的G
if retake(now) != 0 {
idle = 0
} else {
idle++
}
// check if we need to force a GC
//...
}
}
retake
#
retake是怎么区分是否是本次调度一直在运行?
- 通过p结构体里面的sysmontick,快照p结构体中schedtick,所以下次再比较两者,参见下面的16-31行
type sysmontick struct {
schedtick uint32
schedwhen int64
syscalltick uint32
syscallwhen int64
}
retake怎么判断是否应该抢断?
- 只有P是
_Prunning/_Psyscall
状态,才会进行抢占 - 一种是用户代码运行太久会被抢占(参见下方的25行):如果
pd.schedwhen ~ now
这个时间段大于阀值forcePreemptNS
,就会调用preemptone函数做抢断准备 - 一种是进入了系统调用的抢占:主要思想是如果P接下来没有其他工作可做(本地G队列为空了),这时候抢占它没有意义,但为了防止sysmon线程深度睡眠(上文有分析retake的返回参数能决定sysmon的睡眠时长)
retake函数判断不进行系统剥夺抢占逻辑,由第50行代码决定: runqempty(_p_) && atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 && pd.syscallwhen+10*1000*1000 > now
- _p_的本地运行队列没有Gs; runqempty(p)返回true
- 有空闲的P,或者有正在自旋状态的M(正在偷其他P队列的Gs); atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0返回true
- 上次观测到的系统调用还没有超过10毫秒; pd.syscallwhen+1010001000 > now返回true
所以当程序没有工作需要做,且系统调用没有超过10ms就不进行系统调用抢占; 上式前两项说明这个程序没有工作需要做; 最后一项说明系统调用还没超过10ms
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
| func retake(now int64) uint32 {
n := 0
// Prevent allp slice changes. This lock will be completely
// uncontended unless we're already stopping the world.
lock(&allpLock)
// We can't use a range loop over allp because we may
// temporarily drop the allpLock. Hence, we need to re-fetch
// allp each time around the loop.
for i := 0; i < len(allp); i++ { //遍历所有的P
_p_ := allp[i]
if _p_ == nil {
// This can happen if procresize has grown
// allp but not yet created new Ps.
continue
}
pd := &_p_.sysmontick // 最后一次被sysmon观察到的tick
s := _p_.status
sysretake := false
if s == _Prunning || s == _Psyscall { //只有当p处于 _Prunning 或 _Psyscall 状态时才会进行抢占
// Preempt G if it's running for too long.
t := int64(_p_.schedtick) // _p_.schedtick:每发生一次调度,调度器对该值加一
if int64(pd.schedtick) != t { // 监控线程监控到一次新的调度,所以重置跟sysmon相关的schedtick和schedwhen变量
pd.schedtick = uint32(t)
pd.schedwhen = now
} else if pd.schedwhen+forcePreemptNS <= now { // 1. 没有进第一个if语句内,说明:pd.schedtick == t; 说明(pd.schedwhen ~ now)这段时间未发生过调度;
preemptone(_p_) // 2. 但是这个_P_上面的某个Goroutine被执行,一直在执行这个Goroutiine; 中间没有切换其他Goroutine,因为如果切会导致_P_.schedtick增长,导致进入第一个if语句内;
// In case of syscall, preemptone() doesn't // 3. 连续运行超过10毫秒了,设置抢占请求.
// work, because there is no M wired to P.
sysretake = true // 需要系统抢占
}
}
if s == _Psyscall { // P处于系统调用之中,需要检查是否需要抢占
// Retake P from syscall if it's there for more than 1 sysmon tick (at least 20us).
t := int64(_p_.syscalltick) // 用于记录系统调用的次数,主要由工作线程在完成系统调用之后加一
if !sysretake && int64(pd.syscalltick) != t { // 不相等---说明已经不是上次观察到的系统调用,开始了一个新的系统调用,所以重置一下
pd.syscalltick = uint32(t)
pd.syscallwhen = now
continue
}
// On the one hand we don't want to retake Ps if there is no other work to do,
// but on the other hand we want to retake them eventually
// because they can prevent the sysmon thread from deep sleep.
// 1. _p_的本地运行队列没有Gs; runqempty(_p_)返回true
// 2. 有空闲的P,或者有正在自旋状态的M(正在偷其他P队列的Gs); atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0返回true
// 3. 上次观测到的系统调用还没有超过10毫秒; pd.syscallwhen+10*1000*1000 > now返回true
// - concluing: 当程序没有工作需要做,且系统调用没有超过10ms就不进行系统调用抢占.
// - 1和2说明这个程序没有工作需要做;
// - 3说明系统调用还没超过10ms
if runqempty(_p_) && atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 && pd.syscallwhen+10*1000*1000 > now {
continue
}
// Drop allpLock so we can take sched.lock.
unlock(&allpLock)
// Need to decrement number of idle locked M's
// (pretending that one more is running) before the CAS.
// Otherwise the M from which we retake can exit the syscall,
// increment nmidle and report deadlock.
incidlelocked(-1)
if atomic.Cas(&_p_.status, s, _Pidle) { // 需要抢占,则通过使用cas修改p的状态来获取p的使用权
if trace.enabled { // CAS: 工作线程此时此刻可能正好从系统调用返回了,也正在获取p的使用权
traceGoSysBlock(_p_)
traceProcStop(_p_)
}
n++
_p_.syscalltick++
handoffp(_p_) // 寻找一个新的m出来接管P
}
incidlelocked(1)
lock(&allpLock)
}
}
unlock(&allpLock)
return uint32(n)
}
|
下面两章将分别说说两种不同的剥夺抢占。
- 用户执行过久: 第26行的preemptone函数
- 陷入系统调用: 第67行的handoffp函数