返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

4.7.2 系统监控

发布于 2024-10-12 19:16:07 字数 5725 浏览 0 评论 0 收藏 0

监控(sysmon)自运行时启动时,就使用专门的线程运行。

runtime.main 里 newm(sysmon, nil)

  • 如果超过 2 分钟没有垃圾回收,强制执行。
  • 网络轮询(netpoll),将结果添加到任务队列。
  • 向长时间运行 G 任务发出抢占调度。
  • 收回因 syscall 长时间阻塞的 P。
  • 执行定时器。
// proc.go

// Always runs without a P, so write barriers are not allowed.

func sysmon() {
 
    idle := 0                     // 闲置周期计数。
    delay := uint32(0)            // 循环间隔。
    
    for {
        if idle == 0 {            // 初始 20 纳秒(us)。
            delay = 20
        } else if idle > 50 {     // 如果闲置 50 个周期,那么间隔加倍。
            delay *= 2
        }
        if delay > 10*1000 {      // 最高 10 毫秒(ms)。
            delay = 10 * 1000
        }
        
        usleep(delay)
        
        // 当前时间,及下个计时器触发时间。
        now := nanotime()
        next, _ := timeSleepUntil()
        
        // STW,或没有 P 在工作。
        if atomic.Load(&sched.gcwaiting) != 0 || atomic.Load(&sched.npidle) == uint32(gomaxprocs) {
            // 且没有计时器要触发,短期休眠。
            if next > now {
                atomic.Store(&sched.sysmonwait, 1)
                notetsleep(&sched.sysmonnote, sleep)
                
                now = nanotime()
                next, _ = timeSleepUntil()
                
                atomic.Store(&sched.sysmonwait, 0)
                noteclear(&sched.sysmonnote)
            }
            
            // 重置计数器。
            idle = 0
            delay = 20
        }
        
        lastpoll := int64(atomic.Load64(&sched.lastpoll))
        
        // 如超过 10ms,则轮询网络。
        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() {
                injectglist(&list)
            }
        }
        
        // 执行定时器。
        // 启动一个 MP,让 schedule 去 checkTimers。
        if next < now {
            // There are timers that should have already run,
            // perhaps because there is an unpreemptible P.
            // Try to start an M to run them.
            startm(nil, false)
        }
        
        // 抢夺阻塞 syscall,抢占调度长时间运行 G。
        // 如果成功,那么闲置计数归零,否则累加。
        if retake(now) != 0 {
            idle = 0
        } else {
            idle++     // 也就是说,每个周期都可能累加。
        }
        
        // 距上次垃圾回收超过 2 分钟,强制执行。
        if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != 0 {
            forcegc.idle = 0
            var list gList
            list.push(forcegc.g)
            injectglist(&list)
        }
 }
}

retake

在 P 里有专门保存上次检查结果的字段。

schedtick 在 schedule/execute 调用时累加。

syscalltick 在 syscall 等调用时累加。

sysmontick 仅在 sysmon/retak 检查时累加。

// runtime2.go

type p struct {
    schedtick   uint32      // incremented on every scheduler call
    syscalltick uint32      // incremented on every system call
    sysmontick  sysmontick  // last tick observed by sysmon
}
// proc.go

type sysmontick struct {
    schedtick   uint32
    schedwhen   int64
    syscalltick uint32
    syscallwhen int64
}

对比调用和检查计数(tick),就可知道是否执行了新任务。再加上 sysmon 检查间隔时间(when),可判断是否超时。

// proc.go

func retake(now int64) uint32 {

    n := 0
    
    // 循环检查所有 P。
    for i := 0; i < len(allp); i++ {
        _p_ := allp[i]
        if _p_ == nil { continue }
        
        // 获取检查计数。
        pd := &_p_.sysmontick
        
        s := _p_.status
        sysretake := false
        
        if s == _Prunning || s == _Psyscall {
            
            // 计数不同,表示检查间隔期间执行了新任务,跳过。
            t := int64(_p_.schedtick)
            if int64(pd.schedtick) != t {
                
                // 更新检查计数和时间。
                pd.schedtick = uint32(t)
                pd.schedwhen = now
                
            } else if pd.schedwhen+forcePreemptNS <= now {
                
                // 计数相同,表示还在执行上次任务。
                // 距上次检查时间超过 10ms,发出抢占调度。
                preemptone(_p_)
                sysretake = true
            }
        }
        
        if s == _Psyscall {
            
            // 计数不同,执行了新任务,跳过。
            t := int64(_p_.syscalltick)
            if !sysretake && int64(pd.syscalltick) != t {
                
                // 更新检查计数和时间。
                pd.syscalltick = uint32(t)
                pd.syscallwhen = now
                
                continue
            }
            
            // 如当前 syscall P 没额外任务,尚有自旋或闲置 MP,且检查间隔不超过 10ms,跳过。
            // 反正有自旋 MP 去执行新任务,没必要强行剥夺 syscall P。当然,时间过长肯定不行。
            if runqempty(_p_) && atomic.Load(&sched.nmspinning) + atomic.Load(&sched.npidle) > 0 &&
               pd.syscallwhen+10*1000*1000 > now {
                continue
            }
            
            // 抢回 P。
            if atomic.Cas(&_p_.status, s, _Pidle) {
                _p_.syscalltick++
                handoffp(_p_)
            }
        }
    }
}
// proc.go

// forcePreemptNS is the time slice given to a G before it is preempted.

const forcePreemptNS = 10 * 1000 * 1000 // 10ms
// proc.go

// Tell the goroutine running on processor P to stop.

func preemptone(_p_ *p) bool {

    mp := _p_.m.ptr()
    gp := mp.curg
    
    // 设定抢占标记。
    gp.preempt = true
    gp.stackguard0 = stackPreempt
    
    // 请求异步抢占。
    if preemptMSupported && debug.asyncpreemptoff == 0 {
        _p_.preempt = true
        preemptM(mp)
    }
    
    return true
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文