Location>code7788 >text

Go runtime scheduler in a nutshell (3): main goroutine creation

Popularity:653 ℃/2024-09-19 11:47:33

0. Preface

Recall.previous session The main thread, m0, is ready to work. The main thread, m0, is poised and ready to do its work. g0 provides the execution environment for m0, and P is bound to m0, providing m0 with a live, or goroutine. So the question is, where is the live? Where is the work for m0?

In this talk, we'll cover the first job executed by m0, the main goroutine. main gouroutine is the goroutine that executes the main function, as opposed to the goroutine that executes the main function using thego goroutine created by the keyword, there are some differences in their execution (more on that later).

1. main goroutine creation

Continuing from the previous lecture, after the scheduler is initialized, it executes into theasm_amd64.s/rt0_go:352

TEXT runtime·rt0_go(SB),NOSPLIT|NOFRAME|TOPFRAME,$0
		...
	    // create a new goroutine to start program
352 MOVQ $runtime·mainPC(SB), AX // entry
353 PUSHQ AX
354 CALL runtime·newproc(SB)
355 POPQ AX

// dlv Go to command execution.
dlv exec ./hello
Type 'help' for list of commands.
(dlv) b /usr/local/go/src/runtime/asm_amd64.s:352
Breakpoint 1 set at 0x45433c for runtime.rt0_go() /usr/local/go/src/runtime/asm_amd64.s:352
(dlv) c
(dlv) si
> runtime.rt0_go() /usr/local/go/src/runtime/asm_amd64.s:353 (PC: 0x454343)
Warning: debugging optimized function
		asm_amd64.s:349 0x454337 e8e4290000 call $
        asm_amd64.s:352 0x45433c* 488d05659d0200 lea rax, ptr [rip+0x29d65]
=> asm_amd64.s:353 0x454343 50 push rax

Analyzed in conjunction with CPU execution instructions and Go plan9 assembly code.

First, the$runtime·mainPC(SB) The address is passed to the AX register, and the CPU executes the instructionmov qword ptr [rsp+0x8], rax. Useregs You can see the value of rax, which is$runtime·mainPC(SB) The address of the

(dlv) regs
    Rip = 0x0000000000454343
    Rsp = 0x00007ffd58324080
    Rax = 0x000000000047e0a8        // rax = $(SB) = [rsp+0x8]

in that case$(SB) What does the address of refer to? Let's see.$(SB) Definition of:

// mainPC is a function value for , to be passed to newproc.
// The reference to  is made via ABIInternal, since the
// actual function (not the ABI0 wrapper) is needed by newproc.
DATA	runtime·mainPC+0(SB)/8,$runtime·main<ABIInternal>(SB)
GLOBL	runtime·mainPC(SB),RODATA,$8

$(SB) is a program designed to perform The function value of the

carry on withPUSH AX commander-in-chief (military)(SB) onto the stack. Note that the stack here is the g0 stack, which is the stack on which the main thread m0 is running.

Moving on down the road:

=>      asm_amd64.s:354 0x45ca64        e8f72a0000              call $
        asm_amd64.s:355 0x45ca69        58                      pop rax

call (programming)$ function.newproc is the function that creates the goroutine. We use thego The goroutines created by the keyword are converted by the compiler and eventually called into thenewproc Creates the goroutine. as you can imagine, this function is very important.

Entering this function we are still operating on the g0 stack.

// Create a new g running fn.
// Put it on the queue of g's waiting to run.
// The compiler turns a go statement into a call to this.
func newproc(fn *funcval) {
	gp := getg() // gp = g0
	pc := getcallerpc() // Get the address of the caller's instruction,That is, calling the newproc from call Function return address of the instruction stack
	systemstack(func() {
		newg := newproc1(fn, gp, pc) // establish g
		...
	})
}

newproc call (programming)newproc1 Create the goroutine, and introduce the passes to thenewproc1 specificationsfngp cap (a poem)pc

in the first placefn Yes, it contains function value, print the value of thefn Below:

(dlv) print fn
(*)(0x47e0a8)
* {fn: 4386432}

As can be seen.fn is a pointer to the structurefuncval address (that is, the previously described$(SB)Address0x47e0a8), which are contained within the structurefn It is the actual implementation of the The address of the function:

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

The second argument, gp, is equal to g0, which provides the runtime environment for the main thread, m0, and pc, which is the return address of the function that was stacked by the call instruction when newproc was called.

Parameters are done. Look at them.systemstack function.systemstack wouldgoroutine runningfn The call runs to the system stack (g0 stack), where m0 is already running on the g0 stack and does not need to be called. If the goroutine is not on the g0 stack, e.g. m0 is running on the g1 stack, then thesystemstack It cuts the g1 stack to the g0 stack, then runs fn and returns to the g1 stack. For more details, seehere are

Now enternewproc1(fn, gp, pc) ferret outnewproc1 How a new goroutine is created.

func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
mp := acquirem() // acquirem gets the current thread the goroutine is bound to, here m0
pp := () // acquire the P that the thread is bound to, here pp = allp[0]

// Get the free goroutine from P's local gFree queue or the global gFree queue, or nil if it's not available.
// This is the main goroutine creation phase, there are no free goroutines.
newg := gfget(pp)
if newg == nil {
newg = malg(stackMin) // malg creates new goroutine
casgstatus(newg, _Gidle, _Gdead) // the initial state of the created goroutine is _Gidle, here update the goroutine state to _Gdead
allgadd(newg) // Add the new goroutine to the global variable allgs.
}
...
}

