Do you know how the first Goroutine of Go was created?

Or the example of the previous article:

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

Let's talk about the last article.

After schedinit completes the initialization of the scheduling system, it returns to rt0_go starts calling newproc(), creating a new goroutine to execute the runtime.main() corresponding to mainPC.

Look at runtime/asm_amd64.s file 197 lines of code:

# create a new goroutine to start programMOVQ  $runtime·mainPC(SB), AX # entry,mainPC yes runtime.main# newproc The second parameter of the stack, that is, the new one goroutine Functions to execute PUSHQ  AX          # AX = &funcval{runtime·main},​# newproc The first parameter of the stack, which represents runtime.main The parameter size required by the function because runtime.main There are no parameters, so here is 0 PUSHQ  $0CALL  runtime·newproc(SB) # establish main goroutinePOPQ  AXPOPQ  AX​# start this MCALL  runtime·mstart(SB)  # The main thread enters the scheduling loop and runs the newly created goroutine​# The above mstart should never be returned. If it is returned, there must be a problem with the code logic. Directly abortcall runtime · abort(SB)// mstart should never returnRET data runtime · mainPC+0(SB)/8,$runtime · main (sb) globe lruntime · mainPC(SB),RODATA,

runtime.main() ends with main.main(), so look at newproc() before analyzing runtime.main().

Newproc is used to create a new goroutine with two parameters. The newly created goroutine will be executed from the second parameter fn, and fn may also have parameters. The first parameter of newproc is the parameter of fn, in bytes.

Let's look at the following Go Code:

func start(a, b, c int64) {    ......}​func main() {    go start(1, 2, 3)}

When compiling the above code, the compiler will adjust it to call newproc. The compiled code logic is basically equivalent to the following code cases:

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

At compile time, the compiler first uses several instructions to stack the three parameters required by start and then calls newproc.

Since the three int64 type parameters of start account for 24 bytes in total, the first parameter passed to newproc is 24, indicating that start requires a 24 byte parameter.

Then why do you need to pass the parameter size of fn to newproc?

This is because newproc will create a new goroutine to execute fn, and the newly created goroutine does not use the same stack as the current goroutine. Therefore, when creating a new goroutine, you need to copy the parameters required by fn from the current goroutine stack to the stack used by the new goroutine, so that it can be executed, Newproc itself does not know how much data needs to be copied to the newly created goroutine stack, so it needs to use parameters to specify how much data needs to be copied.

Let's continue to analyze newproc, which is actually a package of newproc1.

Look at the detailed code in line 3232 of the runtime/proc.go file:

// 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:nosplitfunc newproc(siz int32, fn *funcval)  {/ / the stack order of function call parameters is from right to left, and the stack increases from high address to low address. / / Note: argp points to the first parameter of FN function instead of the parameter of newproc function. / / the parameter FN stores the first parameter of FN function at address + 8 on the stack. Argp: = add (unsafe. Pointer (& FN), sys. Ptrsize) GP: = getg () //Get the running g, which is m0.g0 during initialization / / getcallerpc() returns an address, that is, the address returned by the call instruction stack pressing function when calling newproc. / / for our current scenario, pc is the address of POPQ AX after the CALLruntime · newproc(SB) instruction, pc: = getcallerpc() //The function of systemstack is to switch to the G0 stack and execute the function as a parameter. / / our scenario is in the G0 stack, so we do nothing. Instead, we directly call the function systemstack (func() {newproc1 (FN, (* uint8) (argp), siz, GP, pc)}}

As can be seen from the above code, there are two most important preparations here. One is to obtain the address of the first parameter of fn, that is, argp in the code, and the other is to use systemstack to switch to g0 stack. Of course, the initialization scenario in this paper is g0, so there is no need to switch, but this function is general, and G will be created again in the user goroutine Oroutine, you need to switch the stack at this time.

The first parameter fn of newproc1 is the function to be executed by the newly created goroutine. The structure type of fn is funcval, which is defined as follows:

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

The second parameter argp of newproc1 is the address of the first parameter of fn, and the third parameter is the size of fn in bytes. To understand that newproc1 runs on the g0 stack, let's take a look at the source code in segments.

The first is line 3248 of the runtime/proc.go file:

// 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)  {/ / because we have switched to the G0 stack, there is _g_ = g0 in any scenario. Of course, this G0 refers to the G0 of the current working thread. / / for our scenario, the current working thread is the main thread, so here G0 = m0.g0 _g_: = getg()... _p_: = _g_. M.p.ptr () //During initialization, _p_ = g0.m.p. from the previous analysis, we can know that it is actually allp [0] newg: = gfget (_p_) / / obtain an unused G from the local buffer of P. during initialization, it does not return nil if newg = = nil {/ / new, a G structure object, then allocate a stack from the heap, and set the stack member of G and two stackgard members newg = MalG (_stackmin) caststatus (newg, _gidle, _Gdead) / / initialize g in _Gdead / / put it into the global variable allgs slice allgadd (newg) / / publish with a G - > status of Gdead so GC scanner doesn't look at uninitialized stack.}... / / adjust the stack top pin of G without paying attention to totalsize: = 4 * sys. Regsize + uintptr (siz) +Sys. Minframesize / / extra space in case of reads slowly beyond frame totalsize + = - totalsize & (sys. Spalign - 1) / / align to spalign SP: = newg.stack.hi - totalsize sparg: = SP / /... If narG > 0 {/ / remove parameters from the stack where newproc function is executed (G0 stack during initialization) Copy to the new G stack memmove (unsafe. Pointer (sparg), unsafe. Pointer (argp), uintpr (narG)) / /...}

