Skip to content

sysmon

sysmon is a common function, literally translated as system monitor. Removing the comment parts, it's only about 200 lines of code. It's assigned a separate thread to start during the program bootstrap phase, and then continuously monitors the state of the Go program runtime in the background and makes corresponding processing. The code for its startup can be viewed in the runtime.main function:

go
func main() {
    ...
  mp := getg().m
  mainStarted = true
  systemstack(func() {
        newm(sysmon, nil, -1)
    })
    ...
}

The system monitor itself is just a for loop. The interval between each round of loops is 20 microseconds, and as the program idle index rises, the interval time increases up to a maximum of 10 milliseconds. In each round of the loop, it mainly does the following things:

  • Assists goroutine scheduling, preempts long-running goroutines
  • Checks memory conditions and determines whether garbage collection is needed
  • Checks network poller conditions and handles timed waiting goroutines
  • Scavenges unused memory and returns it to the operating system
  • Flushes heap profile information
  • Drops mcache buffers

Preemption

Preemption work is completed by the runtime.preemptone function. It mainly checks whether the current goroutine's running time exceeds the limit. If so, it sets the goroutine's preempt field to true to indicate it needs to be preempted.

go
func preemptone(pp *p) bool {
  mp := pp.m.ptr()
  if mp == nil || mp == getg().m {
    return false
  }
  gp := mp.curg
  if gp == nil || gp == mp.g0 {
    return false
  }

  gp.preempt = true

  // Every call in a goroutine should check for preemption now.
  gp.stackguard0 = stackPreempt

  return true
}

The goroutine's stackguard0 field is used to check whether preemption is needed. When it's set to stackPreempt, it indicates the goroutine needs to be preempted. This is the mechanism for stack expansion checks. When a goroutine's stack needs to expand, it sets stackguard0 to stackPreempt. When the goroutine calls a function, it checks whether stackguard0 is stackPreempt. If so, it triggers stack expansion. But if the goroutine doesn't call functions for a long time, it can't trigger stack expansion checks. So the system monitor needs to check this situation.

go
func goschedImpl(gp *g, inheritTime bool) {
  ...
  casgstatus(gp, _Grunning, _Grunnable)
  if !inheritTime {
    gp.schedtick++
  }
  globrunq.put(gp, 0)
  schedule()
}

When a goroutine is preempted, its status is changed from _Grunning to _Grunnable, then it's put into the global runnable queue to wait for rescheduling.

Garbage Collection

The system monitor periodically checks whether garbage collection is needed. If the time since the last garbage collection exceeds the limit, it forcibly triggers garbage collection. This time limit is determined by the runtime.forcegcperiod constant, which is 2 minutes.

go
func sysmon() {
  ...
  for {
    ...
    // check if we need to force a GC
    if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && forcegc.idle.Load() {
      lock(&forcegc.lock)
      forcegc.idle.Store(false)
      var list gList
      list.push(forcegc.g)
      injectglist(&list)
      unlock(&forcegc.lock)
    }
    ...
  }
}

When garbage collection is triggered, the forcegc goroutine is awakened to execute garbage collection work.

Network Poller

The system monitor also periodically checks the network poller's conditions. If there are timed waiting goroutines, it handles them. This part of the work is completed by the runtime.netpoll function.

go
func sysmon() {
  ...
  for {
    ...
    // poll network
    npoll := netpoll(-1)
    if npoll != 0 {
      npollcnt++
    }
    ...
  }
}

When the network poller has timed waiting goroutines, they are awakened to continue execution.

Memory Scavenge

The system monitor also periodically scavenges unused memory and returns it to the operating system. This part of the work is completed by the runtime.mheap.scavenge function.

go
func sysmon() {
  ...
  for {
    ...
    // scavenge heap
    if scavengeHeap() {
      scavengeCnt++
    }
    ...
  }
}

When there's unused memory in the heap, it's returned to the operating system to reduce memory usage.

Other Work

The system monitor also does some other work, such as flushing heap profile information, dropping mcache buffers, etc. These tasks are not the main work of the system monitor, but they are also necessary.

go
func sysmon() {
  ...
  for {
    ...
    // flush heap profile
    if shouldFlushHeapProfile() {
      flushHeapProfile()
    }
    // drop mcache buffers
    if shouldDropMcache() {
      dropMcache()
    }
    ...
  }
}

Overall, the system monitor is an important part of Go runtime. It monitors Go program runtime state and makes corresponding processing to ensure Go program stable operation.

Golang by www.golangdev.cn edit