first callgfget Get the idle goroutine in the current thread P or in the global idle queue, if there is none then call themalg(stackMin) Create a new goroutine.malg(stackMin) hit the nail on the headstackMin Equal to 2048, or 2K. viewmalg Did what:

func malg(stacksize int32) *g {
	newg := new(g) // new establish g
	if stacksize >= 0 { // stacksize = 2048
		stacksize = round2(stackSystem + stacksize) // stackSystem = 0, stacksize = 2048
		systemstack(func() {
			 = stackalloc(uint32(stacksize)) // call (programming) stackalloc new acquisition goroutine a wooden or bamboo pen for sheep or cattle,meso- (chemistry) goroutine a wooden or bamboo pen for sheep or cattle大小为 2K
		})
		newg.stackguard0 = + stackGuard
		newg.stackguard1 = ^uintptr(0)
		// Clear the bottom word of the stack. We record g
		// there on gsignal stack during VDSO on ARM and ARM64.
		*(*uintptr)(()) = 0
	}
	return newg
}

malg Create a new goroutine with a goroutine stack size of 2KB.

then callcasgstatus Update the status of the goroutine to_Gdead. Then call theallgadd function will create the goroutine and the global variableallgs Associated:

func allgadd(gp *g) {
lock(&allglock) // allgs is a global variable, add a lock to the global variable.
allgs = append(allgs, gp) // add newg:gp to allgs
if &allgs[0] ! = allgptr { // allgptr is a pointer to allgs[0], here nil
atomicstorep((&allgptr), (&allgs[0])) // allgptr = &allgs[0]
}
(&allglen, uintptr(len(allgs))) // update global variable allglen = len(allgs)
unlock(&allglock) // unlock the variable
}

Keep reading.newproc1 of the implementation process:

func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
	...
	totalSize := uintptr(4* + ) // extra space in case of reads slightly beyond frame
	totalSize = alignUp(totalSize, )
	sp := - totalSize // sp is a pointer to the top of the stack

	// set up All members of the 0,Followed by reassigning them
	memclrNoHeapPointers((&), ())
	 = sp
	 = sp

	// denote newg The instructions are executed from this address when they are scheduled to run.
	 = abi.FuncPCABI0(goexit) + // +PCQuantum so that previous instruction is in same function
	 = guintptr((newg))
	gostartcallfn(&, fn)

This code essentially gives the Assignment. of the structure is as follows:

type gobuf struct {
sp uintptr // pointer to the top of the stack of the goroutine. pc uintptr // address of the instruction that executes the goroutine.
pc uintptr // address of the instruction that executes the goroutine
g guintptr // Address of the goroutine.
ctxt // address of the structure funcval that wraps the function executed by the goroutine
ret uintptr // Return address
lr uintptr
bp uintptr
}

The main members are shown in the comments, and the thread will know where to run the code from by using this structure.

In the assignment When, this code is interesting:

 = abi.FuncPCABI0(goexit) + 

It is a combination ofgoexit The address of the function + 1 after passing theView at this time The value of the

  4530:          = abi.FuncPCABI0(goexit) +  // +PCQuantum so that previous instruction is in same function
=>4531:          = guintptr((newg))
(dlv) print 
 {sp: 824633976800, pc: 4540513, g: 0, ctxt: (0x0), ret: 0, lr: 0, bp: 0}
(dlv) print (4540513)
(0x454861)

In reality, it will be0x454861 Pass it on.Let's forget about that for a moment.0x454861, then read on. Callinggostartcallfn(&, fn) function:

func gostartcallfn(gobuf *gobuf, fv *funcval) {
var fn
if fv ! = nil {
fn = () // Assign to fn, which is actually the address of fn.
} else {
fn = ((nilfunc))
}
gostartcall(gobuf, fn, (fv))
}

func gostartcall(buf *gobuf, fn, ctxt ) {
sp := // take top-of-stack pointer from g1
sp -= // take the top-of-stack pointer and subtract 1 byte from it.
*(*uintptr)((sp)) = // Decrease 1 byte of space for abi.FuncPCABI0(goexit) + = sp // Decrease 1 byte of space for abi.
= sp // Use the subtracted 1 byte of sp as the new top of the stack.
= uintptr(fn) // repoint pc to fn
= ctxt // point pc to funcval again
}

Seeing this we understand why we need to add a layer ofgoexit and subtracts 1 from the top-of-stack pointer to make the new top of the stack. This is because the new top of the stack will be executed when you return to thegoexitThis is what the scheduler expects every goroutine to do after executing thegoexit It's the only way to really quit.

Okay, let's get back tonewproc1 Keep looking down:

func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
...
= // newg's parent id, = 0
= callerpc // caller's pc

= // = = = &

...
casgstatus(newg, _Gdead, _Grunnable) // Update the status of newg to _Grunnable.
// Get the new one via goidcache, where the main goroutine's goid is 1.

...
releasem(mp)
return newg
}

At this point our new goroutine is created. To recap, we first request 2KB of stack space for the new goroutine, and then we create the environment for executing the goroutine in the new goroutine.The thread is based on the Finally, set the state of the goroutine to _Grunnable to indicate that the goroutine is ready to run.

We draw the memory distribution based on the above analysis as follows:

image

2. Summary

The logic of creating the main gouroutine is basically finished here. In the next section, we will continue to describe how the main gouroutine is run.