返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

4.5.3 执行

发布于 2024-10-12 19:15:58 字数 3111 浏览 0 评论 0 收藏 0

切换到 G.stack,执行并发函数。

// proc.go

// 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.

func execute(gp *g, inheritTime bool) {
	_g_ := getg()

    // 设置任务状态。
	_g_.m.curg = gp
	gp.m = _g_.m
	casgstatus(gp, _Grunnable, _Grunning)
	gp.waitsince = 0
	gp.preempt = false
	gp.stackguard0 = gp.stack.lo + _StackGuard
    
    // 累加执行计数器。
	if !inheritTime {
		_g_.m.p.ptr().schedtick++
	}

    // 执行。
	gogo(&gp.sched)
}

函数 gogo 是用汇编实现的执行函数,它会切换到 G.stack,并跳转到用户函数。详情参考下节。

结束

无论是 execute,还是 gogo 都没有直接回到 schedule 的意思。那么,如何清理执行现场?比如将 dead G 放回复用链表。如何继续循环调度呢?

在 newproc1 设置 G.sched 的时候,实际保存在 pc 的是 goexit。

// proc.go

func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
    
	newg.sched.sp = sp
	newg.stktopsp = sp

    newg.sched.pc = abi.FuncPCABI0(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.startpc = fn.fn

	return newg
}

关键就在于 gostartcallfn 做了什么。

// stack.go

// adjust Gobuf as if it executed a call to fn
// and then stopped before the first instruction in fn.

func gostartcallfn(gobuf *gobuf, fv *funcval) {
	var fn unsafe.Pointer
	if fv != nil {
		fn = unsafe.Pointer(fv.fn)
	} else {
		fn = unsafe.Pointer(abi.FuncPCABIInternal(nilfunc))
	}
	gostartcall(gobuf, fn, unsafe.Pointer(fv))
}
// sys_x86.go

// adjust Gobuf as if it executed a call to fn with context ctxt
// and then stopped before the first instruction in fn.

func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) {
	sp := buf.sp
    
    // 将 G.sched.sp 上移一个指针空间。
	sp -= goarch.PtrSize
    
    // 存储 G.sched.pc,也就是预设的 goexit。
	*(*uintptr)(unsafe.Pointer(sp)) = buf.pc
    
    // 调整 G.sched 设置,此时 pc 指向用户函数。
	buf.sp = sp
	buf.pc = uintptr(fn)
	buf.ctxt = ctxt
}

当 execute 通过 gogo 切换到 G.stack,执行用户函数时,其堆栈如下:

lo  |          |
    | G.fn     |
    +----------+  G.sched.sp
    | goexit   |
hi  +----------+
    |          |

而 gogo 以 JMP 调用 G.fn,就是说不会将 gogo 的 IP/PC 入栈。等 G.fn RET 执行时, POP PC 的结果自然是 goexit。该函数以 mcall 切换回 g0 栈,完成清理操作,重新回到调度循环。

// asm_amd64.s

// The top-most function running on a goroutine
// returns to goexit+PCQuantum.

TEXT runtime·goexit(SB),NOSPLIT|TOPFRAME,$0-0
    CALL	runtime·goexit1(SB)	// does not return
// proc.go

// Finishes execution of the current goroutine.
func goexit1() {
    mcall(goexit0)
}

// goexit continuation on g0.
func goexit0(gp *g) {
    _g_ := getg()
    _p_ := _g_.m.p.ptr()
    
    // 清理已结束任务状态,并记入 GC。
    casgstatus(gp, _Grunning, _Gdead)
    gcController.addScannableStack(_p_, -int64(gp.stack.hi-gp.stack.lo))
    
    gp.m = nil
    gp.lockedm = 0
    ...
    gp.timer = nil

    // 解除与当前 M 的关联。
    dropg()
    
    // 放回闲置链表(P.gFree),等待复用。
    gfput(_p_, gp)

    // 重回调度函数。
    schedule()
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文