Ir a idioma inicialización del programador de rutinas doce

El siguiente contenido se reproduce de  https://mp.weixin.qq.com/s/W9D4Sl-6jYfcpczzdPfByQ

Awa me encanta escribir programas originales Zhang  source Travels  2019-05-05

Este artículo es el duodécimo capítulo de la serie "Análisis de escenarios de código fuente de Go Language Scheduler", y también es la segunda subsección del Capítulo 2.


 

Este capítulo tomará el siguiente programa simple Hello World como ejemplo para analizar la inicialización del programador de lenguaje Go, la creación y salida de goroutines, el ciclo de programación de subprocesos de trabajo y el cambio de goroutines mediante el seguimiento del proceso completo en ejecución desde el inicio. para salir. Y otro contenido importante.

package main

import "fmt"

func main() {
    fmt.Println("Hello World!")
}

Primero, analizamos la inicialización del planificador desde el inicio del programa.

Antes de analizar el proceso de inicio del programa, primero echemos un vistazo al estado inicial de la pila del programa antes de ejecutar la primera instrucción.

Cualquier programa escrito en un lenguaje compilado (ya sea C, C ++, go o ensamblador) pasará por las siguientes etapas en secuencia cuando es cargado y ejecutado por el sistema operativo:

  1. Lea el programa ejecutable del disco en la memoria;

  2. Crear proceso e hilo principal;

  3. Asignar espacio de pila para el hilo principal;

  4. Copie los parámetros ingresados ​​por el usuario en la línea de comando a la pila del hilo principal;

  5. Coloque el hilo principal en la cola de ejecución del sistema operativo y espere a que se programe su ejecución.

Antes de que el hilo principal esté programado para ejecutar la primera instrucción por primera vez, la pila de funciones del hilo principal se muestra en la siguiente figura:

imagen

Después de comprender el estado inicial del programa, comencemos oficialmente.

Entrada al programa

Use go build para compilar hello.go en la línea de comandos de Linux para obtener el programa ejecutable hello, y luego use gdb para depurar. En gdb, primero usamos el comando info files para encontrar la dirección del punto de entrada del programa es 0x452270, y luego use b * 0x452270 en 0x452270 Junto al punto de interrupción en la dirección, gdb nos dice que el código fuente correspondiente a esta entrada es la línea 8 del archivo runtime / rt0_linux_amd64.s.

