Crear goroutine principal del programador de idiomas de Go (13)

El siguiente contenido se reproduce de  https://mp.weixin.qq.com/s/FF8YU8nXP9TKt0qvSuzmtw

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

Este artículo es el capítulo 13 de la serie "Análisis de escenarios de código fuente del programador de idiomas de Go", y también es la tercera subsección del capítulo 2.


En la sección anterior analizamos la inicialización del planificador, en esta sección vemos cómo se crea la primera gorutina del programa.

Crear goroutine principal

Continuando con la sección anterior, después de que schedinit completa la inicialización del sistema de programación, vuelve a la función rt0_go y comienza a llamar a newproc () para crear una nueva goroutine para ejecutar la función runtime · main correspondiente a mainPC. Mira el siguiente código:

tiempo de ejecución / asm_amd64.s: 197

# create a new goroutine to start program
MOVQ  $runtime·mainPC(SB), AX # entry,mainPC是runtime.main
# newproc的第二个参数入栈,也就是新的goroutine需要执行的函数
PUSHQ  AX          # AX = &funcval{runtime·main},

# newproc的第一个参数入栈,该参数表示runtime.main函数需要的参数大小,因为runtime.main没有参数,所以这里是0
PUSHQ  $0
CALL  runtime·newproc(SB) # 创建main goroutine
POPQ  AX
POPQ  AX

# start this M
CALL  runtime·mstart(SB)  # 主线程进入调度循环,运行刚刚创建的goroutine

# 上面的mstart永远不应该返回的,如果返回了,一定是代码逻辑有问题,直接abort
CALL  runtime·abort(SB)// mstart should never return
RET

DATA  runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOB  Lruntime·mainPC(SB),RODATA,$8

En el proceso de análisis posterior, veremos que este runtime.main eventualmente llamará a la función main.main que escribimos Antes de analizar runtime · main, nos centraremos en la función newproc.

La función newproc se utiliza para crear una nueva goroutine. Tiene dos parámetros. Hablemos del segundo parámetro fn. La nueva goroutine creada se ejecutará desde la función fn, y la función fn también puede tener parámetros, el primero de newproc Cada parámetro es el tamaño del parámetro de la función fn en bytes. Por ejemplo, existe el siguiente fragmento de código de Go:

func start(a, b, c int64) {
    ......
}

func main() {
    go start(1, 2, 3)
}

Cuando el compilador compila la sentencia go anterior, la reemplazará con una llamada a la función newproc. El código compilado es lógicamente equivalente al siguiente pseudocódigo

func main() {
    push 0x3
    push 0x2
    push 0x1
    runtime.newproc(24, start)
}

Al compilar, el compilador primero usará varias instrucciones para insertar los 3 parámetros necesarios para la función de inicio en la pila, y luego llamará a la función newproc. Debido a que los tres parámetros int64 de la función de inicio ocupan 24 bytes en total, el primer parámetro que se pasa a newproc es 24, lo que significa que la función de inicio requiere un parámetro de 24 bytes.

Entonces, ¿por qué necesitamos pasar el tamaño del parámetro de la función fn a la función newproc? La razón es que la función newproc creará una nueva goroutine para ejecutar la función fn, y esta goroutine recién creada usará una pila diferente de la goroutine actual, por lo que debe cambiar los parámetros que fn necesita usar al crear la goroutine desde la actual. La pila de goroutine se copia en la pila de la nueva goroutine antes de que se pueda ejecutar. La función newproc en sí no sabe cuántos datos deben copiarse en la pila de la nueva goroutine, por lo que es necesario especificar cuántos datos copiar mediante parámetros.

Después de comprender los conocimientos previos, analicemos el código de newproc. La función newproc es un contenedor para newproc1. Aquí hay dos preparaciones más importantes. Una es obtener la dirección del primer parámetro de la función fn (argp en el código), y la otra es usar la función systemstack para cambiar a la pila g0. Por supuesto, para nuestro escenario de inicialización, ya está en la pila g0, por lo que no es necesario cambiar. Sin embargo, esta función es universal. La goroutine también se creará en la goroutine del usuario. En este momento, la pila debe cambiarse.

tiempo de ejecución / proc.go: 3232

// Create a new g running fn with siz bytes of arguments.
// Put it on the queue of g's waiting to run.
// The compiler turns a go statement into a call to this.
// Cannot split the stack because it assumes that the arguments
// are available sequentially after &fn; they would not be
// copied if a stack split occurred.
//go:nosplit
func newproc(siz int32, fn *funcval) {
    //函数调用参数入栈顺序是从右向左,而且栈是从高地址向低地址增长的
    //注意:argp指向fn函数的第一个参数,而不是newproc函数的参数
    //参数fn在栈上的地址+8的位置存放的是fn函数的第一个参数
    argp := add(unsafe.Pointer(&fn), sys.PtrSize)
    gp := getg()  //获取正在运行的g,初始化时是m0.g0
   
    //getcallerpc()返回一个地址,也就是调用newproc时由call指令压栈的函数返回地址,
    //对于我们现在这个场景来说,pc就是CALLruntime·newproc(SB)指令后面的POPQ AX这条指令的地址
    pc := getcallerpc()
   
    //systemstack的作用是切换到g0栈执行作为参数的函数
    //我们这个场景现在本身就在g0栈,因此什么也不做,直接调用作为参数的函数
    systemstack(func() {
        newproc1(fn, (*uint8)(argp), siz, gp, pc)
    })
}

