【golang】调度系列之sysmon

调度系列
调度系列之goroutine
调度系列之m
调度系列之p

在golang的调度体系中,除了GMP本身,还有另外一个比较重要的角色sysmon。实际上,除了GMP和sysmon,runtime中还有一个全局的调度器对象。但该对象只是维护一些全局的数据,而不承担实际的调度职责,并不值得单独介绍,感兴趣的同学可以自己了解一下。

回到sysmon,sysmon是一个管理线程或者说守护线程,其是对GMP调度架构的补充和兜底。通过前面的几篇介绍,可以知道GMP的调度完全是主动协作式的调度。主动协作式的调度性能很高,但是在某些情况下会出现单个goroutine长期占据时间片甚至一直占据时间片的情况。

比如:

  • 某个goroutine不执行主动调度、不调用系统调用、不做函数调用,就会一直运行直到goroutine退出;
  • 某个goroutine处于syscall状态时也无法触发主动调度,可能会造成该goroutine长时间占据时间片;

sysmon的作用就是处理类似上面情况,其主要的工作内容有:

  • 定期查看netpoll有无就绪的任务,防止netpoll阻塞队列中的goroutine饥饿;
  • 定期查看是否有p长时间(10ms)处于syscall状态,如有则将p的持有权释放以执行其他g;
  • 定期查看是否有p长时间(10ms)没有调度,如有则对当前m发送信号,触发基于信号的异步抢占调度;

在main函数启动时,会调用newm函数创建sysmon线程,sysmon作为mstartfn传入。

go 复制代码
// src/runtime/proc.go 145
// The main goroutine.
func main() {
    ...
    if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
       systemstack(func() {
          newm(sysmon, nil, -1)
       })
    }
    ...
}

在介绍m的时候,我们提到过,mstart中会先调用mstartfn,然后再获取p并调用schedule函数。由于sysmon函数是循环不返回的,所以对应的m(也就是线程)永远运行sysmon,并且不需要获取p。所以并不是所有的m都需要p才可以运行的。

接下来,我们看下sysmon的里面具体做了些什么。

  • 进入sysmon可以看到里面是一个死循环,这和我们上面提到的一样。该循环并非一直忙等,而是会根据系统的情况进行延时睡眠,初始的interval是20us,最大的interval是10ms。
  • 在某些特殊的情况,sysmon可以进入更长时间(超过10ms)的睡眠,条件包括:
    • 系统不需要schedtrace。看起来是和调度相关观测的内容,如果需要schedtrace,则sysmon需要及时输出相关数据;
    • 系统处于停滞状态。这个停滞是我自己描述的,不一定准确,包括两种情况:1. 所有的p都是空闲的,此时系统中没有任务执行;2. 系统在等待进入gc状态,马上要stop the world;
      满足上面两个条件,则可最大进行1min的睡眠。1min是最大强制gc时间(2min)的一半。
  • sysmon的活跃状态,首先会坚持netpoll是否超过10ms没有被检查过,这是为了防止netpoll挂载goroutine的饥饿;
  • 然后会进行retake操作,retake的内容就是对所有p进行检查,查看p是否处于syscall或者被一个goroutine占据时间过长(超过10ms),如果有则进行相应的处理;
  • 最后还会进行gc和schedtrace相关的操作;
go 复制代码
// src/runtime.go 5134
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)

       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)
                if shouldRelax {
                   osRelax(false)
                }
                lock(&sched.lock)
                atomic.Store(&sched.sysmonwait, 0)
                noteclear(&sched.sysmonnote)
             }
             if syscallWake {
                idle = 0
                delay = 20
             }
          }
          unlock(&sched.lock)
       }

       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 {...}
       // 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() {
             incidlelocked(-1)
             injectglist(&list)
             incidlelocked(1)
          }
       }
       if GOOS == "netbsd" && needSysmonWorkaround {...}
       if scavenger.sysmonWake.Load() != 0 {
          // Kick the scavenger awake if someone requested it.
          scavenger.wake()
       }
       // retake P's blocked in syscalls
       // and preempt long running G's
       if retake(now) != 0 {
          idle = 0
       } else {
          idle++
       }
       // 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)
       }
       if debug.schedtrace > 0 && lasttrace+int64(debug.schedtrace)*1000000 <= now {
          lasttrace = now
          schedtrace(debug.scheddetail > 0)
       }
       unlock(&sched.sysmonlock)
    }
}

retake的操作也相对比较好理解。在p的介绍中我们提到过schedtick、syscalltick、sysmontick三个字段,其作用正是为了sysmon的检查。

sysmontick表示sysmon观测到的调度和系统调用情况,schedtick、syscalltick为实际的调度和系统调用情况。因为sysmon会经常睡眠,所以两者之间会有差异。

  • sysmon在检查所有p的过程中,如果发现sysmontick落后于实际情况,就会以实际情况为准更新sysmontick,同时也不会再做校验。因为sysmon睡眠最大时间为10ms,说明对应的p在10ms内做了调度。
  • 如果sysmontick和实际情况一只,则要看p是否运行一个goroutine超过10ms,如果是,则对m发送信号,触发异步抢占调度;如果p处于syscall状态超过10ms,则将p的持有权释放执行其他g。
go 复制代码
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_ := allp[i]
       if _p_ == nil {
          // This can happen if procresize has grown
          // allp but not yet created new Ps.
          continue
       }
       pd := &_p_.sysmontick
       s := _p_.status
       sysretake := false
       if s == _Prunning || s == _Psyscall {
          // Preempt G if it's running for too long.
          t := int64(_p_.schedtick)
          if int64(pd.schedtick) != t {
             pd.schedtick = uint32(t)
             pd.schedwhen = now
          } else if pd.schedwhen+forcePreemptNS <= now {
             preemptone(_p_)
             // In case of syscall, preemptone() doesn't
             // work, because there is no M wired to P.
             sysretake = true
          }
       }
       if s == _Psyscall {
          // 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.
          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) {
             if trace.enabled {
                traceGoSysBlock(_p_)
                traceProcStop(_p_)
             }
             n++
             _p_.syscalltick++
             handoffp(_p_)
          }
          incidlelocked(1)
          lock(&allpLock)
       }
    }
    unlock(&allpLock)
    return uint32(n)
}
相关推荐
devlei1 小时前
从源码泄露看AI Agent未来:深度对比Claude Code原生实现与OpenClaw开源方案
android·前端·后端
努力的小郑3 小时前
Canal 不难,难的是用好:从接入到治理
后端·mysql·性能优化
Victor3564 小时前
MongoDB(87)如何使用GridFS?
后端
Victor3564 小时前
MongoDB(88)如何进行数据迁移?
后端
小红的布丁4 小时前
单线程 Redis 的高性能之道
redis·后端
GetcharZp4 小时前
Go 语言只能写后端?这款 2D 游戏引擎刷新你的认知!
后端
宁瑶琴5 小时前
COBOL语言的云计算
开发语言·后端·golang
普通网友6 小时前
阿里云国际版服务器,真的是学生党的性价比之选吗?
后端·python·阿里云·flask·云计算
IT_陈寒7 小时前
Vue的这个响应式问题,坑了我整整两小时
前端·人工智能·后端
Soofjan7 小时前
Go 内存回收-GC 源码1-触发与阶段
后端