bobo@ubuntu:~/study/go$ go build hello.go 
bobo@ubuntu:~/study/go$ gdb hello
GNU gdb (GDB) 8.0.1
(gdb) info files
Symbols from "/home/bobo/study/go/main".
Local exec file:
`/home/bobo/study/go/main', file type elf64-x86-64.
Entry point: 0x452270
0x0000000000401000 - 0x0000000000486aac is .text
0x0000000000487000 - 0x00000000004d1a73 is .rodata
0x00000000004d1c20 - 0x00000000004d27f0 is .typelink
0x00000000004d27f0 - 0x00000000004d2838 is .itablink
0x00000000004d2838 - 0x00000000004d2838 is .gosymtab
0x00000000004d2840 - 0x00000000005426d9 is .gopclntab
0x0000000000543000 - 0x000000000054fa9c is .noptrdata
0x000000000054faa0 - 0x0000000000556790 is .data
0x00000000005567a0 - 0x0000000000571ef0 is .bss
0x0000000000571f00 - 0x0000000000574658 is .noptrbss
0x0000000000400f9c - 0x0000000000401000 is .note.go.buildid
(gdb) b *0x452270
Breakpoint 1 at 0x452270: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.

Abra el editor de código y busque el archivo runtime / rt0_linx_amd64.s, que es un archivo de código fuente escrito en lenguaje ensamblador go. Hemos discutido su formato en la primera parte de este libro. Ahora mire la línea 8:

tiempo de ejecución / rt0_linx_amd64.s: 8

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
    JMP_rt0_amd64(SB)

La primera línea de código anterior define el símbolo _rt0_amd64_linux, que no es una instrucción de CPU real. La instrucción JMP en la segunda línea es la primera instrucción del hilo principal. Esta instrucción simplemente salta a (equivalente a go language o c Goto in) _rt0_amd64 continúa ejecutándose en el símbolo. La definición de _rt0_amd64 está en el archivo runtime / asm_amd64.s:

tiempo de ejecución / asm_amd64.s: 14

TEXT _rt0_amd64(SB),NOSPLIT,$-8
    MOVQ0(SP), DI// argc 
    LEAQ8(SP), SI // argv
    JMPruntime·rt0_go(SB)

Las primeras dos líneas de instrucciones colocan las direcciones de los parámetros argc y argv array pasados ​​por el núcleo del sistema operativo en los registros DI y SI, respectivamente, y la tercera línea de instrucciones salta a rt0_go para su ejecución.

La función rt0_go completa todo el trabajo de inicialización cuando se inicia el programa go, por lo que esta función es relativamente larga y complicada, pero aquí solo nos enfocamos en algunas inicializaciones relacionadas con el planificador, veámoslo en secciones:

tiempo de ejecución / asm_amd64.s: 87

TEXT runtime·rt0_go(SB),NOSPLIT,$0
    // copy arguments forward on an even stack
    MOVQDI, AX// AX = argc
    MOVQSI, BX// BX = argv
    SUBQ$(4*8+7), SP// 2args 2auto
    ANDQ$~15, SP     //调整栈顶寄存器使其按16字节对齐
    MOVQAX, 16(SP) //argc放在SP + 16字节处
    MOVQBX, 24(SP) //argv放在SP + 24字节处

La cuarta instrucción anterior se utiliza para ajustar el valor del registro superior de la pila para alinearlo a 16 bytes, es decir, para hacer que la dirección de la memoria apuntada por el registro superior SP de la pila sea un múltiplo de 16, y la razón por la que está alineado a 16 bytes es porque la CPU tiene un conjunto de instrucciones SSE Las direcciones de memoria que aparecen en estas instrucciones deben ser múltiplos de 16. Las dos últimas instrucciones mueven argc y argv a nuevas ubicaciones. Las otras partes de este código se han comentado con más detalle, por lo que no explicaré mucho aquí.

Inicializar g0

Continuando con el siguiente código, la variable global g0 se inicializará a continuación. Como dijimos anteriormente, la función principal de g0 es proporcionar una pila para la ejecución del código en tiempo de ejecución, por lo que aquí inicializamos principalmente varios miembros de g0 relacionados con la pila. Desde aquí se puede ver que la pila de g0 es de aproximadamente 64K y el rango de direcciones es SP-64 * 1024 + 104 ~ SP.

tiempo de ejecución / asm_amd64.s: 96

// create istack out of the given (operating system) stack.
// _cgo_init may update stackguard.
//下面这段代码从系统线程的栈空分出一部分当作g0的栈,然后初始化g0的栈信息和stackgard
MOVQ$runtime·g0(SB), DI       //g0的地址放入DI寄存器
LEAQ(-64*1024+104)(SP), BX //BX = SP - 64*1024 + 104
MOVQBX, g_stackguard0(DI) //g0.stackguard0 = SP - 64*1024 + 104
MOVQBX, g_stackguard1(DI) //g0.stackguard1 = SP - 64*1024 + 104
MOVQBX, (g_stack+stack_lo)(DI) //g0.stack.lo = SP - 64*1024 + 104
MOVQSP, (g_stack+stack_hi)(DI) //g0.stack.hi = SP

La relación entre g0 y la pila después de ejecutar las líneas de instrucciones anteriores se muestra en la siguiente figura:

imagen

 

El hilo principal está ligado a m0

Después de configurar la pila g0, omitimos la verificación del modelo de CPU y el código relacionado con la inicialización de cgo, y continuamos el análisis directamente desde la línea 164.

tiempo de ejecución / asm_amd64.s: 164

  //下面开始初始化tls(thread local storage,线程本地存储)
LEAQruntime·m0+m_tls(SB), DI //DI = &m0.tls,取m0的tls成员的地址到DI寄存器
CALLruntime·settls(SB) //调用settls设置线程本地存储,settls函数的参数在DI寄存器中

// store through it, to make sure it works
//验证settls是否可以正常工作,如果有问题则abort退出程序
get_tls(BX) //获取fs段基地址并放入BX寄存器,其实就是m0.tls[1]的地址,get_tls的代码由编译器生成
MOVQ$0x123, g(BX) //把整型常量0x123拷贝到fs段基地址偏移-8的内存位置,也就是m0.tls[0]= 0x123
MOVQruntime·m0+m_tls(SB), AX //AX = m0.tls[0]
CMPQAX, $0x123 //检查m0.tls[0]的值是否是通过线程本地存储存入的0x123来验证tls功能是否正常
JEQ 2(PC)
CALLruntime·abort(SB) //如果线程本地存储不能正常工作,退出程序

Este código primero llama a la función settls para inicializar el almacenamiento local de subprocesos (TLS) del subproceso principal. El propósito es asociar m0 con el subproceso principal. En cuanto a por qué my el subproceso de trabajo están vinculados, ya lo hemos introducido en la sección anterior. Ahora, no lo repetiré aquí. Después de configurar el almacenamiento local de subprocesos, las siguientes instrucciones son para verificar si la función TLS es normal y, si no es normal, abortar el programa directamente.

Echemos un vistazo detallado a cómo la función settls implementa las variables globales privadas de subprocesos.

tiempo de ejecución / sys_linx_amd64.s: 606

// set tls base to DI
TEXT runtime·settls(SB),NOSPLIT,$32
//......
//DI寄存器中存放的是m.tls[0]的地址,m的tls成员是一个数组,读者如果忘记了可以回头看一下m结构体的定义
//下面这一句代码把DI寄存器中的地址加8,为什么要+8呢,主要跟ELF可执行文件格式中的TLS实现的机制有关
//执行下面这句指令之后DI寄存器中的存放的就是m.tls[1]的地址了
ADDQ$8, DI// ELF wants to use -8(FS)

  //下面通过arch_prctl系统调用设置FS段基址
MOVQDI, SI //SI存放arch_prctl系统调用的第二个参数
MOVQ$0x1002, DI// ARCH_SET_FS //arch_prctl的第一个参数
MOVQ$SYS_arch_prctl, AX //系统调用编号
SYSCALL
CMPQAX, $0xfffffffffffff001
JLS2(PC)
MOVL$0xf1, 0xf1 // crash //系统调用失败直接crash
RET

Como puede ver en el código, la dirección de m0.tls [1] se establece en la dirección base del segmento fs a través de la llamada al sistema arch_prctl. Hay un registro de segmento llamado fs en la CPU correspondiente, y cada hilo tiene su propio conjunto de valores de registro de CPU. El sistema operativo nos ayudará a guardar los valores en todos los registros de la memoria cuando el hilo se sintonice fuera de la CPU. Cuando el subproceso de programación está en funcionamiento, los valores de estos registros se restaurarán de la memoria a la CPU, de modo que después de esto, el código del subproceso de trabajo pueda encontrar m.tls a través del registro fs. Los lectores pueden consulte la función tls después de inicializar tls arriba Verifique el código para comprender este proceso.

Continuemos analizando rt0_go,

tiempo de ejecución / asm_amd64.s: 174

ok:
// set the per-goroutine and per-mach "registers"
get_tls(BX) //获取fs段基址到BX寄存器
LEAQruntime·g0(SB), CX //CX = g0的地址
MOVQCX, g(BX) //把g0的地址保存在线程本地存储里面,也就是m0.tls[0]=&g0
LEAQruntime·m0(SB), AX //AX = m0的地址

//把m0和g0关联起来m0->g0 = g0,g0->m = m0
// save m->g0 = g0
MOVQCX, m_g0(AX) //m0.g0 = g0
// save m0 to g0->m 
MOVQAX, g_m(CX) //g0.m = m0

El código anterior primero coloca la dirección de g0 en el almacenamiento local de subprocesos del subproceso principal, y luego pasa

m0.g0 = &g0
g0.m = &m0

Una m0 y g0 juntos, de modo que g0 se pueda obtener a través de get_tls en el subproceso principal, y m0 se pueda encontrar a través del miembro m de g0, por lo que la asociación entre m0 y g0 y el subproceso principal se realiza aquí. También se puede ver desde aquí que el valor almacenado en el almacenamiento local del subproceso principal es la dirección de g0, lo que significa que la variable global privada del subproceso de trabajo es en realidad un puntero ag en lugar de un puntero a m. presente, este puntero apunta a g0. Indica que el código se está ejecutando en la pila g0. En este momento, la relación entre la pila del subproceso principal, m0, g0 y g0 se muestra en la siguiente figura:

imagen

 

 

Inicializar m0

El siguiente código comienza a procesar los parámetros de la línea de comandos. No nos importa esta parte, así que omítala. Después de que se procesan los parámetros de la línea de comando, se llama a la función osinit para obtener el número de núcleos de CPU y se almacena en la variable global ncpu.Cuando se inicializa el programador, necesita saber cuántos núcleos de CPU tiene el sistema actual.

tiempo de ejecución / asm_amd64.s: 189

//准备调用args函数,前面四条指令把参数放在栈上
MOVL16(SP), AX// AX = argc
MOVLAX, 0(SP)       // argc放在栈顶
MOVQ24(SP), AX// AX = argv
MOVQAX, 8(SP)       // argv放在SP + 8的位置
CALLruntime·args(SB)  //处理操作系统传递过来的参数和env,不需要关心

//对于linx来说,osinit唯一功能就是获取CPU的核数并放在global变量ncpu中,
//调度器初始化时需要知道当前系统有多少CPU核
CALLruntime·osinit(SB)  //执行的结果是全局变量 ncpu = CPU核数
CALLruntime·schedinit(SB) //调度系统初始化

A continuación, continúe para ver cómo se inicializa el programador.

tiempo de ejecución / proc.go: 526

func schedinit() {
// raceinit must be the first call to race detector.
// In particular, it must be done before mallocinit below calls racemapshadow.
   
    //getg函数在源代码中没有对应的定义,由编译器插入类似下面两行代码
    //get_tls(CX) 
    //MOVQ g(CX), BX; BX存器里面现在放的是当前g结构体对象的地址
    _g_ := getg() // _g_ = &g0

    ......

    //设置最多启动10000个操作系统线程,也是最多10000个M
    sched.maxmcount = 10000

    ......
   
    mcommoninit(_g_.m) //初始化m0,因为从前面的代码我们知道g0->m = &m0

    ......

    sched.lastpoll = uint64(nanotime())
    procs := ncpu  //系统中有多少核,就创建和初始化多少个p结构体对象
    if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
        procs = n //如果环境变量指定了GOMAXPROCS,则创建指定数量的p
    }
    if procresize(procs) != nil {//创建和初始化全局变量allp
        throw("unknown runnable goroutine during bootstrap")
    }

    ......
}

Como hemos visto anteriormente, la dirección de g0 se ha establecido en el almacenamiento local del subproceso, y schedinit usa la función getg (la función getg es implementada por el compilador y no podemos encontrar su definición en el código fuente) del almacenamiento local de subprocesos. Obtenga el g actualmente en ejecución, aquí está g0, y luego llame a la función mcommoninit para inicializar m0 (g0.m) según sea necesario. Después de que se complete la inicialización de m0, llame a procresize para inicializar el objeto de estructura p que el sistema necesidades, según go De acuerdo con el lenguaje oficial, p es el significado de procesador, y su número determina que puede haber como máximo menos goroutines ejecutándose en paralelo al mismo tiempo. Además de inicializar m0 yp, la función schedinit también establece el miembro maxmcount de la variable global sched en 10000, lo que limita el número de subprocesos del sistema operativo que se pueden crear hasta 10000 para que funcionen.

Aquí debemos centrarnos en cómo mcommoninit inicializa m0 y cómo la función procresize crea e inicializa p objetos de estructura. Primero, nos sumergimos en la función mcommoninit para averiguarlo. Cabe destacar aquí que esta función no solo se ejecuta durante la inicialización, sino también si se crea un subproceso de trabajo durante la operación del programa, por lo que veremos el bloqueo y comprobaremos si el número de subprocesos ha superado el máximo en la función y otros relacionados código.

tiempo de ejecución / proc.go: 596

func mcommoninit(mp *m) {
    _g_ := getg() //初始化过程中_g_ = g0

    // g0 stack won't make sense for user (and is not necessary unwindable).
    if _g_ != _g_.m.g0 {  //函数调用栈traceback,不需要关心
        callers(1, mp.createstack[:])
    }

    lock(&sched.lock)
    if sched.mnext+1 < sched.mnext {
        throw("runtime: thread ID overflow")
    }
    mp.id = sched.mnext
    sched.mnext++
    checkmcount() //检查已创建系统线程是否超过了数量限制(10000)

    //random初始化
    mp.fastrand[0] = 1597334677 * uint32(mp.id)
    mp.fastrand[1] = uint32(cputicks())
    if mp.fastrand[0]|mp.fastrand[1] == 0 {
        mp.fastrand[1] = 1
    }

    //创建用于信号处理的gsignal,只是简单的从堆上分配一个g结构体对象,然后把栈设置好就返回了
    mpreinit(mp)
    if mp.gsignal != nil {
        mp.gsignal.stackguard1 = mp.gsignal.stack.lo + _StackGuard
    }

    //把m挂入全局链表allm之中
    // Add to allm so garbage collector doesn't free g->m
    // when it is just in a register or thread-local storage.
    mp.alllink = allm 

    // NumCgoCall() iterates over allm w/o schedlock,
    // so we need to publish it safely.
    atomicstorep(unsafe.Pointer(&allm), unsafe.Pointer(mp))
    unlock(&sched.lock)

    // Allocate memory to hold a cgo traceback if the cgo call crashes.
    if iscgo || GOOS == "solaris" || GOOS == "windows" {
        mp.cgoCallers = new(cgoCallers)
    }
}

Puede verse en el código fuente de esta función que no hay una inicialización relacionada con la programación para m0, por lo que simplemente puede pensar que esta función simplemente coloca m0 en la lista global vinculada allm y regresa.

Después de que m0 complete la inicialización básica, continúe llamando a procresize para crear e inicializar el objeto de estructura p. En esta función, se creará un número específico de objetos de estructura p (determinado por el número de núcleos de CPU o variables de entorno) y se colocará en el allp variable completa, y enlazar m0 y allp [0] juntos, de modo que cuando se ejecute esta función, habrá

m0.p = allp[0]
allp[0].m = &m0

En este punto, m0, g0 yp requeridos por m están completamente relacionados.

Inicializar allp

Veamos la función procresize. Una vez completada la inicialización, el código de usuario también puede llamarla a través de la función GOMAXPROCS () para recrear e inicializar el objeto de estructura p. Hay muchos problemas involucrados en el ajuste dinámico de p durante la operación, por lo que El procesamiento de esta función es más complicado, pero si solo considera la inicialización, es relativamente más simple, por lo que aquí solo se retiene el código que se ejecutará durante la inicialización:

tiempo de ejecución / proc.go: 3902

func procresize(nprocs int32) *p {
    old := gomaxprocs //系统初始化时 gomaxprocs = 0

    ......

    // Grow allp if necessary.
    if nprocs > int32(len(allp)) { //初始化时 len(allp) == 0
        // Synchronize with retake, which could be running
        // concurrently since it doesn't run on a P.
        lock(&allpLock)
        if nprocs <= int32(cap(allp)) {
            allp = allp[:nprocs]
        } else { //初始化时进入此分支,创建allp 切片
            nallp := make([]*p, nprocs)
            // Copy everything up to allp's cap so we
            // never lose old allocated Ps.
            copy(nallp, allp[:cap(allp)])
            allp = nallp
        }
        unlock(&allpLock)
    }

    // initialize new P's
    //循环创建nprocs个p并完成基本初始化
    for i := int32(0); i < nprocs; i++ {
        pp := allp[i]
        if pp == nil {
            pp = new(p)//调用内存分配器从堆上分配一个struct p
            pp.id = i
            pp.status = _Pgcstop
            ......
            atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
        }

        ......
    }

    ......

    _g_ := getg()  // _g_ = g0
    if _g_.m.p != 0 && _g_.m.p.ptr().id < nprocs {//初始化时m0->p还未初始化,所以不会执行这个分支
        // continue to use the current P
        _g_.m.p.ptr().status = _Prunning
        _g_.m.p.ptr().mcache.prepareForSweep()
    } else {//初始化时执行这个分支
        // release the current P and acquire allp[0]
        if _g_.m.p != 0 {//初始化时这里不执行
            _g_.m.p.ptr().m = 0
        }
        _g_.m.p = 0
        _g_.m.mcache = nil
        p := allp[0]
        p.m = 0
        p.status = _Pidle
        acquirep(p) //把p和m0关联起来,其实是这两个strct的成员相互赋值
        if trace.enabled {
            traceGoStart()
        }
    }
   
    //下面这个for 循环把所有空闲的p放入空闲链表
    var runnablePs *p
    for i := nprocs - 1; i >= 0; i-- {
        p := allp[i]
        if _g_.m.p.ptr() == p {//allp[0]跟m0关联了,所以是不能放任
            continue
        }
        p.status = _Pidle
        if runqempty(p) {//初始化时除了allp[0]其它p全部执行这个分支,放入空闲链表
            pidleput(p)
        } else {
            ......
        }
    }

    ......
   
    return runnablePs
}

Este código de función es relativamente largo, pero no complicado, aquí hay un resumen del flujo principal de esta función:

  1. Utilice make ([] * p, nprocs) para inicializar la variable global allp, es decir, allp = make ([] * p, nprocs)

  2. Cree e inicialice objetos de estructura nprocs p cíclicamente y guárdelos en segmentos allp a su vez

  3. Unir m0 y allp [0], es decir, m0.p = allp [0], allp [0] .m = m0

  4. Ponga todo p excepto allp [0] en la cola libre pidle de la variable global sched

Una vez ejecutada la función procresize, el trabajo de inicialización relacionado con el planificador básicamente ha terminado. En este momento, la relación entre los diversos componentes de todo el planificador se muestra en la siguiente figura:

imagen

 

Después de analizar la inicialización básica del planificador, en la siguiente sección veremos cómo se crea la primera goroutine en el programa.


Finalmente, si crees que este artículo te es útil, por favor ayúdame a hacer clic en "Mirar" en la esquina inferior derecha del artículo o reenviarlo al círculo de amigos, ¡muchas gracias!

imagen

Supongo que te gusta

Origin blog.csdn.net/pyf09/article/details/115238748
Recomendado
Clasificación