Since ancient times applications have started with Hello World, and so has the Go language that you and I have written:

import "fmt"

func main() {
	fmt.Println("hello world.")
}
Copy the code

The output of this program is Hello World. It’s as simple and straightforward as that. But at this time, I can’t help thinking about how this Hello world is output and what process it has gone through.

Really very curious, today we come together to explore the Go program start process. Which involves the Go Runtime scheduler startup, what is g0, m0?

Door welded to death, officially began to suck fish road.

Go boot phase

To find the entrance

Start by compiling the sample program mentioned above:

$ GOFLAGS="-ldflags=-compressdwarf=false" go build 
Copy the code

The GOFLAGS parameter is specified in the command because as of Go1.11, debugging information is compressed to reduce the binary file size. It makes it difficult to understand the meaning of compressed DWARF when using GDB on MacOS (which I do).

Therefore, it is necessary to turn it off in this debugging, and then use GDB for debugging, so as to achieve the purpose of observation:

$ gdb awesomeProject (gdb) info files Symbols from "/Users/eddycjy/go-application/awesomeProject/awesomeProject". Local exec file: `/Users/eddycjy/go-application/awesomeProject/awesomeProject', file type mach-o-x86-64. Entry point: 0x1063c80 0x0000000001001000 - 0x00000000010a6aca is .text ... (gdb) b *0x1063c80 Breakpoint 1 at 0x1063c80: The file/usr/local/Cellar/go / 1.15 / libexec/SRC/runtime/rt0_darwin_amd64. S, line 8.Copy the code

Through Entry Point debugging, you can see that the real Entry of the program is in the Runtime package. Different computer architectures point to different points. Such as:

  • MacOS insrc/runtime/rt0_darwin_amd64.s.
  • Linux insrc/runtime/rt0_linux_amd64.s.

It ends up pointing to the rt0_darwin_amd64.s file, which has a very intuitive name:

Breakpoint 1 at 0 x1063c80: the file/usr/local/Cellar/go / 1.15 / libexec/SRC/runtime/rt0_darwin_amd64. S, line 8.Copy the code

Rt0 stands for Runtime0 and refers to genesis at runtime, super Dad:

  • Darwin stands for target operating system (GOOS).
  • Amd64 stands for target operating system architecture (GOHOSTARCH).

Go also supports more target system architectures, such as AMD64, AMR, MIPS, WASM, etc.

If you are interested, go to the SRC/Runtime directory for a further look, which is not covered here.

Entry method

In the rt0_linux_amd64.s file, _rt0_amd64_DARWIN JMP jumped to the _rt0_amd64 method:

TEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8
	JMP	_rt0_amd64(SB)
...
Copy the code

Runtime ·rt0_go

TEXT _rt0_amd64(SB),NOSPLIT,$-8
	MOVQ	0(SP), DI	// argc
	LEAQ	8(SP), SI	// argv
	JMP	runtime·rt0_go(SB)
Copy the code

This method moves program input argc and argv from memory to registers.

The first two values of the stack pointer (SP) are argc and argv, which correspond to the number of arguments and the value of each argument.

Open the main line

With the program parameters ready, the formal initialization method falls into the Runtime ·rt0_go method:

The TEXT, the runtime rt0_go (SB), NOSPLIT, $0... CALL Runtime ·check(SB) MOVL 16(SP), AX // copy argc MOVL AX, 0(SP) MOVQ 24(SP), AX // copy argv MOVQ AX, 8(SP) CALL Runtime ·args(SB) CALL Runtime ·osinit(SB) CALL Runtime ·schedinit(SB) // Create a new goroutine to start The program MOVQ $runtime · mainPC (SB), AX // entry PUSHQ AX PUSHQ $0 // arg size CALL Runtime ·newproc(SB) POPQ AX POPQ AX // start this M CALL The runtime, mstart (SB)...Copy the code
  • Runtime. check: A runtime type check to verify that the compiler’s translation is working correctly. The basic code is checkedint8unsafe.SizeofMethod is equal to 1.
  • Runtime. args: System parameter passing, mainly to pass system parameter conversion to the program for use.
  • Runtime. osinit: Sets basic system parameters, including the number of CPU cores and the physical page size of memory.
  • Runtime.schedinit: Initializes various run-time components, including schedulers, memory allocators, heaps, stacks, GC, etc. P is initialized and m0 is bound to some p.
  • Runtime. main: The main job is to run main Goroutine, although inThe runtime, rt0_goTheta is pointing to theta$runtime·mainPCBut the point isruntime.main.
  • Runtime. newProc: Creates a new Goroutine and binds itruntime.mainMethod (that is, the entry main method in the application). And put it into the local queue of P bound by M0 for subsequent scheduling.
  • Runtime. mstart: start M, and the scheduler starts the cycle scheduling.

In the Runtime · rt0_GO method, it mainly completes various run-time checks, system parameter Settings and acquisition, and initializes a large number of Go basic components.

After initialization, the main goroutine is run, and put into the waiting queue (GMP model), and finally the scheduler starts to cycle scheduling.

summary

Based on the above source code analysis, the following flow chart of Go application guidance can be obtained:

In Go, the actual entry point is not the main func or runtime.main method, but rt0_*_amd64.s, and finally JMP to Runtime ·rt0_go. This method does most of the initialization that Go itself needs to do.

Including:

  • Runtime type checking, system parameter passing, CPU core acquisition and setting, initialization of runtime components (scheduler, memory allocator, heap, stack, GC, etc.).
  • Run main Goroutine.
  • Run a number of default behaviors such as corresponding GMP.
  • A lot of knowledge about schedulers is involved.

We’ll take a closer look at love and hate in Runtime ·rt0_go, especially with scheduling methods like Runtime. main and Runtime. schedinit.

The Go scheduler initializes

Now that we know how the Go program boots up, we need to understand how the scheduler flows through the Go Runtime.

runtime.mstart

Here we focus on the runtime.mstart method:

Func mstart() {// get g0 _g_ := getg() // determine the stack boundary osStack := _g_.stack.lo == 0 if osStack {size := _g_.stack.hi if size == 0 { size = 8192 * sys.StackGuardMultiplier } _g_.stack.hi = uintptr(noescape(unsafe.Pointer(&size))) _g_.stack.lo = _g_.stack. hi-size + 1024} _g_.stackGuard0 = _g_.stack.lo + _StackGuard _g_.stackGuard1 = _g_.stackGuard0 Allocated Thread if mStackIsSystemAllocated() {osStack = true} mexit(osStack)}Copy the code
  • callgetgMethod to obtain g in the GMP model, here g0 is obtained.
  • By checking g’s execution stack_g_.stackThe stack boundary is exactly LO, hi to determine whether it is a system stack. If so, g execution stack boundaries are initialized according to the system stack.
  • callmstart1Methods Start the system thread M to schedule the scheduler cycle.
  • callmexitMethod to exit the system thread M.

runtime.mstart1

So it seems that the essential logic is in the mstart1 method, we continue to analyze:

Func mstart1() {// get g0 _g_ := getg() if _g_! = _g_. M.g 0 {throw (" bad runtime · mstart ")} / / initializes the m and record the caller PC, sp save (getcallerpc (), Getcallersp ()) asminit() minit() // Set signal handler if _g_. M == &m0 {mstartm0()} // Run the start function if fn := _g_. fn ! = nil { fn() } if _g_.m ! = &m0 { acquirep(_g_.m.nextp.ptr()) _g_.m.nextp = 0 } schedule() }Copy the code
  • callgetgMethod to obtain g. And through the previous binding_g_.m.g0Determine whether the obtained g is G0. If not, a fatal error is thrown. Because the scheduler only runs on G0.
  • callminitMethod initializes M, and records the PC and SP of the caller for reuse in the subsequent schedule stage.
  • Is called if it is determined that the m bound to the current g is m0mstartm0Method to set up a signal handler. The action must be inminitAfter method, like thisminitMethod to prepare threads in advance so that signals can be processed.
  • Runs if m bound to g currently has a start function. Otherwise skip.
  • If m bound to g is not m0, the call is requiredacquirepMethod takes and binds p, that is, m binds p.
  • callscheduleMethod for formal scheduling.

After a long circle of busy work, I finally entered the main course of the topic. The original hidden schedule method is the real method of scheduling, while the other methods are pre-processing and data preparation.

Due to the length problem, the analysis of schedule method will be continued in the next chapter. We will focus on some details of this article first.

Problems of deep profile control

But here the space has been relatively long, accumulated a lot of problems. We took a look at the two most visible elements in Runtime:

  1. m0What is it? What does it do?
  2. g0What is it? What does it do?

m0

M0 is the first system thread created by the Go Runtime. A Go process has only one M0, also known as the main thread.

In many ways:

  • Data structure: M0 is no different from any other CREATED M.
  • Creation process: M0 should be compiled and copied directly to m0 when the process is started, other subsequent m’s are created by Go Runtime itself.
  • Variable declaration: m0 is defined as mvar m0 mNothing special.

g0

G is generally divided into three types, namely:

  • Those who perform user tasks are called G.
  • performruntime.mainThe main goroutine.
  • The name of the scheduling task is G0.

G0 is special in that there is only one G0 for each M (and only one g0), and only one g0 is bound to each M. The assignment of g0 is also done by assembly, and the rest of the subsequent creation is regular G.

In many ways:

  • Data structure: G0 is the same data structure as the other created GS, but there are stack differences. The stack on G0 is allocated to the system stack. On Linux, the stack size is fixed by default at 8MB and cannot be expanded or shrunk. Conventional G starts at 2KB and can be expanded.
  • Running state: Unlike regular G, G0 has fewer running states and is not preempted by the scheduler, which itself runs on G0.
  • Variable declarations: g0 and general g, defined asvar g0 gNothing special.

summary

In this chapter, we explained a process of Go scheduler initialization, involving:

  • The runtime. Mstart.
  • The runtime mstart1.

Based on this, we also learned what to prepare and initialize during the scheduler initialization process. In addition, the concepts of M0 and G0 mentioned most frequently in the scheduling process are sorted out and explained.

conclusion

In today’s article, we have gone through all the flow and initialization actions of the Go language’s boot boot process in detail.

At the same time, the initialization of the scheduler is analyzed, and the use and difference of M0 and G0 are introduced in detail. In the next article we will take a closer look at the schedule method for real scheduling, which is also a tough one.