1.【Go 语言设计与实现】 笔记 — 定时器源码分析
2.Go语言的main 函数是如何被调用的?
3.彻底解决Golang获取当前项目绝对路径问题
4.go源码:Sleep函数与线程
5.如何处理好Golang中的panic与recover
6.Golang源码剖析panic与recover,看不懂你打我好了
【Go 语言设计与实现】 笔记 — 定时器源码分析
本文深入探讨了《Go语言设计与实现》一书中的定时器源码分析,旨在为读者提供关于Go语言中定时器实现的全面理解。阅读过程中,结合源码阅读和资料查阅,补充了书中未详细介绍的spring 启动过程 源码内容,旨在帮助读者巩固对Go语言调度器和定时器核心机制的理解。
在数据结构部分,重点分析了runtime.timer结构体中的pp字段。该字段在书中虽未详细讲解,但在源码中表明了pp代表了定时器在四叉堆中的P(P为调度器的核心组件)位置。深入理解了pp字段对于后续源码解读的重要性。
进一步,分析了time.Timer与NewTimer之间的关联,以及time.NewTimer函数的实现细节。这一过程揭示了时间间隔设置(when)、时间发送(sendTime)和启动定时器(startTimer)之间的逻辑关系,清晰地展示了NewTimer函数的完整工作流程。
状态机部分详细解析了addtimer、deltimer、cleantimers和modtimer等函数的实现。addtimer函数用于将定时器添加至当前P的timer四叉堆中,deltimer负责修改定时器状态,cleantimers用于清除堆顶的定时器,而modtimer则用于修改定时器的多个属性。通过深入分析这些函数的源码,揭示了定时器状态转换的完整流程。
在清除计时器(cleantimers)和调整计时器(adjusttimers)中,讨论了函数如何处理不同状态的定时器,以及如何在调整定时器时保持堆结构的正确性。这些过程展示了Go语言中定时器管理的精细操作。
运行计时器(runtimer)部分,探讨了定时器执行的条件以及如何在没有定时器执行或第一个定时器未执行时处理返回值。这一分析深入理解了定时器执行机制。
最后,文章触及了定时器触发机制与调度器、网络轮询器之间的关系,这部分内容有待进一步整理和补充。文章末尾强调了定时器执行时间误差的来源,并鼓励读者提供反馈,以促进学习和知识共享。
通过本文,读者能够获得对Go语言定时器实现的深入理解,从数据结构、状态转换到执行机制,全面涵盖了定时器的挂机互赞源码核心概念。本文章旨在为读者提供一个全面的资源,帮助在实践中更好地应用Go语言定时器功能。
Go语言的main 函数是如何被调用的?
假设我们有这段程序:
我们可以直接运行:
我们所写的代码是用户空间代码,Go 是通过runtime来管理用户代码的,所以很显然 main 函数只是用户空间代码的入口,而不是一个可执行go二进制文件的入口,毕竟runtime也要做初始化。
go run 的本质其实就是先编译一个可执行文件到临时路径,然后运行。
Go的编译过程包括编译源代码,链接库文件,生成可执行文件:
我们可以通过以下代码来观测这个过程:
稍微解释一下这几个参数:
那么,实际上运行二进制文件的入口在哪里呢? 通过上面输出的信息并不能看到,但是我们可以通过gdb来确定。
首先,我们先自行安装gdb~
安装好之后,在 ~/.gdbinit 中配置:
然后使用gdb调试刚刚编译的二进制文件:
其中elf-x- 是linux可执行文件的格式,可以自行去了解。
从输出可以看到,程序的入口地址是:0x,我们打上断点,并执行程序:
至此,我们找到一个go程序真正的入口。
以上结论只能说明在linux amd下entry point 是 _rt0_amd_linux,实际上不同平台不同架构的入口点是不一样的。
_rt0_amd_linux 是一段汇编代码(runtime/rt0_linux_amd.s):
直接跳转到了 _rt0_amd(runtime/asm_amd.s),接着看:
没什么好说的,我们重点看rt0_go的代码:
搜索mainPC可以得到以下信息:
由此可以得出 runtime·mainPC 这个符号代表的是runtime.main函数(主协程调用runtime.main)。
runtime.main 函数压入栈之后,调用了runtime.newproc()函数:
fn 代表的就是runtime.main,接下来调用了newproc1函数:
newproc1 返回一个和fn绑定的携程,具体fn会在gostartcallfn 处理:
继续看gostartcall(注意,不同的平台此方法实现不一样):
到此,newproc 整个流程就讲完了,但是稍安勿躁,目前所有的准备仅仅是将runtime.main 挂在了newg.sche.pc上,那么什么时候才被调用呢?
我们接着看:
mstart 函数调用了 runtime.mstart0:
继续跟mstart1():
最终到schedule循环,我们暂时会忽略调度寻找gp的逻辑,目前只有主协程:
execute 最后调用gogo函数:
gogo 函数在汇编代码中:
最后的BX也就是在newproc时候绑定的runtime.main函数,JMP BX即运行runtime.main:
到了这一步,我们才算是终于明白了一个go程序到底是从哪里开始运行的,整个流程下来,我们忽略了很多细节,比如goexit函数到底是什么时候调用的、schedule怎么找到待执行的境外独立站源码goroutine,等等。
我们通过gdb调试,明确了go程序的入口,并且通过源码的阅读一步一步的了解到我们编写的main函数又是怎么被执行的。
你有收获吗?如果有,恭喜你,如果没有,非常抱歉,因为本人水平有限不能为你讲解的更加细致。
有问题欢迎留言,水平有限,肯定会有很多错误,欢迎指正。
彻底解决Golang获取当前项目绝对路径问题
由于Golang是编译型语言,获取当前执行目录变得复杂。传统做法是通过启动传参或环境变量手动传递路径,但今天发现了一种更便捷的解决方案。
Go程序有两种执行方式:go run和go build。这两种方式在获取当前执行路径时会产生不同的问题。
下面直接展示代码示例。我们编写一个获取当前可执行文件路径的方法,然后通过go run和go build两种方式来测试。
通过对比执行结果,我们发现go run获取到的路径是错误的。原因是go run会将源代码编译到系统TEMP或TMP环境变量目录中并启动执行,而go build只会在当前目录编译出可执行文件,并不会自动执行。
我们可以简单理解为,go run main.go等价于go build & ./main。虽然两种执行方式最终都是一样的过程,但他们的执行目录却完全不一样了。
在我查看服务日志(zap库)时,发现了一种新的解决方案。比如一条简单的日志,服务是通过go run启动的,但日志库却正确地打印出了程序路径D:/Projects/te-server/modules/es/es.go:。
我发现这是通过runtime.Caller()实现的,而所有Golang日志库都会有runtime.Caller()这个调用。我以为找到了最终答案,然后写代码试了下,结果完全正确!但后来发现,在Linux上运行时,它会打印出Windows的路径,这让我很失望。单峰突破指标源码
我意识到,既然go run时可以通过runtime.Caller()获取到正确的结果,go build时也可以通过os.Executable()来获取到正确的路径;那如果我能判定当前程序是通过go run还是go build执行的,选择不同的路径获取方法,所有问题不就迎刃而解了吗。
Go没有提供接口来区分程序是go run还是go build执行,但我们可以根据go run的执行原理来判断。我们可以直接在程序中对比os.Executable()获取到的路径是否与环境变量TEMP设置的路径相同,如果相同,说明是通过go run启动的,因为当前执行路径是在TEMP目录;不同的话自然是go build的启动方式。
下面是完整代码:
在windows执行
在windows编译后上传到Linux执行
对比结果,我们可以看到,在不同的系统中,不同的执行方式,我们封装的getCurrentAbPath方法最终都输出的正确的结果,perfect!
go源码:Sleep函数与线程
在探索 Go 语言的并发编程中,Sleep 函数与线程的交互方式与 Java 或其他基于线程池的并发模型有所不同。本文将深入分析 Go 语言中 Sleep 函数的实现及其与线程的互动方式,以解答关于 Go 语言中 Sleep 函数与线程关系的问题。
首先,重要的一点是,当一个 goroutine(g)调用 Sleep 函数时,它并不会导致当前线程被挂起。相反,Go 通过特殊的机制来处理这种情景,确保 Sleep 函数的调用不会影响到线程的执行。这一特性是 Go 语言并发模型中独特而关键的部分。
具体来说,当一个 goroutine 调用 Sleep 函数时,它首先将自身信息保存到线程的关键结构体(p)中并挂起。这一过程涉及多个函数调用,包括 `time.Sleep`、`runtime.timeSleep`、`runtime.gopark`、`runtime.mcall`、`runtime.park_m`、`runtime.resetForSleep` 等。最终,该 goroutine 会被放入一个 timer 结构体中,并将其放入到 p 关联的一个最小堆中,从而实现了对当前 goroutine 的保存,同时为调度器提供了切换到其他 goroutine 或 timer 的机会。因此,推箱子源码分析这里的 timer 实际上代表了被 Sleep 挂起的 goroutine,它在睡眠到期后能够及时得到执行。
接下来,我们深入分析 goroutine 的调度过程。当线程 p 需要执行时,它会通过 `runtime.park_m` 函数调用 `schedule` 函数来进行 goroutine 或 timer 的切换。在此过程中,`runtime.findrunnable` 函数会检查线程堆中是否存在已到期的 timer,如果存在,则切换到该 timer 进行执行。如果 timer 堆中没有已到期的 timer,线程会继续检查本地和全局的 goroutine 队列中是否还有待执行的 goroutine,如果队列为空,则线程会尝试“偷取”其他 goroutine 的任务。这一过程包括了检查 timer 堆、偷取其他 p 中的到期 timer 或者普通 goroutine,确保任务能够及时执行。
在“偷取”任务的过程中,线程会优先处理即将到期的 timer,确保这些 timer 的准时执行。如果当前线程正在执行其他任务(如 epoll 网络),则在执行过程中会定期检查 timer 到期情况。如果发现其他线程的 timer 到期时间早于自身,会首先唤醒该线程以处理其 timer,确保不会错过任何到期的 timer。
为了证明当前线程设置的 timer 能够准时执行,本文提出了两种证明方法。第一种方法基于代码细节,重点分析了线程状态的变化和 timer 的执行流程。具体而言,文章中提到的三种线程状态(正常运行、epoll 网络、睡眠)以及相应的 timer 执行情况,表明在 Go 语言中,timer 的执行策略能够确保其准时执行。第二种方法则从全局调度策略的角度出发,强调了 Go 语言中线程策略的设计原则,即至少有一个线程处于“spinning”状态或者所有线程都在执行任务,这保证了 timer 的准时执行。
总之,Go 语言中 Sleep 函数与线程之间的交互方式,通过特殊的线程管理机制,确保了 goroutine 的 Sleep 操作不会阻塞线程,同时保证了 timer 的准时执行。这一机制是 Go 语言并发模型的独特之处,为开发者提供了一种高效且灵活的并发处理方式。
如何处理好Golang中的panic与recover
Go 语言以其高性能和高并发特性而闻名,特别是其提供的 pile -N -l -S main.go就可以看到对应的汇编码了,我们截取部分片段分析:
上面重点部分就是画红线的三处,第一步调用runtime.deferprocStack创建defer对象,这一步大家可能会有疑惑,我上一文忘记讲个这个了,这里先简单概括一下,defer总共有三种模型,编译一个函数里只会有一种defer模式。在讲defer实现机制时,我们一起看过defer的结构,其中有一个字段就是_panic,是触发defer的作用,我们来看看的panic的结构:
简单介绍一下上面的字段:
上面的pc、sp、goexit我们单独讲一下,runtime包中有一个Goexit方法,Goext能够终止调用它的goroutine,其他的goroutine是不受影响的,goexit也会在终止goroutine之前运行所有延迟调用函数,Goexit不是一个panic,所以这些延迟函数中的任何recover调用都将返回nil。如果我们在主函数中调用了Goexit会终止该goroutine但不会返回func main。由于func main没有返回,因此程序将继续执行其他gorountine,直到所有其他goroutine退出,程序才会crash。
下面就开始我们的重点吧~。
在讲defer实现机制时,我们一起看过defer的结构,其中有一个字段就是_panic,是触发defer的作用,我们来看看的panic的结构:简单介绍一下上面的字段:上面的pc、sp、goexit我们单独讲一下,runtime包中有一个Goexit方法,Goext能够终止调用它的goroutine,其他的goroutine是不受影响的,goexit也会在终止goroutine之前运行所有延迟调用函数,Goexit不是一个panic,所以这些延迟函数中的任何recover调用都将返回nil。如果我们在主函数中调用了Goexit会终止该goroutine但不会返回func main。由于func main没有返回,因此程序将继续执行其他gorountine,直到所有其他goroutine退出,程序才会crash。写个简单的例子:运行上面的例子你就会发现,即使在主goroutine中调用了runtime.Goexit,其他goroutine是没有任何影响的。所以结构中的pc、sp、goexit三个字段都是为了修复runtime.Goexit,这三个字段就是为了保证该函数的一定会生效,因为如果在defer中发生panic,那么goexit函数就会被取消,所以才有了这三个字段做保护。看这个例子:
英语好的可以看一看这个: github.com/golang/go/is...,这就是上面的一个例子,这里就不过多解释了,了解就好。
接下来我们再来看一看gopanic方法。
gopanic的代码有点长,我们一点一点来分析:
根据不同的类型判断当前发生panic错误,这里没什么多说的,接着往下看。
上面的代码都是截段,这些部分都是为了判断当前defer是否可以使用开发编码模式,具体怎么操作的就不展开了。
在第三部分进行defer内联优化选择时会执行调用延迟函数(reflectcall就是这个作用),也就是会调用runtime.gorecover把recoverd = true,具体这个函数的操作留在下面讲,因为runtime.gorecover函数并不包含恢复程序的逻辑,程序的恢复是在gopanic中执行的。先看一下代码:
这段代码有点长,主要就是分为两部分:
第一部分主要是这个判断if gp._panic != nil && gp._panic.goexit && gp._panic.aborted { ... },正常recover是会绕过Goexit的,所以为了解决这个,添加了这个判断,这样就可以保证Goexit也会被recover住,这里是通过从runtime._panic中取出了程序计数器pc和栈指针sp并且调用runtime.recovery函数触发goroutine的调度,调度之前会准备好 sp、pc 以及函数的返回值。
第二部分主要是做panic的recover,这也与上面的流程基本差不多,他是从runtime._defer中取出了程序计数器pc和栈指针sp并调用recovery函数触发Goroutine,跳转到recovery函数是通过runtime.call进行的,我们看一下其源码(src/runtime/asm_amd.s 行):
因为go语言中的runtime环境是有自己的堆栈和goroutine,recovery函数也是在runtime环境执行的,所以要调度到m->g0来执行recovery函数,我们在看一下recovery函数:
在recovery 函数中,利用 g 中的两个状态码回溯栈指针 sp 并恢复程序计数器 pc 到调度器中,并调用 gogo 重新调度 g , goroutine 继续执行,recovery在调度过程中会将函数的返回值设置为1。这个有什么作用呢? 在deferproc函数中找到了答案:
当延迟函数中recover了一个panic时,就会返回1,当 runtime.deferproc 函数的返回值是 1 时,编译器生成的代码会直接跳转到调用方函数返回之前并执行 runtime.deferreturn,跳转到runtime.deferturn函数之后,程序就已经从panic恢复了正常的逻辑。
在这里runtime.fatalpanic实现了无法被恢复的程序崩溃,它在中止程序之前会通过 runtime.printpanics 打印出全部的 panic 消息以及调用时传入的参数。
这就是这个逻辑流程,累死我了。。。。
结尾给大家发一个小福利,哈哈,这个福利就是如果避免出现panic,要注意这些:这几个是比较典型的,还有很多会发生panic的地方,交给你们自行学习吧~。
好啦,这篇文章就到这里啦,素质三连(分享、点赞、在看)都是笔者持续创作更多优质内容的动力!
Go 语言一次性定时器使用方式和实现原理
在 Go 语言的标准库time包中,有一个名为Timer的类型,它代表了一个单一事件的计时器,即一次性定时器。
在Go语言的项目开发中,定时器的使用非常普遍。本文将向大家介绍如何在Go语言中使用Timer,以及其背后的实现原理。
要使用Timer一次性定时器,首先需要导入time包。创建Timer的方式有两种:
func NewTimer(d Duration) *Timer
使用func NewTimer创建Timer时,需要传入定时器的等待时间。时间到达时,会向channel中发送当前时间。
示例代码:
通过阅读上面的代码,我们可以看到我们定义了一个2秒后执行的定时器timer,然后使用select读取timer.C中的数据。当读取到数据时,会执行特定的业务逻辑代码。
func AfterFunc(d Duration, f func()) *Timer
使用func AfterFunc创建Timer时,需要传入定时器的等待时间和时间到达时执行的函数。
示例代码:
细心的读者可能已经发现,在代码末尾我们使用了time.Sleep(),这是因为time.AfterFunc()是异步执行的,所以需要等待协程退出。
在Timer的源码中,我们可以看到一个数据结构,它包含两个字段:一个是可导出字段C,这是一个Time类型的channel;另一个是不可导出字段r,这是一个runtimeTimer类型。
实际上,每个Go应用程序底层都会有一个特定的协程来管理Timer。当监控到某个Timer指定的时间到达时,这个协程会将当前时间发送到C中,然后上层读取到C中的数据时,执行相关的业务逻辑代码。
底层协程会监控Timer的r字段中的数据。在源码中查看runtimeTimer的数据结构,我们可以发现其中包含的所有字段。重点了解when、f和arg。
在简单了解Timer的数据结构后,我们查看func NewTimer的代码,可以看到它的实现非常简单。它实际上就是构造了一个Timer,然后把Timer.r传参给startTimer(),除了startTimer()函数外,还有两个函数,分别是when()和sendTime,其中when()是计算计时器的执行时间,sendTime是计时器时间到达时执行的事件(实际上就是将当前时间写入通道中)。
sendTime源码:
我们已经了解到,func NewTimer将构造的Timer.r传参给startTimer(),它负责将runtimeTimer写入底层协程的数组中(如果底层协程未运行,它将会启动底层协程),将Timer交给底层协程监控。也就是说,当底层协程监控到某个Timer指定时间到达时,将当前时间发送到它的通道中。
本文介绍了Go语言标准库time包提供的一次性定时器Timer,不仅介绍了它的使用方式,还介绍了它的实现原理。
限于篇幅,本文没有介绍Stop()和Reset()方法,感兴趣的读者可以查阅相关资料。
go程序是怎样运行起来的?
本文基于 Go1..0 版本详细介绍 Go 语言程序的启动过程。首先,Go 程序启动顺序通常如下:理解 Go 中的 Runtime,分析 Go 的 Runtime 功能,确定程序入口点为 Runtime,深入分析 Runtime 实现,找到对应操作系统的 Go 语言程序启动入口,接着分析 runtime·rt0_go 函数,理解其作用,之后重点分析 runtime·check、runtime·args、runtime·osinit、runtime·schedinit、runtime·newproc 和 runtime·mstart 函数。了解 Go 启动流程需要对 GMP 模型有一定了解。 Go 的启动流程主要包括以下几个关键步骤: 启动 Runtime:理解 Runtime 是 Go 的运行时环境,它包含内存管理、GC、协程和操作系统调用屏蔽等功能。 确定入口点:找到 Runtime 入口,通常位于 go 源码中 src/runtime 目录下的特定文件。 分析 runtime·rt0_go:这是 Go 语言运行时的入口点,负责设置和初始化运行时环境,然后创建 g0 和 m0 来运行程序的主函数。 深入细节:逐步分析 runtime·check、runtime·args、runtime·osinit、runtime·schedinit、runtime·newproc 和 runtime·mstart 函数,了解各自的作用。 全局变量初始化:理解全局变量的初始化主要发生在链接阶段,由编译器或链接器安排。 通过以上步骤,我们全面理解了 Go 语言程序的启动过程,以及启动流程中的关键组件和函数。深入细节分析有助于开发人员更好地掌握 Go 程序的执行机制。