前言

不知各位有木有遇到这样的情况,生产环境下的Python进程突然卡死了,所有其他线程都无法调度,如果我们kill掉重启,通常会丧失掉当前报错的上下文信息,失去这些信息,对后续报错定位不太友好,能不能在不关停Python程序的情况下,看看程序发生了什么问题?

首先,你可以尝试一下,此前我提过的py-spy(漫画:如何分析运行中的 Python 程序?),py-spy可以打印出简单的调用信息,但很多时候不够用,这里我们通过gdb来调试Python程序,同时打印c栈和py栈的信息,调试起来,一目了然。

gdb主要用于调试c/c++程序的,因为Python是使用c写的(cpython,当然还有其他语言实现的),对gdb而言,将python当成普通的c程序调试则可。

当我们使用gdb调试python进程时,我们会获得解释器级别的调试信息和内存状态,而不是应用程序级别的(我们使用pdb便是应用程序基本的),这样我们可以看到Python完整的执行流程,包括解释器上函数与变量的信息。

我的服务器是Ubuntu 20.04,所以本文的操作都在Ubuntu 20.04上进行。

安装gdb调试环境

首先,需要安装一下gdb。

sudo apt-get install gdb

光安装gdb,在调试python时,虽然可以用,但不太友好,为了更好的浏览调试信息,我们为cpython安装gdb debugging Symbols。

gdb中的debugging Symbols的主要作用是将程序编译后的二进制指令映射到源代码相应的变量、函数和行中,这样在调试时可以很好的浏览调试信息。

在Ubuntu中,安装python-dbg则可。

sudo apt-get install python-dbg

python-dbg提供了gdb Debugging Symbols和一些调试python的命令,如py-bt、py-list,本文后续会使用。

python-dbg之所以可以发挥作用,是因为它将python gdb相关的内容自动复制到gdb auto-load目录下了,这样gdb在启动时,会自动加载这些内容。

e24bcb5caf47742eb36567a5d9c07d4a.png

你可以在进入gdb交互命令后,通过info auto-load来查看相关信息。

e0c65992d41ffbddf7431315920ae31a.png

调试Python程序

安装好gdb调试环境后,准备一段代码,简单使用一下gdb。

0e4efae15c3e17b0b8a3239e7364356f.png

我在当前用户目录下,创建了play_gdb目录并在其中创建了虚拟环境,然后在目录中创建了play_gdb.py,里面就是一段简单的计算斐波那契数列的代码。

在日常开发中,我们经常使用python虚拟环境,所以这里想测试一下,如果我们使用python虚拟环境运行程序时,gdb是否可以正常调试。

在使用python venv前,先通过系统的python来试试,不进入虚拟环境,直接运行。

2db4b68b1284f89a853d55171f225051.png

从上图可知,我们使用系统python运行程序,开启新窗口,通过gdb调试一下,如下图:

5439d320e75f7d26282520aefafac2af.png

上图命令为:

ps -x | grep python
sudo gdb -p 1199469

先找到pid,然后再让gdb直接附加到运行中的python进程上。

通过bt命令,查一下当前程序的调用栈。

d0e8c2626e040438e8ebe725682cb909.png

从上图可以看出,有很多python解释器级别的打印,我们看到程序目前在python的timemodule.c的pysleep方法中,最终调用了linux系统的select.c(即通过I/O复用相关的逻辑来实现python进程中主线程的sleep)。

gdb的bt命令可以将c调用栈完整打印出来,如果我们只想看python调用栈,可以使用py-bt(你需要安装python-dbg才能用),此外,如果想查看当前程序的py代码,可以使用py-list(等价pdb的ll命令),如下图:

d184b9d0c4324bbf409405d8af72b43f.png

从上图看,py-bt和py-list都没有正常运行,这是因为我们使用gdb时,没有在当前项目的根目录,通过q命令退出一下,然后进入项目根目录,再用gdb开启调试。

f9a66363f0a1e7577f47447bd1418408.pngaa507686446d75732c38bb1441d313b5.png

简单列一下gdb调试时的常用命令:

bt    # 当前C调用栈
py-bt  # 当前Py调用栈
py-list  # 当前py代码位置
py-up  # 上一帧(py级别的帧)
py-down  # 下一帧(py级别的帧)
info thread   # 线程信息
thread <id>   # 切换到某个线程
thread apply all py-list  # 查看所有线程的py代码位置
ctrl-c  # 中断

更多用法可以看pthon官方关于gdb的文档:https://devguide.python.org/gdb/

如果利用python虚拟环境中的python解释器来执行py程序,gdb也可以正常调试,没有什么使用上的差异。

美化gdb调试信息

通过gdb-dashboard(https://github.com/cyrus-and/gdb-dashboard)可以美化gdb调试信息,从而增加效率。

从gdb7开始,gdb便支持使用Python代码来扩展gdb,gdb-dashboard的原理正是如此。

在当前使用gdb的用户目录下,下载gdb-dashboard提供的.gdbinit文件,你可以直接从github中拉取,然后复制到相应的用户目录下。

然后你再安装一下pygments,用于启用语法高亮显示的效果(选择性安装):

pip install pygments

因为我是通过root用户来使用gdb的,所以需要将.gdbinit放置在root用户目录下。

d66dea8ea83404ba7b0cab8e5af60348.png

然后正常使用gdb,会获得下图效果。

4ef728a75b645140248b7bbfcd8a2a31.png

上图中,可以很直观地看到调用栈(Stack)、变量(Variables)、寄存器(Registers)等信息,都是c层面的。

在gdb中,通过help dashboard可以查看dashboard更多的用法。

2c680ca76281d478d7ce5eaefca62a43.png

结尾

掌握gdb调试,可以更好地理解python源码,对于一些比较难搞的线上情况,也更加游刃有余,学起来呗。

参考

  • 使用gdb调试CPython进程 (https://github.com/ictar/python-doc/blob/master/Others/%E4%BD%BF%E7%94%A8gdb%E8%B0%83%E8%AF%95CPython%E8%BF%9B%E7%A8%8B.md)

  • gdb调试cpython(https://meteorix.github.io/2019/02/13/gdbpython/)

  • 来源:懒编程-二两

    物联沃分享整理
    物联沃-IOTWORD物联网 » 使用gdb调试Python程序

    发表评论