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:
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:
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:
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:
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:
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:
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.
