首页 未分类 正文

【5-2 Golang】实战—dlv调试(golang 调度)

未分类 128
  Go程序出异常怎么办?pprof工具分析啊,可是如果是代码方面bug等呢?分析代码bug有时需要结合执行过程,加日志呗,可是某些异常问题服务重启之后,可能会很难复现。这时候我们可以断点调试,这样就能分析每一行代码的执行,每一个变量的结果,C语言通常使用GDB调试,Go语言有专门的调试工具dlv,本篇文章主要介绍dlv的基本使用。  dlv全称delve,安装也比较简单,go install就能安装:

  Go程序出异常怎么办?pprof工具分析啊,可是如果是代码方面bug等呢?分析代码bug有时需要结合执行过程,加日志呗,可是某些异常问题服务重启之后,可能会很难复现。这时候我们可以断点调试,这样就能分析每一行代码的执行,每一个变量的结果,C语言通常使用GDB调试,Go语言有专门的调试工具dlv,本篇文章主要介绍dlv的基本使用。

  dlv全称delve,安装也比较简单,go install就能安装:

  dlv支持多种方式跟踪你的Go程序,help命令查看:

  dlv与GDB还是比较类似的,可打印变量的值,可设置断点,可单步执行,可查看调用栈,另外还可以查看当前Go进程的所有协程、线程等;常用的功能(命令)如下:

  dlv的命令虽然比较多,但是常用的也就几个,一般只要会设置断点,单步执行,输出变量、调用栈等就能满足基本的调试需求。

  我们写一个小程序,通过dlv调试,复习下之前介绍的管道读写,以及调度器流程。注意,Go是多线程/多协程程序,实际执行过程可能比较复杂,而且笔者也省略了部分调试过程,所以即使你完全跟着步骤调试,结果可能也不一样。程序如下:

  编译Go程序并通过dlv启动执行:

  接下来就可以输入上面介绍的诸多调试命令,开启dlv调试之旅了。我们之前已经介绍过管道的实现原理以及Go调度器相关,管道的读写操作实现函数为runtime.chanrecv/runtime.chansend,调度器主逻辑是runtime.schedule;另外,读者需要知道,我们的主协程也就是main函数,编译后对应的函数是main.main。在这几个函数都添加断点。

  continue(简写c)命令执行到断点处:

  =>指向当前执行的代码,第一次竟然执行到了runtime.schedule,没有到main函数?要知道main函数最终也是作为主协程调度执行的,所以main函数肯定不是第一个执行的,调度主协程之前肯定需要线程,创建主协程,执行调度逻辑等等。那Go程序第一行代码应该是什么?我们看一下调用栈:

  Go程序第一行代码在runtime/asm_amd64.s,入口函数是runtime.rt0_go,有兴趣的可以看看,都是汇编代码。接下来,继续c执行到断点,你会发现还是程序还是会执行的暂停到runtime.schedule,甚至是runtime.chanrecv,这是因为在调度主协程之前,还需要做很多初始化工作(有用到这几个函数)。所以我们通常是先设置断点main.main,c执行到这里,再设置其他断点,restart重新执行程序,删除其他断点,重新在main.main设置断点,并continue执行到断点处:

  这下程序终于执行到main.main函数处了,接下来在管道读写函数设置断点,并continue执行到断点处:

  程序执行到了runtime.chansend函数,对应的应该是"queue <- i"这一行代码。bt看看函数栈桢确认下是不是:

  这里我们通过args命令看一下输入参数,block为true说明会阻塞当前协程(如果管道不可写),ep是一个地址,存储待写入数据,x命令可以查看内存,我们看到就是数值0。

  还记得我们之前介绍的管道chan的实现原理吗?底层维护着一个循环队列(有缓冲管道),写数据主要包含这几步逻辑:1)如果管道为nil,阻塞当前协程(block=true);2)如果已关闭,抛出panic异常;3)如果有协程在等待读,直接将数据交给目标协程,并唤醒该协程;4)如果管道还有剩余容量,写数据;4)管道容量已经满了,阻塞当前协程(block=true)。

  接下来可以单步执行,看看管道写操作的执行流程。这一过程比较简单,重复较多,就不再赘述了,我们只列出来单步执行的一个中间过程:

  单步执行过程中,你可能会发现阻塞协程是通过gopark函数将协程换出,切换到调度器循环的。我们在runtime.schedule以及runtime.gopark函数再设置断点,观察协程切换情况:

  runtime.gopark函数主要是切换到调度栈,并执行runtime.schedule调度器(查找可执行协程并调度),所以再次continue会执行到runtime.schedule断点处:

  bt查看调用栈,发现栈底函数是runtime.mcall,调用栈这么短吗?怎么看不到runtime.gopark函数呢?因为这里切换了栈桢,从用户协程栈切换到调度栈,所以调用链路肯定不一样了,是看不到之前用户栈的调用链路的。runtime.mcall函数就是用来切换栈桢的。

  dlv是Go程序调试非常好的工具,不仅可以帮助我们学习理解Go语言,也可以帮助我们快速排查定位程序bug等,一定要熟练掌握。

版权声明 本文地址:https://bdxsqzb.com/?id=213
1.文章若无特殊说明,均属本站原创,若转载文章请于作者联系。
2.本站除部分作品系原创外,其余均来自网络或其它渠道,本站保留其原作者的著作权!如有侵权,请与站长联系!
广告二
扫码二维码