热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

go1.14基于信号的抢占式调度实现原理

转载地址:http:xiaorui.ccarchives6535前言:疫情期间里老老实实在家蹲着,这期间主要研究下go1.14新增的部分。go1.14

转载地址:http://xiaorui.cc/archives/6535

golang signal retake

前言:

疫情期间里老老实实在家蹲着,这期间主要研究下go 1.14新增的部分。go 1.14中比较大的更新有信号的抢占调度、defer内联优化,定时器优化等。前几天刚写完了golang 1.14 timer定时器的优化,有兴趣的朋友可以看看,http://xiaorui.cc/?p=6483

golang在之前的版本中已经实现了抢占调度,不管是陷入到大量计算还是系统调用,大多可被sysmon扫描到并进行抢占。但有些场景是无法抢占成功的。比如轮询计算 for { i++ } 等,这类操作无法进行newstack、morestack、syscall,所以无法检测stackguard0 = stackpreempt。

go team已经意识到抢占是个问题,所以在1.14中加入了基于信号的协程调度抢占。原理是这样的,首先注册绑定 SIGURG 信号及处理方法runtime.doSigPreempt,sysmon会间隔性检测超时的p,然后发送信号,m收到信号后休眠执行的goroutine并且进行重新调度。

该文章后续仍在不断的更新修改中,请移步到原文地址 http://xiaorui.cc/?p=6535

对比测试:

// xiaorui.ccpackage mainimport ("runtime"
)func main() {runtime.GOMAXPROCS(1)go func() {panic("already call")}()for {}
}

Go

COPY

上面的测试思路是先针对GOMAXPROCS的p配置为1,这样就可以规避并发而影响抢占的测试,然后go关键字会把当前传递的函数封装协程结构,扔到runq队列里等待runtime调度,由于是异步执行,所以就执行到for死循环无法退出。

go1.14是可以执行到panic,而1.13版本一直挂在死循环上。那么在go1.13是如何解决这个问题? 要么并发加大,要么执行一个syscall,要么执行复杂的函数产生morestack扩栈。对比go1.13版,通过strace可以看到go1.14多了一步发送信号中断。这看似就是文章开头讲到的基于信号的抢占式调度了。

源码分析:

以前写过文章来分析go sysmon() 的工作,在新版go 1.14里其他功能跟以前一样,只是加入了信号抢占。

怎么注册的sigurg信号?

// xiaorui.ccconst sigPreempt &#61; _SIGURGfunc initsig(preinit bool) {for i :&#61; uint32(0); i <_NSIG; i&#43;&#43; {fwdSig[i] &#61; getsig(i),,,setsig(i, funcPC(sighandler)) // 注册信号对应的回调方法}
}func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {,,,if sig &#61;&#61; sigPreempt { // 如果是抢占信号// Might be a preemption signal.doSigPreempt(gp, c)},,,
}// 执行抢占
func doSigPreempt(gp *g, ctxt *sigctxt) {if wantAsyncPreempt(gp) && isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()) {// Inject a call to asyncPreempt.ctxt.pushCall(funcPC(asyncPreempt)) // 执行抢占的关键方法}// Acknowledge the preemption.atomic.Xadd(&gp.m.preemptGen, 1)
}

Go

COPY

go在启动时把所有的信号都注册了一遍&#xff0c;包括可靠的信号。(截图为部分)

由谁去发起检测抢占?

go1.14之前的版本是是由sysmon检测抢占&#xff0c;到了go1.14当然也是由sysmon操作。runtime在启动时会创建一个线程来执行sysmon&#xff0c;为什么要独立执行&#xff1f; sysmon是golang的runtime系统检测器&#xff0c;sysmon可进行forcegc、netpoll、retake等操作。拿抢占功能来说&#xff0c;如sysmon放到pmg调度模型里&#xff0c;每个p上面的goroutine恰好阻塞了&#xff0c;那么还怎么执行抢占&#xff1f;

所以sysmon才要独立绑定运行&#xff0c;就上面的脚本在测试运行的过程中&#xff0c;虽然看似阻塞状态&#xff0c;但进行strace可看到sysmon在不断休眠唤醒操作。sysmon启动后会间隔性的进行监控&#xff0c;最长间隔10ms&#xff0c;最短间隔20us。如果某协程独占P超过10ms&#xff0c;那么就会被抢占&#xff01;

