Skip to content

sysmon

sysmon es una función ordinaria, traducida literalmente como monitor del sistema. Eliminando las partes comentadas, tiene alrededor de 200 líneas de código. Se le asigna un hilo separado para iniciarse durante la fase de arranque del programa, y luego continúa monitoreando el estado de ejecución del programa Go en segundo plano y toma las medidas correspondientes. El código relacionado con su inicio se puede ver en la función runtime.main:

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

El monitor del sistema en sí es solo un bucle for. El intervalo de cada ciclo es de 20 microsegundos, y a medida que aumenta el índice de inactividad del programa, el intervalo puede aumentar hasta un máximo de 10 milisegundos. En cada ciclo, realiza principalmente las siguientes tareas:

  • Asistir la planificación de goroutines, preemptar goroutines que se ejecutan durante mucho tiempo
  • Verificar la situación de memoria y determinar si se necesita recolección de basura
  • Monitorear el poller de red
  • Liberar memoria no usada al sistema operativo
  • Registrar información de depuración

Bucle de Monitoreo

El bucle de monitoreo del sistema es bastante simple. Primero obtiene el tiempo actual, y luego ejecuta las tareas de monitoreo correspondientes. El código es el siguiente:

go
func sysmon() {
    var (
        idle           uint32
        now, last, li  int64
        schedtick, gcwTicks int64
        pollUntil      int64 = 1<<64 - 1
        start          int64
        lastTrace      int64 = 20 * 1e6
    )

    lock(&sched.lock)
    sched.nmsys++
    checkdead()
    unlock(&sched.lock)

    for {
        now = nanotime()
        if idle == 0 {
            start = now
        }
        idle = 0

        // 辅助协程调度
        if gp := retake(now); gp != nil {
            // ...
        }

        // 检查内存情况
        if now-last >= forcegcperiod {
            // ...
        }

        // 释放内存
        if now-last >= 30*1e9 {
            // ...
        }

        // 轮询网络
        if netpollinited() && lastpoll != 0 && pollUntil != 0 && pollUntil < now {
            // ...
        }

        // 记录调试信息
        if now-lastTrace >= 30*1e9 {
            // ...
        }

        // 计算下一轮循环的间隔时间
        delay := uint32(20)
        if idle > 50 {
            delay *= 100
        }
        if idle > 200 {
            delay *= 10
        }
        if delay > 10*1000 {
            delay = 10 * 1000
        }

        // 等待下一轮循环
        rel := now - last
        if rel < int32(delay) {
            // ...
        }
        last = now
    }
}

El bucle de monitoreo del sistema se ejecuta en un hilo separado. No está asociado con ningún P, por lo que no participa en la planificación normal de goroutines. Su trabajo principal es monitorear el estado del programa y tomar las medidas correspondientes cuando es necesario.

Preemptación de Goroutines

Una de las tareas importantes del monitor del sistema es preemptar goroutines que se ejecutan durante mucho tiempo. Esto se realiza a través de la función runtime.retake. Esta función recorre todos los P y verifica si alguna goroutine se ha estado ejecutando durante demasiado tiempo. Si es así, fuerza la preemptación.

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 if it's running for too long.
            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)
}

El tiempo umbral para la preemptación es forcePreemptNS, que es de 10 milisegundos. Si una goroutine se ha estado ejecutando durante más de este tiempo, se fuerza la preemptación. Esto asegura que ninguna goroutine pueda monopolizar el tiempo de CPU durante demasiado tiempo.

Recolección de Basura

El monitor del sistema también es responsable de verificar si se necesita realizar la recolección de basura. Si ha pasado demasiado tiempo desde la última recolección de basura, fuerza el inicio de una nueva recolección.

go
if now-last >= forcegcperiod {
    lock(&gcBgMarkWorker.lock)
    if !forcegc.idle || len(gcBgMarkWorker.queue) > 0 {
        unlock(&gcBgMarkWorker.lock)
    } else {
        forcegc.idle = true
        var list gList
        list.pushBack(gcBgMarkWorker.pop())
        for !gList.empty() {
            gp := list.pop()
            gp.schedparam = int32(now)
            goready(gp, 0)
        }
        unlock(&gcBgMarkWorker.lock)
    }
}

El período de fuerza de recolección de basura forcegcperiod es de 2 minutos por defecto. Esto asegura que incluso si el programa no genera mucha basura, se realice una recolección de basura periódica para liberar memoria no usada.

Liberación de Memoria

El monitor del sistema también es responsable de liberar memoria no usada al sistema operativo. Esto se realiza a través de la función runtime.bgscavenge.

go
if now-last >= 30*1e9 {
    if scavengeBgSleep == 0 {
        scavengeBgSleep = 1 * time.Second
    }
    scavengeBgSleep = time.Duration(float64(scavengeBgSleep) * 1.1)
    if scavengeBgSleep > 10*time.Second {
        scavengeBgSleep = 10 * time.Second
    }
}

Esta función libera las páginas de memoria que no se han usado durante mucho tiempo al sistema operativo. Esto ayuda a reducir el uso de memoria del programa Go.

Poller de Red

El monitor del sistema también monitorea el poller de red. Si hay eventos de red pendientes, los procesa.

go
if netpollinited() && lastpoll != 0 && pollUntil != 0 && pollUntil < now {
    lock(&sched.lock)
    if netpollinited() && sched.lastpoll.Load() != 0 {
        list := netpoll(0)
        if !list.empty() {
            injectglist(&list)
        }
    }
    unlock(&sched.lock)
}

Esto asegura que los eventos de red se procesen de manera oportuna, incluso si no hay goroutines disponibles para manejarlos.

Registro de Depuración

El monitor del sistema también registra información de depuración periódicamente. Esto incluye información sobre el uso de memoria, el número de goroutines, y otra información relevante.

go
if now-lastTrace >= 30*1e9 {
    traceReaderScheduler()
    lastTrace = now
}

Esta información es útil para depurar y analizar el rendimiento del programa Go.

Intervalo de Monitoreo

El intervalo de monitoreo del sistema es dinámico. Comienza en 20 microsegundos, y puede aumentar hasta 10 milisegundos dependiendo del índice de inactividad del programa.

go
delay := uint32(20)
if idle > 50 {
    delay *= 100
}
if idle > 200 {
    delay *= 10
}
if delay > 10*1000 {
    delay = 10 * 1000
}

El índice de inactividad idle se calcula basándose en el número de veces que el monitor del sistema no encuentra trabajo que hacer. Si el programa está inactivo durante mucho tiempo, el intervalo de monitoreo aumenta para reducir el consumo de CPU.

Resumen

El monitor del sistema es un componente importante del tiempo de ejecución de Go. Es responsable de monitorear el estado del programa y tomar las medidas correspondientes cuando es necesario. Sus principales funciones incluyen:

  • Preemptar goroutines que se ejecutan durante mucho tiempo
  • Forzar la recolección de basura cuando es necesario
  • Liberar memoria no usada al sistema operativo
  • Monitorear el poller de red
  • Registrar información de depuración

Aunque el monitor del sistema en sí es solo un bucle simple, juega un papel crucial en el mantenimiento del rendimiento y la estabilidad del programa Go.

Golang editado por www.golangdev.cn