layout | title | subtitle | date | categories | cover | tags |
---|---|---|---|---|---|---|
post |
Go程序启动源码分析 |
从第一行源码开始阅读runtime,也包含一些pmg调度知识 |
2019-08-07 |
技术 |
Golang |
测试环境:
$ go version
go version go1.12.7 linux/amd64
$ uname -a
Linux wu-insparition 4.18.0-25-generic #26~18.04.1-Ubuntu SMP Thu Jun 27 07:28:31 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
测试范例:
package main
import "fmt"
func main() {
fmt.Println("hello,world")
}
程序的入口处为:
// usr/local/go/src/runtime/rt0_linux_amd64.s
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)
将断点打在_rt0_amd64
处:
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
JMP runtime·rt0_go(SB)
由于我们将断点打在用户空间的入口处,在这之前在内核中涉及到进程的do_fork()
,这里的argc和argv应该与这个函数的参数有关,由于不影响整个流程,这里分析暂先跳过。下面执行到rt0_go
:
TEXT runtime·rt0_go(SB),NOSPLIT,$0
MOVQ DI, AX // argc
MOVQ SI, BX // argv
SUBQ $(4*8+7), SP // 2args 2auto
ANDQ $~15, SP
MOVQ AX, 16(SP)
MOVQ BX, 24(SP)
这里做了一下参数的拷贝,栈的变化大致如下:
+---------+
| |
| |
+---------+ <--+ 0x00007fffdafd5120
| |
| |
+---------+ <--+ 0x00007fffdafd50f9
| |
| |
+---------+ <--+ 0x00007fffdafd50f0 sp
| |
| ... |
| |
+---------+
MOVQ $runtime·g0(SB), DI // 这里将全局的g0放进DI
LEAQ (-64*1024+104)(SP), BX
MOVQ BX, g_stackguard0(DI)
MOVQ BX, g_stackguard1(DI) // 设置g0的stackguard0和stackguard1
MOVQ BX, (g_stack+stack_lo)(DI)
MOVQ SP, (g_stack+stack_hi)(DI) // 设置g0的stack.hi和stack.lo
可以看出g0的栈大约为64K(system stack)。省略一些读取CPU与cgo的代码,接着看:
LEAQ runtime·m0+m_tls(SB), DI // 将m0.tls的地址放进DI
CALL runtime·settls(SB)
TEXT runtime·settls(SB),NOSPLIT,$32
...
ADDQ $8, DI // ELF wants to use -8(FS),这里暂时没明白为什么会这样设计TODO
...
MOVQ DI, SI // 将m0.tls的地址作为系统调用的参数之一
MOVQ $0x1002, DI // ARCH_SET_FS
MOVQ $SYS_arch_prctl, AX // 存入系统调用号
SYSCALL
CMPQ AX, $0xfffffffffffff001 // 是否成功
JLS 2(PC) // 如果成功直接调到RET处返回
MOVL $0xf1, 0xf1 // crash
RET
这里涉及到TLS的相关知识。总的来说做了两件事:(1)告诉kernel,用户空间以后会用FS这个段寄存器来访问TLS段(glibc使用了GS段寄存器)。(2)告诉kernel,当前线程的TSL是m0.tls。
get_tls(BX)
MOVQ $0x123, g(BX)
MOVQ runtime·m0+m_tls(SB), AX
CMPQ AX, $0x123
JEQ 2(PC)
CALL runtime·abort(SB)
这段代码是为了测试m0的TLS是否设置成功。通过查看m0.tls[0]处的值,发现0x123确实以十进制数291的形式存入了。这里令我疑惑的是代码中的一个宏:
#define g(r) 0(r)(TLS*1)
从表面上,没看出这个宏的作用。通过向get_tls(BX)
后添加一句MOVQ g(BX), AX
,查看rax寄存器中的值居然是0。但是不影响进行,有空再深究TODO。
ok:
// set the per-goroutine and per-mach "registers"
get_tls(BX)
LEAQ runtime·g0(SB), CX
MOVQ CX, g(BX) // 将g0的地址放进当前线程(m0)的TLS
LEAQ runtime·m0(SB), AX
// save m->g0 = g0
MOVQ CX, m_g0(AX)
// save m0 to g0->m
MOVQ AX, g_m(CX)
CLD // convention is D is always left cleared
CALL runtime·check(SB)
(dlv) p m0.tls
[6]uintptr [5746784,0,0,0,0,0]
(dlv) p &g0
(*runtime.g)(0x57b060)
可以看到g0的地址以十进制的形式存入了m0.tls。
MOVL 16(SP), AX // copy argc
MOVL AX, 0(SP)
MOVQ 24(SP), AX // copy argv
MOVQ AX, 8(SP)
这里拷贝参数到esp的正上方(注意中间有ret addr),这两个参数分别是下面3个初始化函数的参数:
CALL runtime·args(SB)
CALL runtime·osinit(SB) // 获取core个数,本机上为4
CALL runtime·schedinit(SB)
func schedinit() {
// 这是一个特殊的函数,它的函数体由编译器填写
// 这个函数的返回值有3种可能:
// 1. m.g0
// 2. m.gsinnal
// 3. m.curg
// 更多见HACKING.md
_g_ := getg() // 这里返回的是当前m的g0,即全局的g0
if raceenabled {
_g_.racectx, raceprocctx0 = raceinit()
}
sched.maxmcount = 10000 // 最大m数量
tracebackinit() // skipPC=4546432
moduledataverify() // symbol table
stackinit() // 初始化栈池
mallocinit() // 内存初始化,留着看allocator的时候再来分析TODO
// 初始化m的一些内容,包括m的gsignal(栈大小为32KB)等
// 在给gsignal分配栈的时候,走的是allocManual() osStackAlloc(s)
mcommoninit(_g_.m)
cpuinit() // must run before alginit
alginit() // maps must not be used before this call
modulesinit() // provides activeModules
typelinksinit() // uses maps, activeModules
itabsinit() // uses activeModules
msigsave(_g_.m)
initSigmask = _g_.m.sigmask
// 这里将g0栈上的两个fork参数拷贝到此程序的全局变量中
goargs()
goenvs()
parsedebugvars()
gcinit() // gc相关的初始化TODO
sched.lastpoll = uint64(nanotime())
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
// 初始化procs个p,并将当前m与第一个p建立联系
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}
// For cgocheck > 1, we turn on the write barrier at all times
// and check all pointer writes. We can't do this until after
// procresize because the write barrier needs a P.
if debug.cgocheck > 1 {
writeBarrier.cgo = true
writeBarrier.enabled = true
for _, p := range allp {
p.wbBuf.reset()
}
}
if buildVersion == "" {
// Condition should never trigger. This code just serves
// to ensure runtime·buildVersion is kept in the resulting binary.
buildVersion = "unknown"
}
}
// 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
注意这里mainPC是:
DATA runtime·mainPC+0(SB)/8,$runtime·main(SB)
下面进入newproc函数:
func newproc(siz int32, fn *funcval) {
argp := add(unsafe.Pointer(&fn), sys.PtrSize) // argp指向第一个参数,下面做了一个小验证
// 前面讲过,这里从当前线程的tls中取得当前正在运行的g
// 有3中可能,这里是gp=g0
gp := getg() // 获得当前的g
// getcallerpc returns the program counter (PC) of its caller's caller. 见stubs.go
// 参考后面的一个小验证范例
pc := getcallerpc()
systemstack(func() {
newproc1(fn, (*uint8)(argp), siz, gp, pc)
})
}
验证代码:
package main
import "fmt"
import "time"
func main() {
i := 69
j := 100
go func(arg1, arg2 int){
fmt.Println(arg1+arg2)
}(i, j)
time.Sleep(1*time.Second)
}
这里将程序build后,使用objdump -d
查看main()
和newproc()
的汇编代码,画出函数的栈帧大致如下:
newproc main
+--------+ +--------+
| rbp | | ... |
+--------+ +--------+ <-+
| ret | | rbp |
+--------+ +--------+ +-+
| TLS | | 69 | |
+--------+ +--------+ | local var
| 16 | | 100 | |
+--------+ +--------+ +-+
argp | |----------+ | 100 | |
+--------| | +--------+ | argument
| |-------+ +---> | 69 | |
+--------+ | +--------+ +-+
| func1 | +------> | fun_ |
+--------+ +--------+
| func1 | | 16 |
+--------+ <-+RSP +--------+
| ret |
+--------+
通过栈帧结构也能看出,在newproc的栈帧中,argp指向了main函数中第一个参数的位置。值得注意的是,newproc中拷贝了上一个栈帧的ret addr到自己的本地变量中,这正是执行pc := getcallerpc()
的效果(caller's caller)。
验证完这句代码,接下来来到systemstack
:
// func systemstack(fn func())
TEXT runtime·systemstack(SB), NOSPLIT, $0-8
// mov 0x8(%rsp),%rdi 结合上面的栈帧结构可以知道,这里的fn指的是传进来的匿名函数
MOVQ fn+0(FP), DI // DI = fn
get_tls(CX)
MOVQ g(CX), AX // AX = g mov %fs:0xfffffffffffffff8,%rax
MOVQ g_m(AX), BX // BX = m
CMPQ AX, m_gsignal(BX) // 判断当前执行的g是否是gsignal
JEQ noswitch
MOVQ m_g0(BX), DX // DX = g0
CMPQ AX, DX // 判断当前执行的g是否是g0,成立,跳转到noswitch,
JEQ noswitch
...
noswitch:
MOVQ DI, DX
MOVQ 0(DI), DI
// 跳转到匿名函数
// 调到注意这里使用的是JMP指令,而不是CALL指令
// 二者的区别在于前者不会压入ret addr,后者会
JMP DI
// 这里再回忆一下这5个参数的作用:
// fn: 从asm_amd64.s中传入,为proc.go中的main函数
// argp: 参数指针,这里没有参数
// narg: 参数总大小,这里为0
// callerpg: 有3种可能,这里是g0
// callerpc: 在asm_amd64.s中执行newproc的前一条指令的地址
// 下面的分析中,只摘取了主线内容
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
_g_ := getg() // go
...
// 这里与内核中禁止抢占的方式有点类似,有待考究TODO
_g_.m.locks++ // disable preemption because it can be holding p in a local var
...
_p_ := _g_.m.p.ptr() // 拿到当前m的p(id为0),它们的关系在前面的procresize()函数中已经建立
newg := gfget(_p_) // 因为目前只有一个g0,这里newg为空
if newg == nil {
newg = malg(_StackMin)
casgstatus(newg, _Gidle, _Gdead) // 改变g的状态
// 将新分配的newg加入allgs数组中
allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
}
注意gfget()
函数,优先从本地队列(p.gFree)中复用(reuse),如果本地队列没有了,就从全局队列(sched.gFree)中取一些(优先取有栈的g)放到本地队列中。注意这里别和下面会讲到的schedule()函数里获取g的方式弄混了。
func malg(stacksize int32) *g {
newg := new(g) // 分配一个g,new会进入runtime调用newObject()
if stacksize >= 0 {
stacksize = round2(_StackSystem + stacksize) // 2048
systemstack(func() { // 依然是切换到g0的栈上操作
newg.stack = stackalloc(uint32(stacksize))
})
// 这两句就很明显了
newg.stackguard0 = newg.stack.lo + _StackGuard
newg.stackguard1 = ^uintptr(0) // stackguard1的值为什么要设置成-1?TODO
}
return newg
}
下面在g0栈上执行stackalloc()函数:
// stackalloc allocates an n byte stack.
//
// stackalloc must run on the system stack because it uses per-P
// resources and must not split the stack.
//
//同上,此函数只摘取主线内容分析
//go:systemstack
func stackalloc(n uint32) stack {
// Stackalloc must be called on scheduler stack, so that we
// never try to grow the stack during the code that stackalloc runs.
// Doing so would cause a deadlock (issue 1547).
// 上面注释也说了,为了避免grow stack,选择在g0的栈上进行函数调用
// 在asm_amd64.s中也提到过,g0的栈大约是64KB,是完全够用的
thisg := getg()
if thisg != thisg.m.g0 {
// 在runtime中,g0的栈有两个别名,system stack和scheduler stack
throw("stackalloc not on scheduler stack")
}
...
// Small stacks are allocated with a fixed-size free-list allocator.
// If we need a stack of a bigger size, we fall back on allocating
// a dedicated span.
var v unsafe.Pointer
if n < _FixedStack<<_NumStackOrders && n < _StackCacheSize {
order := uint8(0)
n2 := n
for n2 > _FixedStack { // n2=_FixedStack,略过循环
order++
n2 >>= 1
}
var x gclinkptr
c := thisg.m.mcache
if stackNoCache != 0 || c == nil || thisg.m.preemptoff != "" {
...
} else {
x = c.stackcache[order].list
if x.ptr() == nil {
// m.mcache.stackcache为空,需要进这个函数填充
// 这里函数会调用stackpoolalloc() allocManual()
// 最后使用osStackAlloc()->mmap()完成映射TODO
stackcacherefill(c, order) // 这里没太看懂order的作用TODO
x = c.stackcache[order].list
}
c.stackcache[order].list = x.ptr().next
c.stackcache[order].size -= uintptr(n)
}
v = unsafe.Pointer(x)
} else {
...
return stack{uintptr(v), uintptr(v) + uintptr(n)}
}
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
...
// totalSize = 4*8 + 0 + 0
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
// 此时newg的栈大致如下:
/*+-------+ <-+
| 32B |
+-------+ <-+ sp/spArg
| ... |
| ... |
| ... |
+-------+
*/
...
// 这里本来有一段mmmove()拷贝参数的操作
// 由于这个goroutine创建时没有参数,所以省略了
// memclr_amd64.s:14 TODO
memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
// 下面就是对newg的初始化
newg.sched.sp = sp
newg.stktopsp = sp
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)
newg.gopc = callerpc
newg.ancestors = saveAncestors(callergp)
newg.startpc = fn.fn
if _g_.m.curg != nil {
newg.labels = _g_.m.curg.labels
}
if isSystemGoroutine(newg, false) {
atomic.Xadd(&sched.ngsys, +1)
}
newg.gcscanvalid = false
// 执行完这条语句后,在delve中发现有1个goroutine在运行,因为g的状态变成了_Grunnable
// 后面可以看到在execute()函数里面由_Grunnable变成_Grunning
casgstatus(newg, _Gdead, _Grunnable)
...
// 将newg放入runnable queue,按照p.runq -> p.runnext -> global queue的顺序尝试
runqput(_p_, newg, true)
...
}
这里注意gostartcallfn()
这个函数。这个函数会调用gostartcall()
:
func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) {
sp := buf.sp
if sys.RegSize > sys.PtrSize {
sp -= sys.PtrSize
*(*uintptr)(unsafe.Pointer(sp)) = 0
}
sp -= sys.PtrSize // 预留一个指针大小的位置,存放ret addr
*(*uintptr)(unsafe.Pointer(sp)) = buf.pc // 在ret addr中存入goexit()函数的地址
buf.sp = sp
buf.pc = uintptr(fn) // 将runtime.main设置为goroutine的任务函数
buf.ctxt = ctxt // TODO
}
下面对比描述了执行gostartcall()
前后goroutine栈桢结构的变化:
+-------+ +-------+
| 32B | | 32B |
+-------+ +-------+
| ... | | ... |
| arg | | arg |
+-------+ <-+ rsp +-------+
| | | goexit| ret addr
| | +-------+ <-+ rsp
| | | |
| | | |
+-------+ +-------+
很明显,这是在构造一个栈桢结构。为了让goroutine上的任务函数执行完后,执行return,将ret addr处的goexit地址放入rip寄存器。在后面会看到执行goroutine任务函数的时候,使用的是JMP指令,而不是CALL指令。这就避免了将不正确的ret addr压栈。
至此,已经完成了newg相关的操作,这个goroutine将执行runtime.main
。接下来回到asm_amd64.s:206
开始执行mstart()
函数:
func mstart() {
_g_ := getg() // 这里_g_为g0
...
// 这里有点奇怪,通过单步调试,执行_g_ := getg(),得到的_g_是g0
// 而当执行到下面这句的时候,通过delve查看_g_就发生了变化
// 相当费解TODO
_g_.stackguard0 = _g_.stack.lo + _StackGuard
_g_.stackguard1 = _g_.stackguard0
mstart1()
...
}
func mstart1() {
_g_ := getg() // 得到g0
if _g_ != _g_.m.g0 {
throw("bad runtime·mstart") // 说明mstart应该在m的systemstack/scheduler stack上执行
}
// Record the caller for use as the top of stack in mcall and
// for terminating the thread.
// We're never coming back to mstart1 after we call schedule,
// so other calls can reuse the current frame.
// TODO
save(getcallerpc(), getcallersp())
asminit() // 这是个空函数
minit()
// Install signal handlers; after minit so that minit can
// prepare the thread to be able to handle the signals.
if _g_.m == &m0 {
mstartm0()
}
// 此时m0的fn为空,注意区别m.mstartfn与前面在newg.startpc中绑定的fn
if fn := _g_.m.mstartfn; fn != nil {
fn()
}
if _g_.m != &m0 {
acquirep(_g_.m.nextp.ptr()) // m执行前需要绑定一个p
_g_.m.nextp = 0
}
schedule() // 进入协程调度,参考xv6内核调度
}
// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.
func schedule() {
_g_ := getg() // _g_为g0
...
top:
...
var gp *g
var inheritTime bool
...
if gp == nil {
// Check the global runnable queue once in a while to ensure fairness.
// Otherwise two goroutines can completely occupy the local runqueue
// by constantly respawning each other.
//
// 每隔61次调度就去sched.runq中照顾一次全局goroutine,以保证公平性,防止starvation
// 至于为什么会选择61这个数,还挺有讲究,这里就不展开说了,更多可以去参考Dmitry Vyukov的liveblog
if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp = globrunqget(_g_.m.p.ptr(), 1)
unlock(&sched.lock)
}
}
if gp == nil {
// 从本地队列中得到了前面放到p中的newg
// 回忆前面,newg是在newproc1()中放到p中的
// inheritTime返回为true,说明应该将g0的剩余时间片给gp
gp, inheritTime = runqget(_g_.m.p.ptr())
if gp != nil && _g_.m.spinning {
throw("schedule: spinning with local work")
}
}
if gp == nil {
// 这个函数很复杂,Tries to steal from other P's, get g from global queue, poll network.
// TODO
gp, inheritTime = findrunnable() // blocks until work is available
}
...
// 执行到这里,离目标越来越近了 :)
execute(gp, inheritTime)
}
从schedule()
函数里可以看到获取g的三种途径:
- globrunqget()
- runqget()
- findrunnable()
回忆前面的gfget()
函数,注意二者获取g的区别。
// Schedules gp to run on the current M.
// If inheritTime is true, gp inherits the remaining time in the
// current time slice. Otherwise, it starts a new time slice.
// Never returns.
//
// Write barriers are allowed because this is called immediately after
// acquiring a P in several places.
//
//go:yeswritebarrierrec
func execute(gp *g, inheritTime bool) {
// 当前的g依旧是g0,整个newproc1函数都是在g0的栈上执行的
// 为什么许多函数执行前都要执行一次?TODO
_g_ := getg()
casgstatus(gp, _Grunnable, _Grunning) // 改变g的状态
gp.waitsince = 0
gp.preempt = false
gp.stackguard0 = gp.stack.lo + _StackGuard
if !inheritTime {
_g_.m.p.ptr().schedtick++
}
/* 执行前栈帧
(dlv) bt
0 0x000000000043258e in runtime.execute
at /usr/local/go/src/runtime/proc.go:2155
1 0x00000000004335ad in runtime.schedule
at /usr/local/go/src/runtime/proc.go:2559
2 0x000000000043061e in runtime.mstart1
at /usr/local/go/src/runtime/proc.go:1213
3 0x0000000000430564 in runtime.mstart
at /usr/local/go/src/runtime/proc.go:1156
4 0x00000000004569e0 in runtime.rt0_go
at /usr/local/go/src/runtime/asm_amd64.s:206
*/
_g_.m.curg = gp // 把当前m.curg指明为我们要调度的goroutine
/* 执行后栈帧
(dlv) bt
0 0x00000000004325ad in runtime.execute
at /usr/local/go/src/runtime/proc.go:2156
1 0x000000000042dbf0 in runtime.main
at /usr/local/go/src/runtime/proc.go:110
2 0x0000000000458a21 in runtime.goexit
at /usr/local/go/src/runtime/asm_amd64.s:1337
*/
gp.m = _g_.m // 指明要调度的goroutine归属于当前m
...
// 注意这里传入的参数,是在newproc1()中设置的
gogo(&gp.sched)
}
// func gogo(buf *gobuf)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $16-8
MOVQ buf+0(FP), BX // gobuf
MOVQ gobuf_g(BX), DX // 这是我们要执行的goroutine
MOVQ 0(DX), CX // make sure g != nil
get_tls(CX)
MOVQ DX, g(CX) // 切换,把将要执行的goroutine放到m的tls中
/*
(dlv) bt
0 0x0000000000456a23 in runtime.gogo
at /usr/local/go/src/runtime/asm_amd64.s:259
1 0x0000000000432600 in runtime.execute
at /usr/local/go/src/runtime/proc.go:2173
2 0x00000000004335ad in runtime.schedule
at /usr/local/go/src/runtime/proc.go:2559
3 0x000000000043061e in runtime.mstart1
at /usr/local/go/src/runtime/proc.go:1213
4 0x0000000000430564 in runtime.mstart
at /usr/local/go/src/runtime/proc.go:1156
5 0x00000000004569e0 in runtime.rt0_go
at /usr/local/go/src/runtime/asm_amd64.s:206
*/
MOVQ gobuf_sp(BX), SP // restore SP,切换到将要执行的goroutine的栈上去
/* 执行前栈帧,
0 0x0000000000456a26 in runtime.gogo
at /usr/local/go/src/runtime/asm_amd64.s:260
*/
MOVQ gobuf_ret(BX), AX
MOVQ gobuf_ctxt(BX), DX
MOVQ gobuf_bp(BX), BP
MOVQ $0, gobuf_sp(BX) // clear to help garbage collector
MOVQ $0, gobuf_ret(BX)
MOVQ $0, gobuf_ctxt(BX)
MOVQ $0, gobuf_bp(BX)
MOVQ gobuf_pc(BX), BX
/* 执行前栈帧
0 0x0000000000456a55 in runtime.gogo
at /usr/local/go/src/runtime/asm_amd64.s:268
*/
JMP BX // 跳转到main goroutine
为什么要用JMP BX
,而不是CALL指令?原因前面在gostartcallfn()
提到过。
真不容易啊,就要大功告成了~
/* 执行前栈帧
0 0x000000000042dc11 in runtime.main
at /usr/local/go/src/runtime/proc.go:111
1 0x0000000000458a21 in runtime.goexit
at /usr/local/go/src/runtime/asm_amd64.s:1337
*/
// The main goroutine.
func main() {
...
if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
systemstack(func() {
newm(sysmon, nil) // 创建一个监控线程执行sysmon()函数
})
}
fn = main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn() // 执行我们编写的main函数
...
}
整个过程跑下来,发现函数启动流程远比我想象中的复杂。文中许多TODO标识,是当下我还没弄懂的一些地方。随着对runtime包的阅读,会持续的回过头来解决这些疑惑。
over! :)