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

前言:

疫情期间里老老实实在家蹲着,这期间主要研究下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 = _SIGURGfunc initsig(preinit bool) {for i := uint32(0); i < _NSIG; i++ {fwdSig[i] = getsig(i),,,setsig(i, funcPC(sighandler)) // 注册信号对应的回调方法}
}func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {,,,if sig == 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在启动时把所有的信号都注册了一遍,包括可靠的信号。(截图为部分)

由谁去发起检测抢占?

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

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

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

runtime/proc.go

// xiaorui.ccfunc main() {g := getg(),,,if GOARCH != "wasm" {systemstack(func() {newm(sysmon, nil)})},,,
}func schedule() {,,,execute(gp, inheritTime)
}func execute(gp *g, inheritTime bool) {if !inheritTime {_g_.m.p.ptr().schedtick++},,,
}func sysmon(){,,,// retake P's blocked in syscalls// and preempt long running G'sif retake(now) != 0 {idle = 0} else {idle++},,,
}// 记录每次检查的信息
type sysmontick struct {schedtick   uint32schedwhen   int64syscalltick uint32syscallwhen int64
}const forcePreemptNS = 10 * 1000 * 1000 // 抢占的时间阈值 10msfunc retake(now int64) uint32 {n := 0lock(&allpLock)for i := 0; i < len(allp); i++ {_p_ := allp[i]if _p_ == nil {continue}pd := &_p_.sysmonticks := _p_.statusif s == _Prunning || s == _Psyscall {// Preempt G if it's running for too long.t := int64(_p_.schedtick)if int64(pd.schedtick) != t {pd.schedtick = uint32(t)pd.schedwhen = now // 记录当前检测时间// 上次时间加10ms小于当前时间,那么说明超过,需进行抢占。} else if pd.schedwhen+forcePreemptNS <= now {preemptone(_p_)}}// 下面省略掉慢系统调用的抢占描述。if s == _Psyscall {// 原子更为p状态为空闲状态if atomic.Cas(&_p_.status, s, _Pidle) {,,,handoffp(_p_)  // 强制卸载P, 然后startm来关联},,,
} func preemptone(_p_ *p) bool {mp := _p_.m.ptr(),,,gp.preempt = true,,,gp.stackguard0 = stackPreempt// Request an async preemption of this P.if preemptMSupported && debug.asyncpreemptoff == 0 {_p_.preempt = truepreemptM(mp)}return true
}

Go

COPY

发送SIGURG信号?

signal_unix.go

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

Go

COPY

收到sigurg信号后如何处理 ?

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

goschedImpl操作就简单的多,把当前协程的状态从_Grunning正在执行改成 _Grunnable可执行,使用globrunqput方法把抢占的协程放到全局队列里,根据pmg的协程调度设计,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 := getg()gp.asyncSafePoint = trueif gp.preemptStop {mcall(preemptPark)} else {mcall(gopreempt_m)}gp.asyncSafePoint = false
}

Go

COPY

runtime/proc.go

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

Go

COPY

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