sysmon依赖schedwhen和schedtick来记录上次的监控信息&#xff0c;schedwhen记录上次的检测时间&#xff0c;schedtick来区分调度时效。比如sysmon在两次监控检测期间&#xff0c;已经发生了多次runtime.schedule协程调度切换&#xff0c;每次调度时都会更新schedtick值。所以retake发现sysmontick.schedtick值不同时重新记录schedtick。

runtime/proc.go

// xiaorui.ccfunc main() {g :&#61; getg(),,,if GOARCH !&#61; "wasm" {systemstack(func() {newm(sysmon, nil)})},,,
}func schedule() {,,,execute(gp, inheritTime)
}func execute(gp *g, inheritTime bool) {if !inheritTime {_g_.m.p.ptr().schedtick&#43;&#43;},,,
}func sysmon(){,,,// retake P&#39;s blocked in syscalls// and preempt long running G&#39;sif retake(now) !&#61; 0 {idle &#61; 0} else {idle&#43;&#43;},,,
}// 记录每次检查的信息
type sysmontick struct {schedtick uint32schedwhen int64syscalltick uint32syscallwhen int64
}const forcePreemptNS &#61; 10 * 1000 * 1000 // 抢占的时间阈值 10msfunc retake(now int64) uint32 {n :&#61; 0lock(&allpLock)for i :&#61; 0; i } func preemptone(_p_ *p) bool {mp :&#61; _p_.m.ptr(),,,gp.preempt &#61; true,,,gp.stackguard0 &#61; stackPreempt// Request an async preemption of this P.if preemptMSupported && debug.asyncpreemptoff &#61;&#61; 0 {_p_.preempt &#61; truepreemptM(mp)}return true
}

Go

COPY

发送SIGURG信号&#xff1f;

signal_unix.go