El primer parámetro fn de la función newproc1 es la función que ejecutará la goroutine recién creada. Tenga en cuenta que el tipo de este fn es el tipo de estructura de función, que se define de la siguiente manera:

type funcval struct {
    fn uintptr
    // variable-size, fn-specific data here
}

El segundo parámetro argp de newproc1 es la dirección del primer parámetro de la función fn, y el tercer parámetro es el tamaño del parámetro de la función fn en bytes No nos importan los dos últimos parámetros. Cabe señalar aquí que newproc1 se ejecuta en la pila de g0. Esta función es muy larga e importante, así que veámosla en secciones.

tiempo de ejecución / proc.go: 3248

// Create a new g running fn with narg bytes of arguments starting
// at argp. callerpc is the address of the go statement that created
// this. The new g is put on the queue of g's waiting to run.
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
    //因为已经切换到g0栈,所以无论什么场景都有 _g_ = g0,当然这个g0是指当前工作线程的g0
    //对于我们这个场景来说,当前工作线程是主线程,所以这里的g0 = m0.g0
    _g_ := getg() 

    ......

    _p_ := _g_.m.p.ptr() //初始化时_p_ = g0.m.p,从前面的分析可以知道其实就是allp[0]
    newg := gfget(_p_) //从p的本地缓冲里获取一个没有使用的g,初始化时没有,返回nil
    if newg == nil {
         //new一个g结构体对象,然后从堆上为其分配栈,并设置g的stack成员和两个stackgard成员
        newg = malg(_StackMin)
        casgstatus(newg, _Gidle, _Gdead) //初始化g的状态为_Gdead
         //放入全局变量allgs切片中
        allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
    }
   
    ......
   
    //调整g的栈顶置针,无需关注
    totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frame
    totalSize += -totalSize & (sys.SpAlign - 1)                  // align to spAlign
    sp := newg.stack.hi - totalSize
    spArg := sp

    //......
   
    if narg > 0 {
         //把参数从执行newproc函数的栈(初始化时是g0栈)拷贝到新g的栈
        memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg))
        // ......
    }

Este código principalmente asigna un objeto de estructura ag del montón y asigna una pila de 2048 bytes para este newg, y establece el miembro de la pila de newg, y luego establece los parámetros de la función que newg necesita ejecutar desde la pila que ejecuta la función newproc . (Es la pila g0 en el momento de la inicialización) Copie en la pila de newg, después de completar estas cosas, el estado de newg se muestra en la siguiente figura:

 

Podemos ver que después del código anterior, hay un objeto de estructura g adicional que llamamos newg en el programa. Este objeto también ha obtenido un espacio de pila de 2k asignado del montón, la pila de newg. Hi y stack.lo respectivamente apuntan a las posiciones inicial y final de su espacio de pila.

A continuación, continuamos analizando la función newproc1.

tiempo de ejecución / proc.go: 3314

    //把newg.sched结构体成员的所有成员设置为0
    memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
   
    //设置newg的sched成员,调度器需要依靠这些字段才能把goroutine调度到CPU上运行。
    newg.sched.sp = sp  //newg的栈顶
    newg.stktopsp = sp
    //newg.sched.pc表示当newg被调度起来运行时从这个地址开始执行指令
    //把pc设置成了goexit这个函数偏移1(sys.PCQuantum等于1)的位置,
    //至于为什么要这么做需要等到分析完gostartcallfn函数才知道
    newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
    newg.sched.g = guintptr(unsafe.Pointer(newg))

    gostartcallfn(&newg.sched, fn) //调整sched成员和newg的栈

Este código primero inicializa el miembro sched de newg. Este miembro contiene cierta información necesaria para que el código del planificador programe la goroutine para que se ejecute en la CPU. El miembro sp de sched representa la pila que debe usarse cuando se programa la ejecución de newg. En la parte superior de la pila, el miembro pc de sched indica que las instrucciones se ejecutarán desde esta dirección cuando se programe la ejecución de newg. Sin embargo, como se puede ver en el código anterior, new.sched.pc se establece en la dirección del segunda instrucción de la función goexit. No fn.fn, ¿por qué? Para responder a esta pregunta, debe profundizar en la función gostartcallfn para un análisis más detallado.

