当 Go 语言程序启动时,运行时会在第一个 Goroutine 中调用 [runtime.main](https://draveness.me/golang/tree/runtime.main) 启动主程序,该函数会在系统栈中创建新的线程:
func main() {...if GOARCH != "wasm" {systemstack(func() {newm(sysmon, nil)})}...}
[runtime.newm](https://draveness.me/golang/tree/runtime.newm) 会创建一个存储待执行函数和处理器的新结构体 [runtime.m](https://draveness.me/golang/tree/runtime.m)。运行时执行系统监控不需要处理器,系统监控的 Goroutine 会直接在创建的线程上运行:
func newm(fn func(), _p_ *p) {mp := allocm(_p_, fn)mp.nextp.set(_p_)mp.sigmask = initSigmask...newm1(mp)}
[runtime.newm1](https://draveness.me/golang/tree/runtime.newm1) 会调用特定平台的 [runtime.newsproc](https://draveness.me/golang/tree/runtime.newsproc) 通过系统调用 clone 创建一个新的线程并在新的线程中执行 [runtime.mstart](https://draveness.me/golang/tree/runtime.mstart):
func newosproc(mp *m) {stk := unsafe.Pointer(mp.g0.stack.hi)var oset sigsetsigprocmask(_SIG_SETMASK, &sigset_all, &oset)ret := clone(cloneFlags, stk, unsafe.Pointer(mp), unsafe.Pointer(mp.g0), unsafe.Pointer(funcPC(mstart)))sigprocmask(_SIG_SETMASK, &oset, nil)...}
在新创建的线程中,我们会执行存储在 [runtime.m](https://draveness.me/golang/tree/runtime.m) 中的 [runtime.sysmon](https://draveness.me/golang/tree/runtime.sysmon) 启动系统监控:
func sysmon() {sched.nmsys++checkdead()lasttrace := int64(0)idle := 0delay := uint32(0)for {if idle == 0 {delay = 20} else if idle > 50 {delay *= 2}if delay > 10*1000 {delay = 10 * 1000}usleep(delay)...}}
当运行时刚刚调用上述函数时,会先通过 [runtime.checkdead](https://draveness.me/golang/tree/runtime.checkdead) 检查是否存在死锁,然后进入核心的监控循环;系统监控在每次循环开始时都会通过 usleep 挂起当前线程,该函数的参数是微秒,运行时会遵循以下的规则决定休眠时间:
- 初始的休眠时间是 20μs;
- 最长的休眠时间是 10ms;
- 当系统监控在 50 个循环中都没有唤醒 Goroutine 时,休眠时间在每个循环都会倍增;
当程序趋于稳定之后,系统监控的触发时间就会稳定在 10ms。它除了会检查死锁之外,还会在循环中完成以下的工作:
- 运行计时器 — 获取下一个需要被触发的计时器;
- 轮询网络 — 获取需要处理的到期文件描述符;
- 抢占处理器 — 抢占运行时间较长的或者处于系统调用的 Goroutine;
- 垃圾回收 — 在满足条件时触发垃圾收集回收内存;
检查死锁
系统监控通过 [runtime.checkdead](https://draveness.me/golang/tree/runtime.checkdead) 检查运行时是否发生了死锁,我们可以将检查死锁的过程分成以下三个步骤:
- 检查是否存在正在运行的线程;
- 检查是否存在正在运行的 Goroutine;
- 检查处理器上是否存在计时器;
该函数首先会检查 Go 语言运行时中正在运行的线程数量,我们通过调度器中的多个字段计算该值的结果:
func checkdead() {var run0 int32run := mcount() - sched.nmidle - sched.nmidlelocked - sched.nmsysif run > run0 {return}if run < 0 {print("runtime: checkdead: nmidle=", sched.nmidle, " nmidlelocked=", sched.nmidlelocked, " mcount=", mcount(), " nmsys=", sched.nmsys, "\n")throw("checkdead: inconsistent counts")}...}
[runtime.mcount](https://draveness.me/golang/tree/runtime.mcount)根据下一个待创建的线程 id 和释放的线程数得到系统中存在的线程数;nmidle是处于空闲状态的线程数量;nmidlelocked是处于锁定状态的线程数量;nmsys是处于系统调用的线程数量;
利用上述几个线程相关数据,我们可以得到正在运行的线程数,如果线程数量大于 0,说明当前程序不存在死锁;如果线程数小于 0,说明当前程序的状态不一致;如果线程数等于 0,我们需要进一步检查程序的运行状态:
func checkdead() {...grunning := 0for i := 0; i < len(allgs); i++ {gp := allgs[i]if isSystemGoroutine(gp, false) {continue}s := readgstatus(gp)switch s &^ _Gscan {case _Gwaiting, _Gpreempted:grunning++case _Grunnable, _Grunning, _Gsyscall:print("runtime: checkdead: find g ", gp.goid, " in status ", s, "\n")throw("checkdead: runnable g")}}unlock(&allglock)if grunning == 0 {throw("no goroutines (main called runtime.Goexit) - deadlock!")}...}
- 当存在 Goroutine 处于
_Grunnable、_Grunning和_Gsyscall状态时,意味着程序发生了死锁; - 当所有的 Goroutine 都处于
_Gidle、_Gdead和_Gcopystack状态时,意味着主程序调用了[runtime.goexit](https://draveness.me/golang/tree/runtime.goexit);
当运行时存在等待的 Goroutine 并且不存在正在运行的 Goroutine 时,我们会检查处理器中存在的计时器
func checkdead() {...for _, _p_ := range allp {if len(_p_.timers) > 0 {return}}throw("all goroutines are asleep - deadlock!")}
如果处理器中存在等待的计时器,那么所有的 Goroutine 陷入休眠状态是合理的,不过如果不存在等待的计时器,运行时会直接报错并退出程序。
运行计时器
在系统监控的循环中,我们通过 runtime.nanotime 和 runtime.timeSleepUntil 获取当前时间和计时器下一次需要唤醒的时间;当前调度器需要执行垃圾回收或者所有处理器都处于闲置状态时,如果没有需要触发的计时器,那么系统监控可以暂时陷入休眠:
func sysmon() {...for {...now := nanotime()next, _ := timeSleepUntil()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) {if next > now {atomic.Store(&sched.sysmonwait, 1)unlock(&sched.lock)sleep := forcegcperiod / 2if next-now < sleep {sleep = next - now}...notetsleep(&sched.sysmonnote, sleep)...now = nanotime()next, _ = timeSleepUntil()lock(&sched.lock)atomic.Store(&sched.sysmonwait, 0)noteclear(&sched.sysmonnote)}idle = 0delay = 20}unlock(&sched.lock)}...if next < now {startm(nil, false)}}}
休眠的时间会依据强制 GC 的周期 forcegcperiod 和计时器下次触发的时间确定,runtime.notesleep 会使用信号量同步系统监控即将进入休眠的状态。当系统监控被唤醒之后,我们会重新计算当前时间和下一个计时器需要触发的时间、调用 runtime.noteclear 通知系统监控被唤醒并重置休眠的间隔。
如果在这之后,我们发现下一个计时器需要触发的时间小于当前时间,这也说明所有的线程可能正在忙于运行 Goroutine,系统监控会启动新的线程来触发计时器,避免计时器的到期时间有较大的偏差。
轮询网络
如果上一次轮询网络已经过去了 10ms,那么系统监控还会在循环中轮询网络,检查是否有待执行的文件描述符:
func sysmon() {...for {...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)if !list.empty() {incidlelocked(-1)injectglist(&list)incidlelocked(1)}}...}}
上述函数会非阻塞地调用 [runtime.netpoll](https://draveness.me/golang/tree/runtime.netpoll) 检查待执行的文件描述符并通过 [runtime.injectglist](https://draveness.me/golang/tree/runtime.injectglist) 将所有处于就绪状态的 Goroutine 加入全局运行队列中:
func injectglist(glist *gList) {if glist.empty() {return}lock(&sched.lock)var n intfor n = 0; !glist.empty(); n++ {gp := glist.pop()casgstatus(gp, _Gwaiting, _Grunnable)globrunqput(gp)}unlock(&sched.lock)for ; n != 0 && sched.npidle != 0; n-- {startm(nil, false)}*glist = gList{}}
该函数会将所有 Goroutine 的状态从 _Gwaiting 切换至 _Grunnable 并加入全局运行队列等待运行,如果当前程序中存在空闲的处理器,会通过 [runtime.startm](https://draveness.me/golang/tree/runtime.startm) 启动线程来执行这些任务。
抢占处理器
系统监控会在循环中调用 runtime.retake 抢占处于运行或者系统调用中的处理器,该函数会遍历运行时的全局处理器,每个处理器都存储了一个 runtime.sysmontick:
type sysmontick struct {schedtick uint32schedwhen int64syscalltick uint32syscallwhen int64}
该结构体中的四个字段分别存储了处理器的调度次数、处理器上次调度时间、系统调用的次数以及系统调用的时间。runtime.retake 的循环包含了两种不同的抢占逻辑:
func retake(now int64) uint32 {n := 0for i := 0; i < len(allp); i++ {_p_ := allp[i]pd := &_p_.sysmonticks := _p_.statusif s == _Prunning || s == _Psyscall {t := int64(_p_.schedtick)if pd.schedwhen+forcePreemptNS <= now {preemptone(_p_)}}if s == _Psyscall {if runqempty(_p_) && atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 && pd.syscallwhen+10*1000*1000 > now {continue}if atomic.Cas(&_p_.status, s, _Pidle) {n++_p_.syscalltick++handoffp(_p_)}}}return uint32(n)}
- 当处理器处于
_Prunning或者_Psyscall状态时,如果上一次触发调度的时间已经过去了 10ms,我们会通过[runtime.preemptone](https://draveness.me/golang/tree/runtime.preemptone)抢占当前处理器; - 当处理器处于
_Psyscall状态时,在满足以下三种情况的任意一种下会调用[runtime.handoffp](https://draveness.me/golang/tree/runtime.handoffp)让出处理器的使用权:
- p 的运行队列里面有等待运行的 goroutine
- 没有无所事事的 p
- 从上一次监控线程观察到 p 对应的 m 处于系统调用之中到现在已经超过 10 毫秒
系统监控通过在循环中抢占处理器来避免同一个 Goroutine 占用线程太长时间造成饥饿问题。
垃圾回收
在最后,系统监控还会决定是否需要触发强制垃圾回收,runtime.sysmon 会构建 runtime.gcTrigger 并调用 runtime.gcTrigger.test 方法判断是否需要触发垃圾回收:
func sysmon() {...for {...if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != 0 {lock(&forcegc.lock)forcegc.idle = 0var list gListlist.push(forcegc.g)injectglist(&list)unlock(&forcegc.lock)}...}}
如果需要触发垃圾回收,我们会将用于垃圾回收的 Goroutine 加入全局队列,让调度器选择合适的处理器去执行。
