调试Go语言的核心转储(Core Dumps)英文原文链接【Go, the unwritten parts】发表于2017/05/22 作者JBD是Go语言开发小组成员
检查程序的执行路径和当前状态是非常有用的调试手段 。核心文件(core file)包含了一个运行进程的内存转储和状态 。它主要是用来作为事后调试程序用的 。它也可以被用来查看一个运行中的程序的状态 。这两个使用场景使调试文件转储成为一个非常好的诊断手段 。我们可以用这个方法来做事后诊断和分析线上的服务(production services) 。
在这篇文章中go语言的核心gmp , 我们将用一个简单的hello world网站服务作为例子 。在现实中go语言的核心gmp,我们的程序很容易就会变得很复杂 。分析核心转储给我们提供了一个机会去重构程序的状态并且查看只有在某些条件/环境下才能重现的案例 。
作者注 : 这个调试流程只在Linux上可行 。我不是很确定它是否在其它Unixs系统上工作 。macOS对此还不支持 。Windows现在也不支持 。
在我们开始前go语言的核心gmp,需要确保核心转储的ulimit设置在合适的范围 。它的缺省值是0,意味着最大的核心文件大小是0 。我通常在我的开发机器上将它设置成unlimited 。使用以下命令go语言的核心gmp:
接下来,你需要在你的机器上安装 delve。
下面我们使用的 main.go 文件 。它注册了一个简单的请求处理函数(handler)然后启动了HTTP服务 。
让我们编译并生产二进制文件 。
现在让我们假设 , 这个服务器出了些问题,但是我们并不是很确定问题的根源 。你可能已经在程序里加了很多辅助信息,但还是无法从这些调试信息中找出线索 。通常在这种情况下,当前进程的快照会非常有用 。我们可以用这个快照深入查看程序的当前状态 。
有几个方式来获取核心文件 。你可能已经熟悉了奔溃转储(crash dumps) 。它们是在一个程序奔溃的时候写入磁盘的核心转储 。Go语言在缺省设置下不会生产奔溃转储 。但是当你把 GOTRACEBACK 环境变量设置成“crash”,你就可以用 Ctrl backslash 才触发奔溃转储 。如下图所示:
上面的操作会使程序终止,将堆栈跟踪(stack trace)打印出来,并把核心转储文件写入磁盘 。
另外个方法可以从一个运行的程序获得核心转储而不需要终止相应的进程 。gcore 可以生产核心文件而无需使运行中的程序退出 。
根据上面的操作 , 我们获得了转储而没有终止对应的进程 。下一步就是把核心文件加载进delve并开始分析 。
差不多就这些 。delve的常用操作都可以使用 。你可以backtrace,list,查看变量等等 。有些功能不可用因为我们使用的核心转储是一个快照而不是正在运行的进程 。但是程序执行路径和状态全部可以访问 。
为什么要使用 Go 语言?Go 语言的优势在哪里1. 保留但大幅度简化指针
Go语言保留着C中值和指针的区别,但是对于指针繁琐用法进行了大量的简化,引入引用的概念 。所以在Go语言中 , 你几乎不用担心会因为直接操作内寸而引起各式各样的错误 。
2. 多参数返回
还记得在C里面为了回馈多个参数,不得不开辟几段指针传到目标函数中让其操作么?在Go里面这是完全不必要的 。而且多参数的支持让Go无需使用繁琐的exceptions体系 , 一个函数可以返回期待的返回值加上error , 调用函数后立刻处理错误信息,清晰明了 。
3. Array,slice,map等内置基本数据结构
如果你习惯了Python中简洁的list和dict操作,在Go语言中,你不会感到孤单 。一切都是那么熟悉,而且更加高效 。如果你是C程序员,你会发现你又找到了STL的vector 和 map这对朋友 。
4. Interface
Go语言最让人赞叹不易的特性,就是interface的设计 。任何数据结构,只要实现了interface所定义的函数,自动就implement了这个interface,没有像Java那样冗长的class申明,提供了灵活太多的设计度和OO抽象度,让你的代码也非常干净 。千万不要以为你习惯了Java那种一条一条加implements的方式 , 感觉还行,等接口的设计越来越复杂的时候,无数Bug正在后面等着你 。
同时,正因为如此,Go语言的interface可以用来表示任何generic的东西,比如一个空的interface,可以是string可以是int,可以是任何数据类型 , 因为这些数据类型都不需要实现任何函数,自然就满足空interface的定义了 。加上Go语言的type assertion,可以提供一般动态语言才有的duck typing特性,而仍然能在compile中捕捉明显的错误 。
5. OO
Go语言本质上不是面向对象语言,它还是过程化的 。但是,在Go语言中,你可以很轻易的做大部分你在别的OO语言中能做的事,用更简单清晰的逻辑 。是的,在这里,不需要class,仍然可以继承,仍然可以多态,但是速度却快得多 。因为本质上 , OO在Go语言中,就是普通的struct操作 。
6. Goroutine
这个几乎算是Go语言的招牌特性之一了,我也不想多提 。如果你完全不了解Goroutine,那么你只需要知道,这玩意是超级轻量级的类似线程的东西,但通过它,你不需要复杂的线程操作锁操作 , 不需要care调度 , 就能玩转基本的并行程序 。在Go语言里,触发一个routine和erlang spawn一样简单 。基本上要掌握Go语言 , 以Goroutine和channel为核心的内存模型是必须要懂的 。不过请放心,真的非常简单 。
7. 更多现代的特性
和C比较,Go语言完全就是一门现代化语言,原生支持的Unicode, garbage collection, Closures(是的,和functional programming language类似), function是first class object,等等等等 。
看到这里,你可能会发现 , 我用了很多轻易,简单,快速之类的形容词来形容Go语言的特点 。我想说的是,一点都不夸张,连Go语言的入门学习到提高,都比别的语言门槛低太多太多 。在大部分人都有C的背景的时代,对于Go语言 , 从入门到能够上手做项目,最多不过半个月 。Go语言给人的感觉就是太直接了,什么都直接,读源代码直接,写自己的代码也直接 。
【golang详解】go语言GMP(GPM)原理和调度Goroutine调度是一个很复杂的机制,下面尝试用简单的语言描述一下Goroutine调度机制,想要对其有更深入的了解可以去研读一下源码 。
首先介绍一下GMP什么意思:
G ----------- goroutine: 即Go协程 , 每个go关键字都会创建一个协程 。
M ---------- thread内核级线程,所有的G都要放在M上才能运行 。
P ----------- processor处理器,调度G到M上,其维护了一个队列 , 存储了所有需要它来调度的G 。
Goroutine 调度器P和 OS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行
【go语言的核心gmp go语言的作用】 模型图:
避免频繁的创建、销毁线程,而是对线程的复用 。
1)work stealing机制
当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程 。
2)hand off机制
当本线程M0因为G0进行系统调用阻塞时,线程释放绑定的P , 把P转移给其他空闲的线程执行 。进而某个空闲的M1获取P,继续执行P队列中剩下的G 。而M0由于陷入系统调用而进被阻塞,M1接替M0的工作,只要P不空闲,就可以保证充分利用CPU 。M1的来源有可能是M的缓存池,也可能是新建的 。当G0系统调用结束后 , 根据M0是否能获取到P,将会将G0做不同的处理:
如果有空闲的P,则获取一个P , 继续执行G0 。
如果没有空闲的P,则将G0放入全局队列,等待被其他的P调度 。然后M0将进入缓存池睡眠 。
如下图
GOMAXPROCS设置P的数量 , 最多有GOMAXPROCS个线程分布在多个CPU上同时运行
在Go中一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死 。
具体可以去看另一篇文章
【Golang详解】go语言调度机制 抢占式调度
当创建一个新的G之后优先加入本地队列,如果本地队列满了 , 会将本地队列的G移动到全局队列里面,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G 。
协程经历过程
我们创建一个协程 go func()经历过程如下图:
说明:
这里有两个存储G的队列,一个是局部调度器P的本地队列、一个是全局G队列 。新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中;处理器本地队列是一个使用数组构成的环形链表 , 它最多可以存储 256 个待执行任务 。
G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系 。M会从P的本地队列弹出一个可执行状态的G来执行 , 如果P的本地队列为空,就会想其他的MP组合偷取一个可执行的G来执行;
一个M调度G执行的过程是一个循环机制;会一直从本地队列或全局队列中获取G
上面说到P的个数默认等于CPU核数,每个M必须持有一个P才可以执行G,一般情况下M的个数会略大于P的个数 , 这多出来的M将会在G产生系统调用时发挥作用 。类似线程池,Go也提供一个M的池子,需要时从池子中获取 , 用完放回池子,不够用时就再创建一个 。
work-stealing调度算法:当M执行完了当前P的本地队列队列里的所有G后,P也不会就这么在那躺尸啥都不干 , 它会先尝试从全局队列队列寻找G来执行 , 如果全局队列为空,它会随机挑选另外一个P,从它的队列里中拿走一半的G到自己的队列中执行 。
如果一切正常 , 调度器会以上述的那种方式顺畅地运行,但这个世界没这么美好,总有意外发生,以下分析goroutine在两种例外情况下的行为 。
Go runtime会在下面的goroutine被阻塞的情况下运行另外一个goroutine:
用户态阻塞/唤醒
当goroutine因为channel操作或者network I/O而阻塞时(实际上golang已经用netpoller实现了goroutine网络I/O阻塞不会导致M被阻塞,仅阻塞G,这里仅仅是举个栗子) , 对应的G会被放置到某个wait队列(如channel的waitq),该G的状态由_Gruning变为_Gwaitting , 而M会跳过该G尝试获取并执行下一个G,如果此时没有可运行的G供M运行 , 那么M将解绑P , 并进入sleep状态;当阻塞的G被另一端的G2唤醒时(比如channel的可读/写通知) , G被标记为,尝试加入G2所在P的runnext(runnext是线程下一个需要执行的 Goroutine 。),然后再是P的本地队列和全局队列 。
系统调用阻塞
当M执行某一个G时候如果发生了阻塞操作 , M会阻塞,如果当前有一些G在执行,调度器会把这个线程M从P中摘除,然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P 。当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列 。如果获取不到P , 那么这个线程M变成休眠状态, 加入到空闲线程中 , 然后这个G会被放入全局队列中 。
队列轮转
可见每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度 。
除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G 。之所以P会周期性地查看全局队列 , 也是为了防止全局队列中的G被饿死 。
除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G 。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死 。
M0
M0是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量rutime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G,在之后M0就和其他的M一样了
G0
G0是每次启动一个M都会第一个创建的goroutine,G0仅用于负责调度G , G0不指向任何可执行的函数 , 每个M都会有一个自己的G0,在调度或系统调用时会使用G0的栈空间,全局变量的G0是M0的G0
一个G由于调度被中断,此后如何恢复?
中断的时候将寄存器里的栈信息,保存到自己的G对象里面 。当再次轮到自己执行时,将自己保存的栈信息复制到寄存器里面,这样就接着上次之后运行了 。
我这里只是根据自己的理解进行了简单的介绍,想要详细了解有关GMP的底层原理可以去看Go调度器 G-P-M 模型的设计者的文档或直接看源码
参考:()
()
golang的线程模型——GMP模型内核线程(Kernel-Level Thread,KLT)
轻量级进程(Light Weight Process,LWP):轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持 , 因此只有先支持内核线程,才能有轻量级进程
用户线程与系统线程一一对应,用户线程执行如lo操作的系统调用时 , 来回切换操作开销相对比较大
多个用户线程对应一个内核线程,当内核线程对应的一个用户线程被阻塞挂起时候,其他用户线程也阻塞不能执行了 。
多对多模型是可以充分利用多核CPU提升运行效能的
go线程模型包含三个概念:内核线程(M),goroutine(G),G的上下文环境(P);
GMP模型是goalng特有的 。
P与M一般是一一对应的 。P(上下文)管理着一组G(goroutine)挂载在M(内核线程)上运行,图中左边蓝色为正在执行状态的goroutine,右边为待执行状态的goroutiine队列 。P的数量由环境变量GOMAXPROCS的值或程序运行runtime.GOMAXPROCS()进行设置 。
当一个os线程在执行M1一个G1发生阻塞时,调度器让M1抛弃P,等待G1返回,然后另起一个M2接收P来执行剩下的goroutine队列(G2、G3...) , 这是golang调度器厉害的地方,可以保证有足够的线程来运行剩下所有的goroutine 。
当G1结束后,M1会重新拿回P来完成,如果拿不到就丢到全局runqueue中,然后自己放到线程池或转入休眠状态 。空闲的上下文P会周期性的检查全局runqueue上的goroutine,并且执行它 。
另一种情况就是当有些P1太闲而其他P2很忙碌的时候,会从其他上下文P2拿一些G来执行 。
详细可以翻看下方第一个参考链接,写得真好 。
最后用大佬的总结来做最后的收尾————
Go语言运行时,通过核心元素G,M,P 和 自己的调度器,实现了自己的并发线程模型 。调度器通过对G,M,P的调度实现了两级线程模型中操作系统内核之外的调度任务 。整个调度过程中会在多种时机去触发最核心的步骤 “一整轮调度” , 而一整轮调度中最关键的部分在“全力查找可运行G”,它保证了M的高效运行(换句话说就是充分使用了计算机的物理资源) , 一整轮调度中还会涉及到M的启用停止 。最后别忘了,还有一个与Go程序生命周期相同的系统监测任务来进行一些辅助性的工作 。
浅析Golang的线程模型与调度器
Golang CSP并发模型
Golang线程模型
关于go语言的核心gmp和go语言的作用的介绍到此就结束了,不知道你从中找到你需要的信息了吗 ?如果你还想了解更多这方面的信息,记得收藏关注本站 。
推荐阅读
- 新媒体如何生产话题,新媒体如何做
- 沙河经营类游戏,沙河游戏有什么
- gis技能大赛技术路线图,gis比赛内容
- go语言切片传参 go语言切片和数组
- 无锡java软件开发代码,无锡java软件培训班
- flutter加载本地图片,flutter图片加载glide
- 电脑如何同步抖音直播记录,手机抖音直播,电脑可以同步操作吗
- c语言中fun函数功能 c语言中func函数用法
- 公众号怎么联系管理,公众号怎么进入管理