Go语言基于信号抢占式调度的示例分析

75次阅读
没有评论

共计 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)

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-08-04发表,共计5533字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)