抢占是否影响性能 ?

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

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

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

  1. 深度解密Go语言之基于信号的抢占式调度

    不知道大家在实际工作中有没有遇到过老版本 Go 调度器的坑:死循环导致程序"死机".我去年就遇到过,并且搞出了一起 P0 事故,还写了篇弱智的找 bug 文章. 识别事故的本质,并 ...

  2. 深度解密 Go 语言之基于信号的抢占式调度

    作者 | qcrao       责编 | 欧阳姝黎 不知道大家在实际工作中有没有遇到过老版本 Go 调度器的坑:死循环导致程序"死机".我去年就遇到过,并且搞出了一起 P0 事故 ...

  3. 抢占式调度与非抢占式调度

    资料来源 这是本人在操作系统期中考试前复习是碰到的问题,花了一些时间解决,现在记录下来. 引 在学习 CPU 调度的时候,关于抢占式.非抢占式调度方式有不理解的地方,想不到google一下就出来了很好 ...

  4. 【Linux 内核】调度器 ① ( 调度器概念 | 调度器目的 | 调度器主要工作 | 调度器位置 | 进程优先级 | 抢占式调度器 | Linux 进程状态 | Linux 内核进程状态 )

    文章目录 一.调度器 0.调度器概念 1.调度器目的 2.调度器主要工作 3.调度器位置 4.进程优先级 5.抢占式调度器 二.Linux 内核进程状态 API 简介 三.Linux 进程状态 一.调 ...

  5. Linux抢占式调度简介(转)

    原文链接:https://www.cnblogs.com/sjks/p/10888620.html 参考链接:https://blog.csdn.net/wudongxu/article/detail ...

  6. 什么是非抢占式和抢占式调度方式?抢占式调度方法和非抢占式调度方法有哪些?

    非抢占式方式:在采用这种调度方式时,一旦把处理机分配给某进程后,就一直让它运行下去,决不会因为时钟中断或任何其它原因去抢占当前正在运行进程的处理机,直至该进程完成,或发生某事件而被阻塞时,才把处理机分 ...

  7. Java——程序的调度_分时调度模型和抢占式调度模型

    在计算机中,线程调度有两种模型,分别是分时调度模型和抢占式调度模型. 分时调度模型是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU时间片. 抢占式调度模型是指让可运行池中优先级 ...

  8. 关于Go1.14,你一定想知道的性能提升与新特性

    作者:绘你一世倾城 链接:https://juejin.im/post/5e3f9990e51d4526cc3b1672 来源:掘金 著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处 ...

  9. go trace 剖析 go1.14 异步抢占式调度

    转载地址:https://mp.weixin.qq.com/s?__biz=MzAxMTA4Njc0OQ==&mid=2651441259&idx=2&sn=b57987e22 ...

最新文章

  1. Nature:iHMP之“微生物组与炎症性肠病”
  2. iOS自定义控件:简易下拉控件
  3. 又见yx — 说说IT公司的团队头儿
  4. 浅析 PHP 中的 Generator
  5. C11全系产品涨价后 零跑汽车宣布T03全系车型调价
  6. Lua 教程 | 菜鸟教程
  7. 5天学会python_学会Python自动制作Word,将看到一个5天4位数的赚钱机会
  8. CSS基础选择器之类选择器(CSS、HTML)
  9. mysql的配置文件解释
  10. Delphi中Hash表的使用方法!
  11. 阿里云块存储快照服务背后的技术原理
  12. java 字符串 数字个数_Java 求一串字符串中字符,字母,数字的个数
  13. 《从0到1》读书笔记第10章“打造帮派文化”第2记: 如何打造一个优秀创业团队
  14. 一元函数积分学的概念与性质
  15. python秒杀脚本 拼多多_点击劫持漏洞之理解 python打造一个挖掘点击劫持漏洞的脚本...
  16. PXE网络启动 windows PE (使用微软官方工具)
  17. 控件(五)——Gridview控件以SqlDataSource控件为数据源实现换肤功能
  18. 划片机操作安全注意事项
  19. 学习嵌入式Linux开发——RK3288开发板学习规划及目标
  20. 04.配置unp.h头文件出现开启 xinetd daytime 服务时 /etc/xinetd.d下 没有daytime 文件的解决办法

热门文章

  1. 华为鸿蒙ipc时延,虚搜
  2. java集成开发工具项目_Java项目开发(一)-不借助集成工具创建Java项目并编写编译执行脚本...
  3. java相关协议_java相关网络协议是什么
  4. 两个Liunx服务器之间的文件夹迁移
  5. select count(*)和select count(1)
  6. CVE-2015-1642 POC
  7. iOS vuforia 学习钻研(一)
  8. 与时共舞,力求变革【我眼中的戴尔转型】
  9. HTMl文件的阶层架构 访问父元素和子元素
  10. Hadoop集群启动、初体验