sysmon 默默无闻的后台监控

golang 里面里面有一个默默无闻的工作者在后台跑着,它的名字叫 sysmon ,你可能在某个地方见到过它。我最早是在 gc 中第一次见到了它,当时只知道默认有一个两分钟的 gc 是由它来控制的,那么它究竟还做了什么工作呢?今天我们就来看看它。

启动

首先让我们来看看 sysmon 是谁启动的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// The main goroutine.
func main() {
...........
if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
// For runtime_syscall_doAllThreadsSyscall, we
// register sysmon is not ready for the world to be
// stopped.
atomic.Store(&sched.sysmonStarting, 1)
systemstack(func() {
newm(sysmon, nil, -1)
})
}
............
fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn()

注意哦,这个 main 不是我们写的 main,而是 runtime 的 main,go 启动的时候会调用这个 runtime.main ,而我们的写的 main 会在后面被调用,也就是 main.main 的时候被调用。而 newm 方法会创建一个 m,这里的 m 就是 gpm 模型中的 m,newm 调用 newm1 然后调用 mstart 启动

这里就有了第一个细节,sysmon 不需要一个 p 就能执行,只需要绑定一个 m 就能执行了

工作内容

检查死锁情况

1
2
3
4
func sysmon() {
lock(&sched.lock)
sched.nmsys++
checkdead()

一上来的第一步就是检查有没有死锁的情况

工作时间调整

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
func sysmon() {
lock(&sched.lock)
sched.nmsys++
checkdead()
unlock(&sched.lock)

// For syscall_runtime_doAllThreadsSyscall, sysmon is
// sufficiently up to participate in fixups.
atomic.Store(&sched.sysmonStarting, 0)

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)
mDoFixup()
  • 一开始 delay 是 20us,也就是说一开始就停 20us 就执行了
  • 然后如果循环了 50 次还是什么也没做,没有唤醒任何 goroutine 那么就开始倍增 delay
  • 最长 delay 时间为 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
now := nanotime()
if debug.schedtrace <= 0 && (sched.gcwaiting != 0 || atomic.Load(&sched.npidle) == uint32(gomaxprocs)) {
lock(&sched.lock)
if atomic.Load(&sched.gcwaiting) != 0 || atomic.Load(&sched.npidle) == uint32(gomaxprocs) {
syscallWake := false
next, _ := timeSleepUntil()
if next > now {
atomic.Store(&sched.sysmonwait, 1)
unlock(&sched.lock)
// Make wake-up period small enough
// for the sampling to be correct.
sleep := forcegcperiod / 2
if next-now < sleep {
sleep = next - now
}
shouldRelax := sleep >= osRelaxMinNS
if shouldRelax {
osRelax(true)
}
syscallWake = notetsleep(&sched.sysmonnote, sleep)
mDoFixup()
if shouldRelax {
osRelax(false)
}
lock(&sched.lock)
atomic.Store(&sched.sysmonwait, 0)
noteclear(&sched.sysmonnote)
}
if syscallWake {
idle = 0
delay = 20
}
}
unlock(&sched.lock)
}

通过 timeSleepUntil 方法计算出下一个 timer 运行需要被唤醒的时间,如果没有需要触发的定时器,并且当前调度器需要 gc 或没有其他工作,那么就还能再休息一会,休息时间根据 forcegcperiod 来计算

网络工作

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
lock(&sched.sysmonlock)
// Update now in case we blocked on sysmonnote or spent a long time
// blocked on schedlock or sysmonlock above.
now = nanotime()

// trigger libc interceptors if needed
if *cgo_yield != nil {
asmcgocall(*cgo_yield, nil)
}
// poll network if not polled for more than 10ms
lastpoll := int64(atomic.Load64(&sched.lastpoll))
if netpollinited() && lastpoll != 0 && lastpoll+10*1000*1000 < now {
atomic.Cas64(&sched.lastpoll, uint64(lastpoll), uint64(now))
list := netpoll(0) // non-blocking - returns list of goroutines
if !list.empty() {
// Need to decrement number of idle locked M's
// (pretending that one more is running) before injectglist.
// Otherwise it can lead to the following situation:
// injectglist grabs all P's but before it starts M's to run the P's,
// another M returns from syscall, finishes running its G,
// observes that there is no work to do and no other running M's
// and reports deadlock.
incidlelocked(-1)
injectglist(&list)
incidlelocked(1)
}
}
mDoFixup()

lastpoll+10*1000*1000 < now 也就是说举例上一次网络轮训已经过去了 10ms 了,那么就需要检查是否有待执行的文件描述符。需要调用 netpoll 进行检查,如果检查到有,那么就通过 injectglist 将这些 goroutine 加入到全局队列中去。

抢占处理工作

1
2
3
4
5
6
7
// retake P's blocked in syscalls
// and preempt long running G's
if retake(now) != 0 {
idle = 0
} else {
idle++
}

retake 方法相信你应该不会陌生,这个方法处理了抢占的情况:一种是阻塞在了系统调用上的 P ,一种是运行时间过长的 G。这个时候就需要解绑 P 和 M,让其他的线程能获得 P 来继续执行其他的 G。具体里面的抢占的工作,让我们留到抢占的章节里面去,这里就不再赘述了。

gc 工作

1
2
3
4
5
6
7
8
9
// check if we need to force a GC
if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != 0 {
lock(&forcegc.lock)
forcegc.idle = 0
var list gList
list.push(forcegc.g)
injectglist(&list)
unlock(&forcegc.lock)
}

创建 gcTrigger 并调用 test 方法来确定是否需要 gc 如果需要的话就将执行 gc 的 g 直接 push 到全局队列中,然后调度执行

总结

sysmon 不需要 P 只需要 M 即可执行,能休息就休息,没工作时就慢下来,sysmon 主要做了以下几样工作:

  1. 检查死锁
  2. 检查网络轮训
  3. 检查抢占
  4. 检查 gc

总之其实 sysmon 其实在背后承担了一些检查的工作,来保证 runtime 的正常运行,同时也尽可能的减少它本身运行所需要的资源

参考链接

https://medium.com/@blanchon.vincent/go-sysmon-runtime-monitoring-cff9395060b5