// adjust Gobuf as if it executed a call to fn
// and then did an immediate gosave.
func gostartcallfn(gobuf *gobuf, fv *funcval) {
    var fn unsafe.Pointer
    if fv != nil {
        fn = unsafe.Pointer(fv.fn) //fn: gorotine的入口地址,初始化时对应的是runtime.main
    } else {
        fn = unsafe.Pointer(funcPC(nilfunc))
    }
    gostartcall(gobuf, fn, unsafe.Pointer(fv))
}

gostartcallfn primero extrae la dirección de la función del parámetro fv (runtime.main durante la inicialización), y luego continúa llamando a la función gostartcall.

// adjust Gobuf as if it executed a call to fn with context ctxt
// and then did an immediate gosave.
func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) {
    sp := buf.sp //newg的栈顶,目前newg栈上只有fn函数的参数,sp指向的是fn的第一参数
    if sys.RegSize > sys.PtrSize {
        sp -= sys.PtrSize
        *(*uintptr)(unsafe.Pointer(sp)) = 0
    }
    sp -= sys.PtrSize //为返回地址预留空间,
    //这里在伪装fn是被goexit函数调用的,使得fn执行完后返回到goexit继续执行,从而完成清理工作
    *(*uintptr)(unsafe.Pointer(sp)) = buf.pc //在栈上放入goexit+1的地址
    buf.sp = sp //重新设置newg的栈顶寄存器
    //这里才真正让newg的ip寄存器指向fn函数,注意,这里只是在设置newg的一些信息,newg还未执行,
    //等到newg被调度起来运行时,调度器会把buf.pc放入cpu的IP寄存器,
    //从而使newg得以在cpu上真正的运行起来
    buf.pc = uintptr(fn) 
    buf.ctxt = ctxt
}

Hay dos funciones principales de la función gostartcall:

  1. Ajuste el espacio de la pila de newg, coloque la dirección de la segunda instrucción de la función goexit en la pila y falsifique la función goexit para llamar a fn, de modo que cuando se ejecute la instrucción ret después de que se complete la ejecución de fn, vuelva a goexit continuar con la ejecución y completar el trabajo de limpieza final;

  2. Restablezca newg.buf.pc a la dirección de la función que debe ejecutarse, es decir, fn, nuestro escenario es la dirección de la función runtime.main.

Después de ajustar la pila y los miembros programados de newg, regrese a la función newproc1, continuamos mirando hacia abajo,

    newg.gopc = callerpc  //主要用于traceback
    newg.ancestors = saveAncestors(callergp)
    //设置newg的startpc为fn.fn,该成员主要用于函数调用栈的traceback和栈收缩
    //newg真正从哪里开始执行并不依赖于这个成员,而是sched.pc
    newg.startpc = fn.fn  

    ......
   
    //设置g的状态为_Grunnable,表示这个g代表的goroutine可以运行了
    casgstatus(newg, _Gdead, _Grunnable)

    ......
   
    //把newg放入_p_的运行队列,初始化的时候一定是p的本地运行队列,其它时候可能因为本地队列满了而放入全局队列
    runqput(_p_, newg, true)

    ......
}

El último código de la función newproc1 es relativamente intuitivo. Primero, establezca varias variables miembro que no estén relacionadas con la programación, y luego modifique el estado de newg a _Grunnable y colóquelo en la cola de ejecución. Esta es la primera goroutine real en este programa Ha sido creado.

En este momento, el estado de newg, que es la goroutine principal, se muestra en la siguiente figura:

imagen

Esta imagen parece más complicada porque hay demasiadas flechas que representan el puntero. Aquí hay una breve explicación.

  • Primero, se ha inicializado el miembro sched del objeto de estructura newg correspondiente a la goroutine principal. En la figura solo se muestran los miembros pc y sp. El miembro pc apunta a la primera instrucción de la función runtime.main y el miembro sp apunta a la parte superior de la pila newg. La unidad de memoria, que guarda la dirección de retorno después de que se completa la ejecución de la función runtime.main, que es la segunda instrucción de la función runtime.goexit. Se espera que el runtime.main la función ejecutará la CALL de la función runtime.exit después de que regrese runtime.goexit1 (SB) esta instrucción;

  • En segundo lugar, newg se ha colocado en la cola de ejecución local del objeto de estructura p vinculado al subproceso principal actual, porque es la primera goroutine real y no hay otra goroutine, por lo que se coloca a la cabeza de la unidad de cola de ejecución local ;

  • Finalmente, el miembro m de newg es nil, porque no se ha programado para ejecutarse y no está vinculado a ningún m.

En esta sección, analizamos la creación de la primera goroutine en el programa, es decir, la goroutine principal, en la siguiente sección continuaremos analizando cómo es enviada a la CPU por el hilo de trabajo principal para su ejecución.

 


Finalmente, si cree que este artículo es útil para usted, por favor ayúdeme 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/115238792
Recomendado
Clasificación