The above code mainly allocates a g structure object on the heap, allocates a 2048 byte stack for this newg, sets the stack member of newg, and then copies the parameters of the function to be executed by newg from the stack executing newproc (g0 stack during initialization) to the stack of newg. Thereafter, the state of newg is as follows:

You can see that there are multiple g structure objects called newg in the program at this time to obtain the 2K stack space allocated from the heap. Newg's stack.hi and stack.lo point to the starting address of its stack space respectively.

Let's continue to look at the source code of newproc1. The location is line 3314 of the runtime/proc.go file:

    //Set all members of the newg.sched structure to 0 memclrnoheappoints (unsafe. Pointer (& newg. Sched), unsafe. Sizeof (newg. Sched)) / / set the sched members of newg. The scheduler needs these fields to schedule goroutine to the CPU. Newg.sched.sp = SP / / newg's stack top newg.stktopsp = SP / / newg.sched.pc means that when newg is scheduled to run, instructions will be executed from this address. / / set PC to the position where the function goexit is offset by 1 (sys.PCQuantum is equal to 1). / / as for why, you need to wait until the gostartcallfn function is analyzed to know newg.sched.pc = funcpc (goexit) +Sys. Pcquantum / / + pcquantum so that previous instruction is in the same function newg. Sched. G = guintptr (unsafe. Pointer (newg)) gostartcallfn (& newg. Sched, FN) / / adjust the stack of sched members and newg

The above code first initializes the sched of newg, which contains some information that the scheduler code must use when scheduling goroutine to the CPU. The sp member of sched represents the top of the stack that newg should use when it is scheduled to run, and the pc member of sched represents that newg starts executing instructions from this address when it is scheduled to run.

Let's take a look at the gostartcallfn source code to talk about why new.sched.pc in the above code is set to the second instruction of goexit instead of fn.fn?

The source code of gostartcallfn is as follows:

// Adjust gobuf as if it executed a call to FN / / and then did an immediate gosave.func gostartcallfn (gobuf * gobuf, FV * function) {var FN unsafe. Pointer if FV! = nil {FN = unsafe. Pointer (FV. FN) / / FN: the entry address of gorotine, corresponding to runtime. Main} else {FN = unsafe. Pointer (funcpc (nilfunc))} gostartcall during initialization (gobuf, fn, unsafe.Pointer(fv))}

gostartcallfn first extracts the function address from fv (runtime.main during initialization), and then continues to execute gostartcall. Here is the source code of 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: = the top of the stack of buf.sp / / newg. At present, there are only fn function parameters on the newg stack. Sp points to the first parameter of fn if sys.regsize > sys.ptrsize {SP - = sys. Ptrsize * (* uintptr) (unsafe. Pointer (SP)) = 0} SP - = sys. Ptrsize / / reserve space for the return address, / / disguise that fn is called by the goexit function, so that fn returns to goexit after execution, so as to complete the cleaning * (* uintptr) (unsafe. Pointer (SP)) =buf.pc / / put the address of goexit+1 on the stack. buf.sp = sp / / reset the stack top register of newg. / / only here can the IP register of newg really point to the fn function. Note that only some information of newg is set here. Newg has not been executed yet. / / when newg is scheduled to run, the scheduler will put buf.pc into the IP register of cpu. / / so that newg can run on the cpu buf.pc = uintptr (fn) buf. Ctxt = ctxt}

The main functions of the above codes are as follows:

  1. Adjusting the newg stack space, goexit second instructions into the stack, forged goexit to call fn, so that after the execution of fn, the ret instruction is called back to goexit to continue to perform the final cleaning.

  2. Reset new.buf.pc to the address of the function to be executed, that is, fn, which is the address of the runtime.main function in this article.

After adjusting the stack and sched of newg, let's look at newproc1. The source code is as follows:

    newg.gopc = callerpc  //Mainly used for traceback newg. Ancers = saveancers (callergp) //Set the startpc of newg to fn.fn, which is mainly used for traceback and stack contraction of function call stack / / where newg actually starts execution does not depend on this member, but sched.pc newg.startpc = fn.fn... / / set the status of g to _Grunnable, indicating that the goroutine represented by g can run casgstatus(newg, _Gdead, _Grunnable) ... / / put newg into the run queue of _p during initialization. It must be the local run queue of P. at other times, it may be put into the global queue runqput (_p, newg, true)...}

The above code is more intuitive. First, set several member variables unrelated to scheduling, then modify the newg status to _grunnableand put it into the run queue. So far, the first goroutine in the real sense of the program has been built.

At this time, the status of new, that is, main goroutine, is as follows:

The description is as follows:

  1. The sched of newg corresponding to main goroutine has been initialized. The above figure only shows pc (the first instruction to runtime.main) and sp (the memory unit at the top of the stack pointing to newg) , the memory unit pointed to by sp stores the return address of runtime.main after execution, that is, the second instruction of runtime.goexit. It is expected that runtime.main will go back to execute the CALL of runtime.exit after execution   runtime.goexit1(SB) instruction.

  2. newg has been placed in the local run queue of p bound by the current main thread. Because it is the first goroutine, it is placed in the head of the local run queue.

  3. newg's m is nil because it has not been scheduled to run, nor has it been bound to any m.

This article mainly talks about the creation of the first goroutine of the program, that is, the main goroutine. Next, let's talk about how it is scheduled to be executed by the main working thread to the CPU.

The above is only a personal point of view, not necessarily accurate. It's the best to help you.

Well, that's the end of this article. If you like, let's have a triple hit.

Scan code to pay attention to official account and get more quality content.

  

Tags: Go Assembly Language cpu goroutine

Posted on Thu, 14 Oct 2021 02:32:48 -0400 by webdata