如果修改信号处理程序中的 ctx.rip 和 ctx.rsp 会发生什么

What happens if ctx.rip and ctx.rsp in a signal handler is modified

众所周知,程序被信号中断并进入内核space然后切换到用户space信号处理程序。信号处理完成后,将重新进入内核space,然后切换回被中断的地方。

我最近正在阅读 go 1.14 中新实现的异步抢占,它使用 OS 信号来中断 "non-preemptive" 用户 goroutine。我正在调试非常简单的程序:

package main

import (
    "runtime"
    "time"
)

func tightloop() {
    for {
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go tightloop()

    time.Sleep(time.Millisecond)
    println("OK")
    runtime.Gosched()
}

在 Go 1.14 中,当抢占信号到达时,tightloop 将被 OS 中断并进入预先配置的信号处理程序 runtime·sigtramp:

TEXT runtime·sigtramp(SB),NOSPLIT,
    MOVQ    DX, ctx-56(SP)
    MOVQ    SI, info-64(SP)
    MOVQ    DI, signum-72(SP)
    MOVQ    $runtime·sigtrampgo(SB), AX
    CALL AX
    RET

sigtrampgo 最终调用 sighandler

//go:nosplit
//go:nowritebarrierrec
func sigtrampgo(sig uint32, info *siginfo, ctx unsafe.Pointer) {
    (...)
    setg(g.m.gsignal)
    (...)
    sighandler(sig, info, ctx, g)
    setg(g)
    (...)
}

据我阅读 sighandler 函数,它调用 doSigPreempt 并修改从系统内核传递的 ctx,并将 rip 设置为序言runtime.asyncPreempt.

//go:nowritebarrierrec
func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {
    _g_ := getg()
    c := &sigctxt{info, ctxt}

    (...)
    if sig == sigPreempt {
        doSigPreempt(gp, c)
    }
}
func doSigPreempt(gp *g, ctxt *sigctxt) {
    if canPreempt {
        // here modifies the rip and rsp
        ctxt.pushCall(funcPC(asyncPreempt))
    }

    (...)
}

但是,我注意到 asyncPreempt 并没有立即执行 信号处理程序已完成,而是:

  1. morestackmorestack_noctxtsighandler 返回后调用 (不进入结语或序言) ,它调用 newstack 并检查抢占标志并进入调度循环,因此安排主 goroutine 完成异步抢占。

  2. OK执行前的输出asyncPreempt

这是我在运行时插入的打印日志:

mstart1 call schedule()
enter schedule()
park_m call schedule()
enter schedule()
mstart1 call schedule()
enter schedule()
mstart1 call schedule()
enter schedule()
park_m call schedule()
enter schedule()
park_m call schedule()
enter schedule()
park_m call schedule()
enter schedule()
mstart1 call schedule()
enter schedule()
park_m call schedule()
enter schedule()
rip: 17149264 eip: 824634034136
before pushCall asyncPreempt
after pushCall asyncPreempt
rip: 17124704 eip: 824634034128      // rip points to asyncPreempt
calling newstack: m0, g0             // how could newstack is called?
newstack call gopreempt_m
gopreempt_m call goschedImpl
goschedImpl call schedule()
enter schedule()
OK
gosched_m call goschedImpl
goschedImpl call schedule()
enter schedule()
asyncPreempt2
asyncPreempt2
asyncPreempt2
asyncPreempt2
preemptPark
gopreempt_m call goschedImpl
goschedImpl call schedule()
enter schedule()

虽然我检查了转储的汇编代码,但没有堆栈拆分检查 asyncPreemptsigtramp.

抱歉说来话长,我的问题是:

非常感谢您阅读问题并感谢 go 团队构建了如此出色的功能。

我已经弄明白了,非常感谢 Ian 的提示:

https://groups.google.com/forum/#!topic/golang-nuts/BA7Dqp_zcwk

根本原因似乎类似于 "uncertainty principle"。

作为观察者,通过在 asyncPreempt 中添加 println 调用 以及 asyncPreempt2 影响实际行为 信号处理后。 println涉及堆栈拆分检查, 调用 morestack.

我花了一段时间才意识到 morestack 存储了它的调用者 g.m.morebuf.pc 中的 pc 自 newstack 中的 getcallerpc 以来 总是 returns 来自 morestack 的电脑,这并没有说明 信息太多了。

//go:nosplit
func asyncPreempt2() {
    // println("asyncPreempt2 is called") // comment here omits calling morestack.
    gp := getg()
    gp.asyncSafePoint = true
    if gp.preemptStop {
        mcall(preemptPark)
    } else {
        mcall(gopreempt_m)
    }
    println("asyncPreempt2 finished")
    gp.asyncSafePoint = false
}

TLDR:在信号处理程序之后,内核恢复 asyncPreemptrip 并直接切换到它,runtime.asyncPreemptruntime.sigtramp 之间没有任何反应.