go func...会被编译成newproc方法,我们可以随便写一个测试文件编译后用gdb打开
(gdb) b runtime.newproc
Breakpoint 5 at 0x10303c0: file /usr/local/homebrew/Cellar/go@1.9/1.9.6/libexec/src/runtime/proc.go, line 2929.
func newproc(siz int32, fn *funcval) {
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
pc := getcallerpc(unsafe.Pointer(&siz))
systemstack(func() {
newproc1(fn, (*uint8)(argp), siz, 0, pc)
})
}
type funcval struct {
fn uintptr
// variable-size, fn-specific data here
}
golang的参数调用方式和C差不多,都是按参数从右到左入栈,所以siz是第一个参数,fn是变长参数,它的长度由siz确定,所以从我们的代码 go func...最终会被编译器call newproc,并且会把参数拷贝到调用栈上,fn中包含了所有g执行的上下文(方法指针[IP]和方法参数)
还一个有意思的是取调用方的pc,汇编中call调用后cpu会把下一条地址也就是方法的返回地址压入栈中。
systemstack是让M使用g0堆栈,我们已经知道M执行的时候会从G中恢复出堆栈执行,但做一些系统任务比如这儿的新建proc的时候如果再用G的堆栈就不合适了,所以golang设计的时候每个M都分配一个g0堆栈,这一点我觉得应该是模仿操作系统的,cpu有4个等级,linux使用0和3,用户态在3级,当系统调用陷入内核的时候操作系统会切换到0级并且把堆栈都切换到内核堆栈。
继续newproc1
// 返回值忽略了,因为goroutine从不返回
func newproc1(fn *funcval, argp *uint8, narg int32, nret int32, callerpc uintptr) *g {
_g_ := getg()
if fn == nil {
_g_.m.throwing = -1 // do not dump full stacks
throw("go of nil func value")
}
_g_.m.locks++ // disable preemption because it can be holding p in a local var
// 对其内存
siz := narg + nret
siz = (siz + 7) &^ 7
// We could allocate a larger initial stack if necessary.
// Not worth it: this is almost always an error.
// 4*sizeof(uintreg): extra space added below
// sizeof(uintreg): caller's LR (arm) or return address (x86, in gostartcall).
if siz >= _StackMin-4*sys.RegSize-sys.RegSize {
throw("newproc: function arguments too large for new goroutine")
}
// 从P获取一个G如果没有就new一个
_p_ := _g_.m.p.ptr()
newg := gfget(_p_)
if newg == nil {
newg = malg(_StackMin)
casgstatus(newg, _Gidle, _Gdead)
allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
}
if newg.stack.hi == 0 {
throw("newproc1: newg missing stack")
}
if readgstatus(newg) != _Gdead {
throw("newproc1: new g is not Gdead")
}
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
if usesLR {
// caller's LR
*(*uintptr)(unsafe.Pointer(sp)) = 0
prepGoExitFrame(sp)
spArg += sys.MinFrameSize
}
if narg > 0 {
// 把参数copy到G的栈中
memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg))
// This is a stack-to-stack copy. If write barriers
// are enabled and the source stack is grey (the
// destination is always black), then perform a
// barrier copy. We do this *after* the memmove
// because the destination stack may have garbage on
// it.
if writeBarrier.needed && !_g_.m.curg.gcscandone {
f := findfunc(fn.fn)
stkmap := (*stackmap)(funcdata(f, _FUNCDATA_ArgsPointerMaps))
// We're in the prologue, so it's always stack map index 0.
bv := stackmapdata(stkmap, 0)
bulkBarrierBitmap(spArg, spArg, uintptr(narg), 0, bv.bytedata)
}
}
memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
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))
// 该方法会把pc换成fn.fn也就是方法执行的地址,而把老pc放入lr,但我不太懂为什么要这么写
gostartcallfn(&newg.sched, fn)
newg.gopc = callerpc
newg.startpc = fn.fn
if _g_.m.curg != nil {
newg.labels = _g_.m.curg.labels
}
if isSystemGoroutine(newg) {
atomic.Xadd(&sched.ngsys, +1)
}
newg.gcscanvalid = false
casgstatus(newg, _Gdead, _Grunnable)
if _p_.goidcache == _p_.goidcacheend {
// Sched.goidgen is the last allocated id,
// this batch must be [sched.goidgen+1, sched.goidgen+GoidCacheBatch].
// At startup sched.goidgen=0, so main goroutine receives goid=1.
_p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch)
_p_.goidcache -= _GoidCacheBatch - 1
_p_.goidcacheend = _p_.goidcache + _GoidCacheBatch
}
newg.goid = int64(_p_.goidcache)
_p_.goidcache++
if raceenabled {
newg.racectx = racegostart(callerpc)
}
if trace.enabled {
traceGoCreate(newg, newg.startpc)
}
// 好了G的创建完成了,放入到P的run队列中
runqput(_p_, newg, true)
// 如果有空闲P就尝试唤醒一个M
if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {
wakep()
}
_g_.m.locks--
if _g_.m.locks == 0 && _g_.preempt { // restore the preemption request in case we've cleared it in newstack
_g_.stackguard0 = stackPreempt
}
return newg
}
// 这就是上面说的把pc替换成真正的fn.fn的地方,不懂为什么要这么写
func gostartcallfn(gobuf *gobuf, fv *funcval) {
var fn unsafe.Pointer
if fv != nil {
fn = unsafe.Pointer(fv.fn)
} else {
fn = unsafe.Pointer(funcPC(nilfunc))
}
gostartcall(gobuf, fn, unsafe.Pointer(fv))
}
func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) {
if buf.lr != 0 {
throw("invalid use of gostartcall")
}
buf.lr = buf.pc
buf.pc = uintptr(fn)
buf.ctxt = ctxt
}
总结下go在go func中干了啥
- 编译器会转成newproc
- newproc中从调用栈中取出执行方法地址,调用参数,调用者pc
- 然后从P中取一个空闲G没有就创建一个newG
- 对newG赋值,主要是赋值G的sched,它保存了G的调用上下文,有方法地址,参数,栈地址,下次M从P中取到这个G的时候运行就会从上下文中恢复到寄存器中从而完成用户态进程的切换
- 最后结束的时候会尝试唤醒M
有疑问加站长微信联系(非本文作者)