search [160cc2e20e9120 brain into the fried fish ] follow this fried fish with fried liver. This article GitHub github.com/eddycjy/blog has been included, there are my series of articles, materials and open source Go books.
Hello everyone, I am fried fish.
Since ancient times, applications have all started from Hello World, and the Go language written by you and me is the same:
import "fmt"
func main() {
fmt.Println("hello world.")
}
The output of this program is hello world.
, which is so simple and straightforward. But at this time I couldn't help thinking about hello world.
was output and what process it went through.
I am very curious. Today we will explore the startup process of the Go program together.
Which involves the start of the Go Runtime scheduler, what are g0 and m0?
The door was welded to death, and the road to sucking fish was officially started.
Go boot phase
Find the entrance
First compile the sample program mentioned above:
$ GOFLAGS="-ldflags=-compressdwarf=false" go build
The GOFLAGS parameter is specified in the command. This is because since Go1.11, in order to reduce the size of the binary file, the debugging information will be compressed. When using gdb on MacOS, I cannot understand the meaning of compressed DWARF (and I use MacOS).
Therefore, you need to turn it off during this debugging, and then use gdb for debugging 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: file /usr/local/Cellar/go/1.15/libexec/src/runtime/rt0_darwin_amd64.s, line 8.
Through the debugging of Entry point, you can see that the real program entry is in the runtime package, and different computer architectures point to different points. E.g:
- MacOS is in
src/runtime/rt0_darwin_amd64.s
. - Linux is in
src/runtime/rt0_linux_amd64.s
.
It finally points to the rt0_darwin_amd64.s file, the name of this file is very intuitive:
Breakpoint 1 at 0x1063c80: file /usr/local/Cellar/go/1.15/libexec/src/runtime/rt0_darwin_amd64.s, line 8.
rt0 stands for the abbreviation of runtime0, referring to the creation of the runtime, super dad:
- darwin stands for Target Operating System (GOOS).
- amd64 stands for the target operating system architecture (GOHOSTARCH).
At the same time, the Go language also supports more target system architectures, such as AMD64, AMR, MIPS, WASM, etc.:
If you are interested src/runtime
directory, and I won’t introduce them here.
Entry method
In the rt0_linux_amd64.s file, you can find that _rt0_amd64_darwin
JMP jumps to the _rt0_amd64
method:
TEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)
...
Then jump to the runtime·rt0_go
method:
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
JMP runtime·rt0_go(SB)
This method moves the argc and argv input by the program from the memory to the register.
The first two values of the stack pointer (SP) are argc and argv, which correspond to the number of parameters and the values of specific parameters.
Open the main line
After the program parameters are ready, the formal initialization method falls in the runtime·rt0_go
method:
TEXT 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 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 runtime·mstart(SB)
...
- runtime.check: Run-time type checking, mainly to verify whether the translation work of the compiler is correct and whether there are "pits". The basic code is to check
int8
unsafe.Sizeof
is equal to 1 under the 060cc2e20e946a method. - runtime.args: System parameter transfer, mainly to transfer the system parameter conversion to the program for use.
- runtime.osinit: Basic system parameter settings, mainly to obtain the number of CPU cores and the size of physical memory pages.
- runtime.schedinit: Perform initialization of various runtime components, including a lot of initialization work such as scheduler, memory allocator, heap, stack, and GC. It will initialize p and bind m0 to a certain p.
- runtime.main: The main job is to run the main goroutine. Although
runtime·rt0_go
points to$runtime·mainPC
, it actually points toruntime.main
. - runtime.newproc: Create a new goroutine and bind the
runtime.main
method (that is, the entry main method in the application). And put it into the local queue of p bound to m0 for subsequent scheduling. - runtime.mstart: Start m, and the scheduler starts cyclic scheduling.
In the runtime·rt0_go
method, it mainly completes various runtime checks, system parameter setting and acquisition, and a large number of Go basic component initializations.
After the initialization is completed, the main goroutine is run and put into the waiting queue (GMP model), and finally the scheduler starts to perform cyclic scheduling.
summary
According to the above source code analysis, the following flow chart of the Go application boot can be obtained:
In the Go language, the actual entry point is not the main func
that the user writes daily, nor the runtime.main
method. It starts from rt0_*_amd64.s
, and finally goes all the way from JMP to runtime·rt0_go
, and then completes a series of Go needs in this method. Most of the initialization actions completed.
The overall includes:
- Runtime type checking, system parameter transfer, CPU core number acquisition and setting, runtime component initialization (scheduler, memory allocator, heap, stack, GC, etc.).
- Run the main goroutine.
- Run the corresponding GMP and many other default behaviors.
- A lot of knowledge related to the scheduler is involved.
Follow-up will continue to analyze further analysis runtime·rt0_go
in love and hate, especially like runtime.main
, runtime.schedinit
and other scheduling method, there is a very large value of learning, there is little interest partners can continue to focus.
Go scheduler initialization
After knowing how the Go program is booted, we need to understand how the scheduler flows in the Go Runtime.
runtime.mstart
Here we mainly focus on the runtime.mstart
method:
func mstart() {
// 获取 g0
_g_ := getg()
// 确定栈边界
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
// 启动 m,进行调度器循环调度
mstart1()
// 退出线程
if mStackIsSystemAllocated() {
osStack = true
}
mexit(osStack)
}
- Call the
getg
method to obtain g in the GMP model, where g0 is obtained. - By checking the boundary of the execution stack
_g_.stack
of g (the boundary of the stack is exactly lo, hi) to determine whether it is a system stack. If it is, the stack boundary is initialized according to the system stack. - Call the
mstart1
method to start the system thread m, and perform the scheduler cyclic scheduling. - Call
mexit
method to exit the system thread m.
runtime.mstart1
It seems that its essential logic mstart1
in the 060cc2e20e9951 method, we continue to analyze it:
func mstart1() {
// 获取 g,并判断是否为 g0
_g_ := getg()
if _g_ != _g_.m.g0 {
throw("bad runtime·mstart")
}
// 初始化 m 并记录调用方 pc、sp
save(getcallerpc(), getcallersp())
asminit()
minit()
// 设置信号 handler
if _g_.m == &m0 {
mstartm0()
}
// 运行启动函数
if fn := _g_.m.mstartfn; fn != nil {
fn()
}
if _g_.m != &m0 {
acquirep(_g_.m.nextp.ptr())
_g_.m.nextp = 0
}
schedule()
}
- Call
getg
method to get g. And judge whether the obtained g is g0 through the previously bound_g_.m.g0
If not, a fatal error will be thrown directly. Because the scheduler only runs on g0. - Call the
minit
method to initialize m, and record the caller's PC and SP to facilitate reuse in the subsequent schedule stage. - If it is determined that the m bound to the current g is m0, call the
mstartm0
method to set the signal handler. This action must be after theminit
method, so that theminit
method can prepare the thread in advance so that it can process the signal. - If the m bound to the current g has a start function, it will run. Otherwise skip.
- If the m currently bound to g is not m0, you need to call the
acquirep
method to get and bind p, that is, m is bound to p. - Call the
schedule
method for formal scheduling.
After a lot of busy work, I finally entered the main course of the topic. The original schedule
method, which was very latent, is the real scheduling method. The rest are pre-processing and data preparation.
Due to space issues, the schedule
method will be put into the next article and then continue to analyze, let's focus on some details of this article first.
In-depth analysis of the problem
However, the space here has been longer, and many problems have been accumulated. We analyze the two elements with the highest appearance rate in Runtime:
m0
what does it do?g0
what is its function?
m0
m0 is the first system thread created by Go Runtime. A Go process has only one m0, also called the main thread.
From multiple perspectives:
- Data structure: There is no difference between m0 and other created m.
- Creation process: m0 is the process that should be assembled and copied directly to m0 when the process is started, and the other subsequent m are all created by themselves in Go Runtime.
- Variable declaration: m0 is the same as regular m, the definition of m0 is
var m0 m
, nothing special.
g0
g is generally divided into three types, namely:
- The one that performs user tasks is called g.
- Execute the main goroutine of
runtime.main
- The one performing the scheduling task is called g0. .
g0 is special, each m has only one g0 (only one g0), and each m is only bound to one g0. The assignment of g0 is also assigned through assembly, and the rest of the subsequent creations are all conventional g.
From multiple perspectives:
- Data structure: g0 and other created g are the same in data structure, but there is a stack difference. The stack on g0 is allocated to the system stack. On Linux, the stack size is fixed at 8MB by default and cannot be expanded or contracted. The conventional g starts with only 2KB, which can be expanded.
- Running status: g0 is different from regular g. There are not so many running statuses, and it will not be preempted by the scheduler. The scheduling itself runs on g0.
- Variable declaration: g0 and regular g, the definition of g0 is
var g0 g
, nothing special.
summary
In this chapter, we explain a process of Go scheduler initialization, which involves:
- runtime.mstart。
- runtime.mstart1。
Based on this, I also learned what needs to be prepared and what to initialize during the scheduler initialization process. In addition, we sort out and explain the concepts of m0 and g0 that are most frequently mentioned in the scheduling process.
to sum up
In today's article, we introduced in detail all the processes and initialization actions in the boot process of the Go language.
At the same time, a preliminary analysis is made for the initialization of the scheduler, and the purpose and difference of m0 and g0 are introduced in detail.
In the next article, we will further schedule
, which is also a hard bone.
If you have any questions please comment and feedback exchange area, best relationship is mutual achievement , everybody thumbs is fried fish maximum power of creation, thanks for the support.
Article continuously updated, can be found [micro letter head into the fried fish] read, reply [ 000 ] have an interview algorithm solution to a problem-tier manufacturers and materials prepared for me; this article GitHub github.com/eddycjy/blog been included , Welcome Star to urge you to update.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。