返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

4.5.3 执行

发布于 2024-10-12 19:16:06 字数 2873 浏览 0 评论 0 收藏 0

利用 gogo 切换到 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.preempt = false
    gp.stackguard0 = gp.stack.lo + _StackGuard
    
    // 任务执行计数器。
    if !inheritTime {
        _g_.m.p.ptr().schedtick++    
    }
    
    gogo(&gp.sched)
}

goexit

无论是 execute,还是 gogo 都没有直接回到 schedule 的意思。

那么,如何清理执行现场?比如将 dead G 放回复用链表。如何继续循环调度呢?

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

func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) *g {
    newg.sched.pc = funcPC(goexit) + sys.PCQuantum
    newg.sched.g = guintptr(unsafe.Pointer(newg))
    gostartcallfn(&newg.sched, fn)
}

关键就在于 gostartcallfn 做了什么。

// stack.go

// adjust Gobuf as if it executed a call to fn
// and then did an immediate gosave.

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))
}
// sys_x86.go

// adjust Gobuf as if it executed a call to fn with context ctxt
// and then did an immediate gosave.

func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) {
    sp := buf.sp
    
    // 将 SP 前移,留出空间保存 buf.pc。
    // 目的是将前面预先设置的 goexit 入栈。
    sp -= sys.PtrSize
    *(*uintptr)(unsafe.Pointer(sp)) = buf.pc
    
    // 重新设置 sched。此时,将 G.fn 保存到 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。

// This function must never be called directly. Call goexit1 instead.
func goexit(neverCallThisFunction)

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

func goexit0(gp *g) {
    casgstatus(gp, _Grunning, _Gdead)
    gp.m = nil
    locked := gp.lockedm != 0
    
    // 解除 dead G 相关信息,放回复用列表。
    dropg()
    gfput(_g_.m.p.ptr(), gp)
    
    // 如果没有解除锁定,出错,终止线程。
    // 参考本章《线程终止》。
    if locked {
        // Return to mstart, which will release the P and exit the thread.
        if GOOS != "plan9" {
            gogo(&_g_.m.g0.sched)
        }
    }
    
    // 回到调度循环。
    schedule()
}

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

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

发布评论

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