共计 5533 个字符,预计需要花费 14 分钟才能阅读完成。
这篇文章将为大家详细讲解有关 Go 语言基于信号抢占式调度的示例分析,文章内容质量较高,因此丸趣 TV 小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。
介绍
在 Go 的 1.14 版本之前抢占试调度都是基于协作的,需要自己主动的让出执行,但是这样是无法处理一些无法被抢占的边缘情况。例如:for 循环或者垃圾回收长时间占用线程,这些问题中的一部分直到 1.14 才被基于信号的抢占式调度解决。
下面我们通过一个例子来验证一下 1.14 版本和 1.13 版本之间的抢占差异:
package main
import (
fmt
runtime
runtime/trace
sync
func main() {runtime.GOMAXPROCS(1)
f, _ := os.Create(trace.output)
defer f.Close()
_ = trace.Start(f)
defer trace.Stop()
var wg sync.WaitGroup
for i := 0; i 30; i++ {wg.Add(1)
go func() {defer wg.Done()
t := 0
for i:=0;i i++ {
t+=2
fmt.Println(total: , t)
wg.Wait()}
这个例子中会通过 go trace 来进行执行过程的调用跟踪。在代码中指定 runtime.GOMAXPROCS(1) 设置最大的可同时使用的 CPU 核数为 1,只用一个 P(处理器),这样就确保是单处理器的场景。然后调用一个 for 循环开启 10 个 goroutines 来执行 func 函数,这是一个纯计算且耗时的函数,防止 goroutines 空闲让出执行。
下面我们编译程序分析 trace 输出:
$ go build -gcflags -N -l main.go
- N 表示禁用优化
- l 禁用内联
$ ./main
然后我们获取到 trace.output 文件后进行可视化展示:
$ go tool trace -http= :6060 ./trace.output
Go1.13 trace 分析
从上面的这个图可以看出:
因为我们限定了只有一个 P,所以在 PROCS 这一栏里面只有一个 Proc0;
我们在 for 循环里面启动了 30 个 goroutines,所以我们可以数一下 Proc0 里面的颜色框框,刚好 30 个;
30 个 goroutines 在 Proc0 里面是串行执行的,一个执行完再执行另一个,没有进行抢占;
随便点击一个 goroutines 的详情栏可以看到 Wall Duration 为 0.23s 左右,表示这个 goroutines 持续执行了 0.23s,总共 10 个 goroutines 执行时间是 7s 左右;
切入调用栈 Start Stack Trace 是 main.main.func1:20,在代码上面是 func 函数执行头:go func();
切走调用栈 End Stack Trace 是 main.main.func1:26,在代码上是 func 函数最后执行打印:fmt.Println(total: , t);
从上面的 trace 分析可以知道,Go 的协作式调度对 calcSum 函数是毫无作用的,一旦执行开始,只能等执行结束。每个 goroutine 耗费了 0.23s 这么长的时间,也无法抢占它的执行权。
Go 1.14 以上 trace 分析
在 Go 1.14 之后引入了基于信号的抢占式调度,从上面的图可以看到 Proc0 这一栏中密密麻麻都是 goroutines 在切换时的调用情况,不会再出现 goroutines 一旦执行开始,只能等执行结束这种情况。
上面跑动的时间是 4s 左右这个情况可以忽略,因为我是在两台配置不同的机器上跑的(主要是我闲麻烦要找两台一样的机器)。
下面我们拉近了看一下明细情况:
通过这个明细可以看出:
这个 goroutine 运行了 0.025s 就让出执行了;
切入调用栈 Start Stack Trace 是 main.main.func1:21,和上面一样;
切走调用栈 End Stack Trace 是 runtime.asyncPreempt:50,这个函数是收到抢占信号时执行的函数,从这个地方也能明确的知道,被异步抢占了;
分析抢占信号的安装
runtime/signal_unix.go
程序启动时,在 runtime.sighandler 中注册 SIGURG 信号的处理函数 runtime.doSigPreempt。
initsig
func initsig(preinit bool) {
// 预初始化
if !preinit {
signalsOK = true
}
// 遍历信号数组
for i := uint32(0); i _NSIG; i++ {t := sigtable[i]
// 略过信号:SIGKILL、SIGSTOP、SIGTSTP、SIGCONT、SIGTTIN、SIGTTOU
if t.flags == 0 || t.flags _SigDefault != 0 {continue}
...
setsig(i, funcPC(sighandler))
}
在 initsig 函数里面会遍历所有的信号量,然后调用 setsig 函数进行注册。我们可以查看 sigtable 这个全局变量看看有什么信息:
var sigtable = [...]sigTabT{/* 0 */ {0, SIGNONE: no trap},
/* 1 */ {_SigNotify + _SigKill, SIGHUP: terminal line hangup},
/* 2 */ {_SigNotify + _SigKill, SIGINT: interrupt},
/* 3 */ {_SigNotify + _SigThrow, SIGQUIT: quit},
/* 4 */ {_SigThrow + _SigUnblock, SIGILL: illegal instruction},
/* 5 */ {_SigThrow + _SigUnblock, SIGTRAP: trace trap},
/* 6 */ {_SigNotify + _SigThrow, SIGABRT: abort},
/* 7 */ {_SigPanic + _SigUnblock, SIGBUS: bus error},
/* 8 */ {_SigPanic + _SigUnblock, SIGFPE: floating-point exception},
/* 9 */ {0, SIGKILL: kill},
/* 10 */ {_SigNotify, SIGUSR1: user-defined signal 1},
/* 11 */ {_SigPanic + _SigUnblock, SIGSEGV: segmentation violation},
/* 12 */ {_SigNotify, SIGUSR2: user-defined signal 2},
/* 13 */ {_SigNotify, SIGPIPE: write to broken pipe},
/* 14 */ {_SigNotify, SIGALRM: alarm clock},
/* 15 */ {_SigNotify + _SigKill, SIGTERM: termination},
/* 16 */ {_SigThrow + _SigUnblock, SIGSTKFLT: stack fault},
/* 17 */ {_SigNotify + _SigUnblock + _SigIgn, SIGCHLD: child status has changed},
/* 18 */ {_SigNotify + _SigDefault + _SigIgn, SIGCONT: continue},
/* 19 */ {0, SIGSTOP: stop, unblockable},
/* 20 */ {_SigNotify + _SigDefault + _SigIgn, SIGTSTP: keyboard stop},
/* 21 */ {_SigNotify + _SigDefault + _SigIgn, SIGTTIN: background read from tty},
/* 22 */ {_SigNotify + _SigDefault + _SigIgn, SIGTTOU: background write to tty},
/* 23 */ {_SigNotify + _SigIgn, SIGURG: urgent condition on socket}
,
/* 24 */ {_SigNotify, SIGXCPU: cpu limit exceeded},
/* 25 */ {_SigNotify, SIGXFSZ: file size limit exceeded},
/* 26 */ {_SigNotify, SIGVTALRM: virtual alarm clock},
/* 27 */ {_SigNotify + _SigUnblock, SIGPROF: profiling alarm clock},
/* 28 */ {_SigNotify + _SigIgn, SIGWINCH: window size change},
/* 29 */ {_SigNotify, SIGIO: i/o now possible},
/* 30 */ {_SigNotify, SIGPWR: power failure restart},
/* 31 */ {_SigThrow, SIGSYS: bad system call},
/* 32 */ {_SigSetStack + _SigUnblock, signal 32}, /* SIGCANCEL; see issue 6997 */
/* 33 */ {_SigSetStack + _SigUnblock, signal 33}, /* SIGSETXID; see issues 3871, 9400, 12498 */
}
具体的信号含义可以看这个介绍:Unix 信号 https://zh.wikipedia.org/wiki/Unix 信号。需要注意的是,抢占信号在这里是 _SigNotify + _SigIgn 如下:
{_SigNotify + _SigIgn, SIGURG: urgent condition on socket}
下面我们看一下 setsig 函数,这个函数是在 runtime/os_linux.go 文件里面:
setsig
func setsig(i uint32, fn uintptr) {
var sa sigactiont
sa.sa_flags = _SA_SIGINFO | _SA_ONSTACK | _SA_RESTORER | _SA_RESTART
sigfillset(sa.sa_mask)
if fn == funcPC(sighandler) {
// CGO 相关
if iscgo {fn = funcPC(cgoSigtramp)
} else {
// 替换为调用 sigtramp
fn = funcPC(sigtramp)
sa.sa_handler = fn
sigaction(i, sa, nil)
}
这里需要注意的是,当 fn 等于 sighandler 的时候,调用的函数会被替换成 sigtramp。sigaction 函数在 Linux 下会调用系统调用函数 sys_signal 以及 sys_rt_sigaction 实现安装信号。
执行抢占信号
到了这里是信号发生的时候进行信号的处理,原本应该是在发送抢占信号之后,但是这里我先顺着安装信号往下先讲了。大家可以跳到发送抢占信号后再回来。
上面分析可以看到当 fn 等于 sighandler 的时候,调用的函数会被替换成 sigtramp,sigtramp 是汇编实现,下面我们看看。
src/runtime/sys_linux_amd64.s:
TEXT runtime·sigtramp ABIInternal (SB),NOSPLIT,$72
// We don t save mxcsr or the x87 control word because sigtrampgo doesn t
// modify them.
MOVQ DX, ctx-56(SP)
MOVQ SI, info-64(SP)
MOVQ DI, signum-72(SP)
MOVQ $runtime·sigtrampgo(SB), AX
CALL AX
RET
这里会被调用说明信号已经发送响应了,runtime·sigtramp 会进行信号的处理。runtime·sigtramp 会继续调用 runtime·sigtrampgo。
这个函数在 runtime/signal_unix.go 文件中:
sigtrampgo sighandler
func sigtrampgo(sig uint32, info *siginfo, ctx unsafe.Pointer) {
if sigfwdgo(sig, info, ctx) {
return
c := sigctxt{info, ctx}
g := sigFetchG(c)
...
sighandler(sig, info, ctx, g)
setg(g)
if setStack {
restoreGsignalStack(gsignalStack)