接上一篇继续分析一下runtime.newproc方法。
函数签名
newproc函数的签名为 newproc(siz int32, fn *funcval)
siz是传入的参数大小(不是个数);fn对应的是函数,但并不是函数指针,funcval.fn才是真正指向函数代码的指针。
// go/src/runtime/runtime2.gotype funcval struct {fn uintptr // 真正指向函数代码的指针}
关键字go
在golang中编译器会把类似 go foo() 编译成调用 runtime.newproc 方法。
准备一段代码:
package mainimport ("fmt""time")func main() {go printAdd(3, 7)time.Sleep(time.Second)}func printAdd(a, b int) {fmt.Println(a + b)}
开始调试:
关于golang栈结构的分析可以参考 Golang源码学习:使用gdb调试探究Golang函数调用栈结构
root@xiamin:~/study# dlv debug test.goType 'help' for list of commands.(dlv) b main.mainBreakpoint 1 set at 0x4ada0f for main.main() ./test.go:8(dlv) c> main.main() ./test.go:8 (hits goroutine(1):1 total:1) (PC: 0x4ada0f) 3:import ( 4:"fmt" 5:"time" 6:) 7:=> 8:func main() { 9:go printAdd(3, 7) 10:time.Sleep(time.Second) 11:} 12: 13:func printAdd(a, b int) {// 这里执行几次si,得到下面。(dlv) disassTEXT main.main(SB) /root/study/test.gotest.go:80x4ada0064488b0c25f8ffffffmov rcx, qword ptr fs:[0xfffffff8]test.go:80x4ada09483b6110cmp rsp, qword ptr [rcx+0x10]test.go:80x4ada0d764fjbe 0x4ada5etest.go:80x4ada0f*4883ec28sub rsp, 0x28test.go:80x4ada1348896c2420mov qword ptr [rsp+0x20], rbptest.go:80x4ada18488d6c2420lea rbp, ptr [rsp+0x20] // 在main的栈帧中设置newproc的参数siz,16字节test.go:90x4ada1dc7042410000000mov dword ptr [rsp], 0x10 // 计算printAdd函数对应的funcval结构体的地址放入raxtest.go:90x4ada24488d057d5e0300lea rax, ptr [rip+0x35e7d] // 在main的栈帧中设置newproc的参数fntest.go:90x4ada2b4889442408mov qword ptr [rsp+0x8], rax // printAdd的参数atest.go:90x4ada3048c744241003000000mov qword ptr [rsp+0x10], 0x3 // printAdd的参数btest.go:90x4ada3948c744241807000000mov qword ptr [rsp+0x18], 0x7 // 调用 runtime.newproc=>test.go:90x4ada42e80902f9ffcall $runtime.newproctest.go:100x4ada4748c7042400ca9a3bmov qword ptr [rsp], 0x3b9aca00test.go:100x4ada4fe86c4afaffcall $time.Sleeptest.go:110x4ada54488b6c2420mov rbp, qword ptr [rsp+0x20]test.go:110x4ada594883c428add rsp, 0x28test.go:110x4ada5dc3rettest.go:80x4ada5ee88d47fbffcall $runtime.morestack_noctxt<autogenerated>:10x4ada63eb9bjmp $main.main
我们来验证一下fn参数:
(dlv) regs ...... Rax = 0x00000000004e38a8// 存储的是 printAdd 对应的 runtime.funcval 地址。 ......(dlv) p *(*runtime.funcval)(0x00000000004e38a8)runtime.funcval {fn: 4905584}// 4905584是十进制,转换成十六进制是 0x4ada70。(dlv) p &printAdd(*)(0x4ada70)// 函数指针与上面的 funcval.fn 相符。
此段仅用来分析go关键字的实现。与下面的 main goroutine无直接关联。
main goroutine的创建
以下注释的场景均为初始化时。
runtime·rt0_go 中调用 runtime.newproc 相关代码:
TEXT runtime·rt0_go(SB),NOSPLIT,$0 ...... // 调用runtime·newproc创建goroutine,指向函数为runtime·mainMOVQ$runtime·mainPC(SB), AX// runtime·mainPC就是runtime·mainPUSHQAX// newproc的第二个参数fn,也就是goroutine要执行的函数。PUSHQ$0// newproc的第一个参数siz,表示要传入runtime·main中参数的大小,此处为0。// 创建 main goroutine。非main goroutine也是此方法创建。CALLruntime·newproc(SB)POPQAXPOPQAX ......DATAruntime·mainPC+0(SB)/8,$runtime·main(SB)GLOBLruntime·mainPC(SB),RODATA,$8
runtime.newproc
func newproc(siz int32, fn *funcval) { // 获取fn函数的参数起始地址,可参考上例中的printAdd,sys.PtrSize的值是8。argp := add(unsafe.Pointer(&fn), sys.PtrSize) // 获取一个g(m0.g0)gp := getg() // 调用者的pc,也就是执行完此函数返回调用者时的下一条指令地址,本例中是 POPQ AXpc := getcallerpc()systemstack(func() {newproc1(fn, argp, siz, gp, pc)})}
runtime.newproc1
func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) {_g_ := getg()// 当前g。g0 ......acquirem() // 禁止抢占siz := nargsiz = (siz + 7) &^ 7// 使siz为8的整数倍。&^为双目运算符,将运算符左边数据相异的保留,相同位清零。 ......_p_ := _g_.m.p.ptr()// 当前关联的p。allp[0]newg := gfget(_p_)// 获取一个g,下有分析。if newg == nil {newg = malg(_StackMin)// 分配一个新gcasgstatus(newg, _Gidle, _Gdead)// 更改状态allgadd(newg)// 加入到allgs切片中}...... // 调整newg的栈顶指针totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frametotalSize += -totalSize & (sys.SpAlign - 1) // align to spAlignsp := newg.stack.hi - totalSizespArg := sp......if narg > 0 {memmove(unsafe.Pointer(spArg), argp, uintptr(narg)) // 将参数从调用newproc的函数栈帧中copy到新的g栈帧中。 ......} // newg.sched存储的是调度相关的信息,调度器要将这些信息装载到cpu中才能运行goroutine。memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))// 将newg.sched结构体清零newg.sched.sp = sp// 栈顶newg.stktopsp = sp // 此处只是暂时借用pc属性存储 runtime.goexit + 1 位置的地址。在gostartcallfn会用到。newg.sched.pc = funcPC(goexit) + sys.PCQuantum// +PCQuantum so that previous instruction is in same functionnewg.sched.g = guintptr(unsafe.Pointer(newg))// 存储newg指针gostartcallfn(&newg.sched, fn)// 将函数与g关联起来。下有分析。......casgstatus(newg, _Gdead, _Grunnable)// 更改状态......runqput(_p_, newg, true)// 存储到运行队列中。 // 初始化时不会执行,mainStarted 在 runtime.main 中设置为 trueif atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {wakep()}releasem(_g_.m)}
总结一下初始化时newproc1做的工作:
- 调用gfget获取newg,如果为nil,调用malg分配一个,然后加入到全局变量allgs中。
- 从调用newproc的函数栈帧中copy参数到newg栈帧中。
- 设置newg.sched属性,调用gostartcallfn,将newg和函数关联。
- 更改状态为_Grunnable,存储到p.runq中(p.runq长度是256,满了会被拿出一些放在sched.runq中)。
概括讲就是:获取g->复制参数->设置调度属性->放入队列等调度。
下面来分析以下gfget、gostartcallfn。
runtime.gfget
整体逻辑为:在p.gFree为空,sched.gFree中不空时,从后者向前者最多转移32个。然后从前者的头部返回一个。如果没有分配栈帧,就分配。
func gfget(_p_ *p) *g {retry: // 如果p.gFree为空,但sched.gFree中不为空,则从其中最多获取32个if _p_.gFree.empty() && (!sched.gFree.stack.empty() || !sched.gFree.noStack.empty()) {lock(&sched.gFree.lock)// Move a batch of free Gs to the P.for _p_.gFree.n < 32 {// Prefer Gs with stacks.gp := sched.gFree.stack.pop()if gp == nil {gp = sched.gFree.noStack.pop()if gp == nil {break}}sched.gFree.n--_p_.gFree.push(gp)_p_.gFree.n++}unlock(&sched.gFree.lock)goto retry}gp := _p_.gFree.pop()// 从列表头部获取一个gif gp == nil {return nil}_p_.gFree.n--if gp.stack.lo == 0 {// 没有栈就分配栈// Stack was deallocated in gfput. Allocate a new one.systemstack(func() {gp.stack = stackalloc(_FixedStack)})gp.stackguard0 = gp.stack.lo + _StackGuard} else {......}return gp}
runtime.gostartcallfn
func gostartcallfn(gobuf *gobuf, fv *funcval) {var fn unsafe.Pointer // fn是真正指向函数的指针if fv != nil {fn = unsafe.Pointer(fv.fn)} else {fn = unsafe.Pointer(funcPC(nilfunc))}gostartcall(gobuf, fn, unsafe.Pointer(fv))}
runtime.gostartcall
gostartcall主要做了两件事:
- 将 fn 伪造成是被 goexit 调用的
- 将 buf.pc 赋值为真正的函数指针
func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) {sp := buf.spif sys.RegSize > sys.PtrSize {sp -= sys.PtrSize*(*uintptr)(unsafe.Pointer(sp)) = 0}sp -= sys.PtrSize// 为返回地址预留空间 // buf.pc 存储的是 funcPC(goexit) + sys.PCQuantum // 将其存储到返回地址是为了伪造成 fn 是被 goexit 调用的,在 fn 执行完后返回 goexit执行,做一些清理工作。*(*uintptr)(unsafe.Pointer(sp)) = buf.pcbuf.sp = sp// 重新赋值buf.pc = uintptr(fn)// 赋值为函数指针buf.ctxt = ctxt}
文章来源:智云一二三科技
文章标题:Golang源码学习:调度逻辑(二)main goroutine的创建
文章地址:https://www.zhihuclub.com/1044.shtml