zoukankan      html  css  js  c++  java
  • Golang源码学习:调度逻辑(二)main goroutine的创建

    接上一篇继续分析一下runtime.newproc方法。

    函数签名

    newproc函数的签名为 newproc(siz int32, fn *funcval)

    siz是传入的参数大小(不是个数);fn对应的是函数,但并不是函数指针,funcval.fn才是真正指向函数代码的指针。

    // go/src/runtime/runtime2.go
    type funcval struct {
    	fn uintptr // 真正指向函数代码的指针
    }
    

    关键字go

    在golang中编译器会把类似 go foo() 编译成调用 runtime.newproc 方法。

    准备一段代码:

    package main
    
    import (
    	"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.go
    Type 'help' for list of commands.
    (dlv) b main.main
    Breakpoint 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) disass
    TEXT main.main(SB) /root/study/test.go
    	test.go:8		0x4ada00	64488b0c25f8ffffff	mov rcx, qword ptr fs:[0xfffffff8]
    	test.go:8		0x4ada09	483b6110		cmp rsp, qword ptr [rcx+0x10]
    	test.go:8		0x4ada0d	764f			jbe 0x4ada5e
    	test.go:8		0x4ada0f*	4883ec28		sub rsp, 0x28
    	test.go:8		0x4ada13	48896c2420		mov qword ptr [rsp+0x20], rbp
    	test.go:8		0x4ada18	488d6c2420		lea rbp, ptr [rsp+0x20]
    
            // 在main的栈帧中设置newproc的参数siz,16字节
    	test.go:9		0x4ada1d	c7042410000000		mov dword ptr [rsp], 0x10
            // 计算printAdd函数对应的funcval结构体的地址放入rax
    	test.go:9		0x4ada24	488d057d5e0300		lea rax, ptr [rip+0x35e7d]
            // 在main的栈帧中设置newproc的参数fn
    	test.go:9		0x4ada2b	4889442408		mov qword ptr [rsp+0x8], rax
            // printAdd的参数a
    	test.go:9		0x4ada30	48c744241003000000	mov qword ptr [rsp+0x10], 0x3
            // printAdd的参数b
    	test.go:9		0x4ada39	48c744241807000000	mov qword ptr [rsp+0x18], 0x7
            // 调用 runtime.newproc
    =>	test.go:9		0x4ada42	e80902f9ff		call $runtime.newproc
    
    	test.go:10		0x4ada47	48c7042400ca9a3b	mov qword ptr [rsp], 0x3b9aca00
    	test.go:10		0x4ada4f	e86c4afaff		call $time.Sleep
    	test.go:11		0x4ada54	488b6c2420		mov rbp, qword ptr [rsp+0x20]
    	test.go:11		0x4ada59	4883c428		add rsp, 0x28
    	test.go:11		0x4ada5d	c3			ret
    	test.go:8		0x4ada5e	e88d47fbff		call $runtime.morestack_noctxt
    	<autogenerated>:1	0x4ada63	eb9b			jmp $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·main
    	MOVQ	$runtime·mainPC(SB), AX	// runtime·mainPC就是runtime·main
    	PUSHQ	AX			// newproc的第二个参数fn,也就是goroutine要执行的函数。
    	PUSHQ	$0			// newproc的第一个参数siz,表示要传入runtime·main中参数的大小,此处为0。
    	// 创建 main goroutine。非main goroutine也是此方法创建。
    	CALL	runtime·newproc(SB)	
    	POPQ	AX
    	POPQ	AX
            ......
    DATA	runtime·mainPC+0(SB)/8,$runtime·main(SB)
    GLOBL	runtime·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 AX
    	pc := 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 := narg
    	siz = (siz + 7) &^ 7	// 使siz为8的整数倍。&^为双目运算符,将运算符左边数据相异的保留,相同位清零。
            ......
    	_p_ := _g_.m.p.ptr()	// 当前关联的p。allp[0]
    	newg := gfget(_p_)	// 获取一个g,下有分析。
    	if newg == nil {
    		newg = malg(_StackMin)			// 分配一个新g
    		casgstatus(newg, _Gidle, _Gdead)	// 更改状态
    		allgadd(newg)				// 加入到allgs切片中
    	}
    	......
            // 调整newg的栈顶指针
    	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 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 function
    	newg.sched.g = guintptr(unsafe.Pointer(newg))	// 存储newg指针
    	gostartcallfn(&newg.sched, fn)			// 将函数与g关联起来。下有分析。
    	......
    	casgstatus(newg, _Gdead, _Grunnable)		// 更改状态
    	......
    	runqput(_p_, newg, true)			// 存储到运行队列中。
    
             // 初始化时不会执行,mainStarted 在 runtime.main 中设置为 true
    	if 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()	// 从列表头部获取一个g
    	if 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.sp
    	if 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.pc
    	buf.sp = sp		// 重新赋值
    	buf.pc = uintptr(fn)	// 赋值为函数指针
    	buf.ctxt = ctxt
    }
    
  • 相关阅读:
    SQL 拾遗
    PowerDesigner技巧
    进步
    'data.csv'
    System.Web”中不存在类型或命名空间名称script /找不到System.Web.Extensions.dll引用
    要学的技术
    Sql 表变量
    Tomcat 7.0的配置
    开发工具
    jQuery UI Dialog
  • 原文地址:https://www.cnblogs.com/flhs/p/12677335.html
Copyright © 2011-2022 走看看