// xiaorui.cc// 给m发送sigurg信号
func preemptM(mp *m) {if !pushCallSupported {// This architecture doesn&#39;t support ctxt.pushCall// yet, so doSigPreempt won&#39;t work.return}if GOOS &#61;&#61; "darwin" && (GOARCH &#61;&#61; "arm" || GOARCH &#61;&#61; "arm64") && !iscgo {return}signalM(mp, sigPreempt)
}

Go

COPY

收到sigurg信号后如何处理 ?

preemptPark方法会解绑mg的关系&#xff0c;封存当前协程&#xff0c;继而重新调度runtime.schedule()获取可执行的协程&#xff0c;至于被抢占的协程后面会去重启。

goschedImpl操作就简单的多&#xff0c;把当前协程的状态从_Grunning正在执行改成 _Grunnable可执行&#xff0c;使用globrunqput方法把抢占的协程放到全局队列里&#xff0c;根据pmg的协程调度设计&#xff0c;globalrunq要后于本地runq被调度。

runtime/preempt.go

// xiaorui.cc//go:generate go run mkpreempt.go// asyncPreempt saves all user registers and calls asyncPreempt2.
//
// When stack scanning encounters an asyncPreempt frame, it scans that
// frame and its parent frame conservatively.
func asyncPreempt()//go:nosplit
func asyncPreempt2() {gp :&#61; getg()gp.asyncSafePoint &#61; trueif gp.preemptStop {mcall(preemptPark)} else {mcall(gopreempt_m)}gp.asyncSafePoint &#61; false
}

Go

COPY

runtime/proc.go

// xiaorui.cc// preemptPark parks gp and puts it in _Gpreempted.
//
//go:systemstack
func preemptPark(gp *g) {,,,status :&#61; readgstatus(gp)if status&^_Gscan !&#61; _Grunning {dumpgstatus(gp)throw("bad g status")},,,schedule()
}func goschedImpl(gp *g) {status :&#61; readgstatus(gp),,,casgstatus(gp, _Grunning, _Grunnable)dropg()lock(&sched.lock)globrunqput(gp)unlock(&sched.lock)schedule()
}

Go

COPY

源码解析粗略的分析完了&#xff0c;还有一些细节不好读懂&#xff0c;但信号抢占实现的大方向摸的89不离10了。

抢占是否影响性能 &#xff1f;

抢占分为_Prunning和Psyscall&#xff0c;Psyscall抢占通常是由于阻塞性系统调用引起的&#xff0c;比如磁盘io、cgo。Prunning抢占通常是由于一些类似死循环的计算逻辑引起的。

过度的发送信号来中断m进行抢占多少会影响性能的&#xff0c;主要是软中断和上下文切换。在平常的业务逻辑下&#xff0c;很难发生协程阻塞调度的问题。&#x1f605;

慢系统调度的错误处理&#xff1f;

EINTR错误通常是由于被信号中断引起的错误&#xff0c;比如在执行epollwait、accept、read&write 等操作时&#xff0c;收到信号&#xff0c;那么该系统调用会被打断中断&#xff0c;然后去执行信号注册的回调方法&#xff0c;完事后会返回eintr错误。

下面是golang的处理方法&#xff0c;由于golang的netpoll设计使多数的io相关的syscall操作非阻塞化&#xff0c;所以就只有epollwait有该问题。

// xiaorui.ccfunc netpoll(delay int64) gList {,,,var events [128]epollevent
retry:n :&#61; epollwait(epfd, &events[0], int32(len(events)), waitms)if n <0 {if n !&#61; -_EINTR {println("runtime: epollwait on fd", epfd, "failed with", -n)throw("runtime: netpoll failed")}goto retry},,,
}func netpollBreak() {for {var b byten :&#61; write(netpollBreakWr, unsafe.Pointer(&b), 1)if n &#61;&#61; 1 {break}if n &#61;&#61; -_EINTR {continue},,,}
}

Go

COPY

通常需要手动来解决EINTR的错误问题&#xff0c;虽然可通过SA_RESTART来重启被中断的系统调用&#xff0c;但不管是syscall兼容和业务上有可能出现偏差。

// xiaorui.cc// epoll_wait
if( -1 &#61;&#61; epoll_wait() )
{if(errno!&#61;EINTR){return -1;}
}// read
again:if ((n &#61; read(fd&#xff0c; buf&#xff0c; BUFFSIZE)) <0) {if (errno &#61;&#61; EINTR)goto again;}

Go

COPY

配置SA_RESTART后&#xff0c;线程被中断后还可继续执行被中断的系统调用。

// xiaorui.cc--- SIGINT {si_signo&#61;SIGINT, si_code&#61;SI_KERNEL, si_value&#61;{int&#61;0, ptr&#61;0x100000000}} ---
rt_sigreturn() &#61; -1 EINTR (Interrupted system call)
futex(0x1b97a30, FUTEX_WAIT_PRIVATE, 0, NULL) &#61; ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGINT {si_signo&#61;SIGINT, si_code&#61;SI_KERNEL, si_value&#61;{int&#61;0, ptr&#61;0x100000000}} ---
rt_sigreturn() &#61; -1 EINTR (Interrupted system call)
...

Bash

COPY

信号的原理&#xff1f;

我们对一个进程发送信号后&#xff0c;内核把信号挂载到目标进程的信号 pending 队列上去&#xff0c;然后进行触发软中断设置目标进程为running状态。当进程被唤醒或者调度后获取CPU后&#xff0c;才会从内核态转到用户态时检测是否有signal等待处理&#xff0c;等进程处理完后会把相应的信号从链表中去掉。

通过kill -l拿到当前系统支持的信号列表&#xff0c;1-31为不可靠信号&#xff0c;也是非实时信号&#xff0c;信号有可能会丢失&#xff0c;比如发送多次相同的信号&#xff0c;进程只能收到一次。

// xiaorui.cc// Print a list of signal names. These are found in /usr/include/linux/signal.hkill -l1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS

Bash

COPY

在Linux中的posix线程模型中&#xff0c;线程拥有独立的进程号&#xff0c;可以通过getpid()得到线程的进程号&#xff0c;而线程号保存在pthread_t的值中。而主线程的进程号就是整个进程的进程号&#xff0c;因此向主进程发送信号只会将信号发送到主线程中去。如果主线程设置了信号屏蔽&#xff0c;则信号会投递到一个可以处理的线程中去。

注册的信号处理函数都是线程共享的&#xff0c;一个信号只对应一个处理函数&#xff0c;且最后一次为准。子线程也可更改信号处理函数&#xff0c;且随时都可改。

多线程下发送及接收信号的问题&#xff1f;

默认情况下只有主线程才可处理signal&#xff0c;就算指定子线程发送signal&#xff0c;也是主线程接收处理信号。

那么Golang如何做到给指定子线程发signal且处理的&#xff1f;如何指定给某个线程发送signal&#xff1f; 在glibc下可以使用pthread_kill来给线程发signal&#xff0c;它底层调用的是SYS_tgkill系统调用。

golang signal

// xiaorui.cc#include "pthread_impl.h"int pthread_kill(pthread_t t, int sig)
{int r;__lock(t->killlock);r &#61; t->dead ? ESRCH : -__syscall(SYS_tgkill, t->pid, t->tid, sig);__unlock(t->killlock);return r;
}

C&#43;&#43;

COPY

那么在go runtime/sys_linux_amd64.s里找到了SYS_tgkill的汇编实现。os_linux.go中signalM调用的就是tgkill的实现。

// xiaorui.cc
#define SYS_tgkill 234TEXT ·tgkill(SB),NOSPLIT,0MOVQ tgid&#43;0(FP), DIMOVQ tid&#43;8(FP), SIMOVQ sig&#43;16(FP), DXMOVLSYS_tgkill, AXSYSCALLRET

C&#43;&#43;

COPY

// xiaorui.ccfunc tgkill(tgid, tid, sig int)// signalM sends a signal to mp.
func signalM(mp *m, sig int) {tgkill(getpid(), int(mp.procid), sig)
}

Go

COPY

总结&#xff1a;

随着go版本不断更新&#xff0c;runtime的功能越来越完善。现在看来基于信号的抢占式调度显得很精妙。下一篇文章继续写go1.4 defer的优化&#xff0c;简单说在多场景下编译器消除了deferproc压入和deferreturn插入调用&#xff0c;而是直接调用延迟方法。


推荐阅读
  • Java自带的观察者模式及实现方法详解
    本文介绍了Java自带的观察者模式,包括Observer和Observable对象的定义和使用方法。通过添加观察者和设置内部标志位,当被观察者中的事件发生变化时,通知观察者对象并执行相应的操作。实现观察者模式非常简单,只需继承Observable类和实现Observer接口即可。详情请参考Java官方api文档。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • Spring学习(4):Spring管理对象之间的关联关系
    本文是关于Spring学习的第四篇文章,讲述了Spring框架中管理对象之间的关联关系。文章介绍了MessageService类和MessagePrinter类的实现,并解释了它们之间的关联关系。通过学习本文,读者可以了解Spring框架中对象之间的关联关系的概念和实现方式。 ... [详细]
  • 面向对象之3:封装的总结及实现方法
    本文总结了面向对象中封装的概念和好处,以及在Java中如何实现封装。封装是将过程和数据用一个外壳隐藏起来,只能通过提供的接口进行访问。适当的封装可以提高程序的理解性和维护性,增强程序的安全性。在Java中,封装可以通过将属性私有化并使用权限修饰符来实现,同时可以通过方法来访问属性并加入限制条件。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 本文介绍了在多平台下进行条件编译的必要性,以及具体的实现方法。通过示例代码展示了如何使用条件编译来实现不同平台的功能。最后总结了只要接口相同,不同平台下的编译运行结果也会相同。 ... [详细]
  • 本文介绍了一个题目的解法,通过二分答案来解决问题,但困难在于如何进行检查。文章提供了一种逃逸方式,通过移动最慢的宿管来锁门时跑到更居中的位置,从而使所有合格的寝室都居中。文章还提到可以分开判断两边的情况,并使用前缀和的方式来求出在任意时刻能够到达宿管即将锁门的寝室的人数。最后,文章提到可以改成O(n)的直接枚举来解决问题。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
author-avatar
49897801g9Iq
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有