Skip to content

sysmon

sysmon é uma função comum, traduzida literalmente como monitor de sistema. Removendo as partes de comentários, tem cerca de 200 linhas de código. Ele é atribuído a uma thread separada para iniciar durante a fase de bootstrap do programa, e depois continua monitorando o estado do programa Go em execução em segundo plano e toma as medidas apropriadas. O código relacionado à sua inicialização pode ser encontrado na função runtime.main:

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

O monitor de sistema em si é apenas um loop for. O intervalo de tempo de cada ciclo é de 20 microssegundos. À medida que o índice de ociosidade do programa aumenta, o intervalo pode aumentar para no máximo 10 milissegundos. Em cada ciclo, ele realiza principalmente as seguintes tarefas:

  • Auxilia o escalonador de goroutines, preemptando goroutines que estão executando por muito tempo
  • Verifica a situação da memória e determina se é necessário realizar coleta de lixo
  • Libera memória física não utilizada de volta ao sistema operacional
  • Coleta estatísticas de rede

Preempção

No artigo de escalonador GMP, mencionamos que antes da versão Go 1.14, o escalonador usava apenas estratégia de escalonamento cooperativo. Se uma goroutine não chamasse funções, não poderia ser preemptada. Para resolver este problema, o Go adicionou estratégia de escalonamento preemptivo baseado em sinal na versão 1.14. O monitor de sistema é uma das entradas para acionar preempção.

No loop do monitor de sistema, ele percorre cada processador P. Se o tempo de execução da goroutine G escalonada por P exceder 10ms, força o acionamento de preempção. Este trabalho é completado pela função runtime.retake. Abaixo está o código simplificado:

go
func retake(now int64) uint32 {
  n := 0
  lock(&allpLock)
  for i := 0; i < len(allp); i++ {
    pp := allp[i]
    if pp == nil {
      continue
    }
    pd := &pp.sysmontick
    s := pp.status
    sysretake := false
    if s == _Prunning || s == _Psyscall {
      // Preempt G se estiver executando por muito tempo
      t := int64(pp.schedtick)
      if int64(pd.schedtick) != t {
        pd.schedtick = uint32(t)
        pd.schedwhen = now
      } else if pd.schedwhen+forcePreemptNS <= now {
        preemptone(pp)
        sysretake = true
      }
    }
  }
  unlock(&allpLock)
  return uint32(n)
}

A função runtime.preemptone é responsável por enviar sinal de preempção para a goroutine em execução:

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

  // Cada chamada para preemptone deve ser seguida por uma chamada
  // para signalM, então podemos definir isso sem medo de perder
  // um sinal
  mp.signalPending.Store(1)

  signalM(mp, sigPreempt)
  return true
}

Primeiro define gp.preempt = true, depois envia sinal sigPreempt para a thread M. Quando o sinal é recebido, o handler de sinal registrado modifica o contexto da goroutine alvo, injetando uma chamada para runtime.asyncPreempt, fazendo a goroutine parar o trabalho atual e entrar em nova rodada de loop de escalonamento para ceder direito de execução para outras goroutines.

Coleta de Lixo

O monitor de sistema também verifica periodicamente se é necessário forçar coleta de lixo. Se não houver coleta de lixo por muito tempo (o tempo padrão é 2 minutos), força o início do GC. Este trabalho é completado pela função runtime.forcegchelper:

go
func sysmon() {
  ...
  for {
    ...
    // verifica se precisamos forçar um 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)
    }
    ...
  }
}

Além disso, o monitor de sistema também é responsável por ajustar o algoritmo de pacing do GC, determinando quando realizar coleta de lixo e quanto trabalho executar.

Liberação de Memória

Quando a memória física não está sendo usada, o monitor de sistema a libera de volta ao sistema operacional. Isso ajuda a reduzir o uso de memória do programa Go. A função runtime.scavengeOne é responsável por liberar memória:

go
func scavengeOne(npages uintptr, urgent bool) uintptr {
  ...
  // Tenta liberar memória do heap
  released := mheap_.pages.scavenge(npages, urgent)
  ...
  return released
}

O monitor de sistema chama esta função periodicamente para liberar memória não utilizada.

Estatísticas de Rede

O monitor de sistema também é responsável por coletar estatísticas de rede, como número de conexões, tráfego de rede, etc. Estas estatísticas são usadas para otimização de desempenho e solução de problemas.

Loop Principal

Abaixo está o código do loop principal do monitor de sistema:

go
func sysmon() {
  var idle uint32 // contador de ociosidade

  for {
    // Obtém tempo atual
    now := nanotime()

    // Se o sistema estiver ocioso por muito tempo, aumenta o intervalo de sono
    if idle == 0 {
      sleep = 20 * 1000 // 20 microssegundos
    } else if idle < 50 {
      sleep += 20 * 1000
      if sleep > 10*1000*1000 {
        sleep = 10 * 1000 * 1000 // 10 milissegundos
      }
    }

    // Verifica se é necessário realizar preempção
    if retake(now) != 0 {
      idle = 0
    } else {
      idle++
    }

    // Verifica se é necessário forçar 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)
    }

    // Libera memória não utilizada
    if scavenged := scavengeOne(1, false); scavenged > 0 {
      idle = 0
    }

    // Dorme por um período
    usleep(sleep)
  }
}

Pode-se ver que o monitor de sistema é um loop infinito. Em cada ciclo, ele realiza as tarefas mencionadas acima. O intervalo de sono é ajustado dinamicamente de acordo com o estado de ociosidade do sistema.

Resumo

O monitor de sistema é um componente importante do runtime do Go. Ele é responsável por:

  • Preempção de goroutines que estão executando por muito tempo
  • Forçar coleta de lixo quando necessário
  • Liberar memória física não utilizada de volta ao sistema operacional
  • Coletar estatísticas de rede

Através do monitor de sistema, o runtime do Go pode melhor gerenciar recursos do sistema, melhorar o desempenho e estabilidade do programa.

Golang por www.golangdev.cn edit