使用pdb附加进程

77 投票
5 回答
107230 浏览
提问于 2025-04-18 17:17

我有一个Python脚本,我怀疑里面有死锁的问题。我尝试用pdb来调试,但如果我一步一步地走,它并不会出现死锁。从输出结果来看,它并不是在同一次循环中卡住的。我想知道有没有办法只在脚本被锁住的时候把它连接到调试器上?如果需要的话,我也愿意使用其他调试工具。

5 个回答

5

使用 pyrasite

>>> pyrasite 172483 dump_stacks.py

...这里的 172483 是正在运行的 Python 进程的 PID。这个 Python 进程会为每个线程打印出一个堆栈跟踪信息。你可以发送任意的 Python 代码来执行,或者打开一个命令行界面。

这个工具非常适合用来调试死锁问题。即使在程序卡住后,你也可以安装 pyrasite。需要注意的是,你必须在相同的环境中安装它才能正常工作。

这并不是唯一的工具,但不知为何,似乎很难偶然发现它。虽然它比较老旧,但在 Python 2 和 3 中都能很好地工作。

这个工具可能不支持 win32,因为大多数注入工具使用的是 Unix 头文件来调用本地 C 函数,具体可以参考 这个未解决的问题

7

VSCode支持调试本地运行的Python程序。

如果你没有launch.json文件,只需按下调试按钮(F5),然后你会看到以下选项。

在这里输入图片描述

选择“使用进程ID附加”会在你的launch.json文件中添加以下内容:

  {
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Python: Attach using Process Id",
      "type": "python",
      "request": "attach",
      "processId": "${command:pickProcess}",
    },
}

现在,当你使用这个配置进行调试时,可以选择你想要调试的本地Python进程。

在这里输入图片描述

12

你可以使用我的项目 madbg。这是一个Python调试工具,可以让你连接到正在运行的Python程序,并在你当前的终端中进行调试。它和 pyrasitepyringe 类似,但支持Python3,不需要使用gdb,并且使用 IPython 作为调试器(这意味着它有颜色显示和自动补全功能)。

比如说,如果你想查看你的脚本卡在哪里,你可以运行:

madbg attach <pid>

然后在调试器的命令行中输入: bt

16

有一个叫做 pdb-clone 的工具,它是 pdb 的一个克隆版,名字起得很有创意。这个工具可以 连接到正在运行的程序

使用方法很简单,你只需要在主程序的代码里加上 from pdb_clone import pdbhandler; pdbhandler.register(),然后就可以用 pdb-attach --kill --pid PID 来启动 pdb。

77

目前,pdb 还不能在一个正在运行的程序上暂停并开始调试。不过,你还有其他一些选择:

GDB

你可以使用 GDB 来进行 C 级别的调试。这有点抽象,因为你是在查看 Python 的 C 源代码,而不是你实际的 Python 脚本,但在某些情况下还是很有用的。具体的使用说明可以在这里找到:https://wiki.python.org/moin/DebuggingWithGdb。这里的内容比较复杂,不能简单总结。

第三方扩展和模块

只需在网上搜索“pdb attach process”,你就能找到一些项目来为 PDB 提供这个功能:
Pyringe: https://github.com/google/pyringe
Pycharm: https://blog.jetbrains.com/pycharm/2015/02/feature-spotlight-python-debugger-and-attach-to-process/
Python 维基的这个页面有几个替代方案:https://wiki.python.org/moin/PythonDebuggingTools


针对你的具体情况,我有一些解决方法的想法:

信号

如果你使用的是 Unix 系统,可以使用信号,就像在这篇博客中提到的那样,尝试暂停并连接到一个正在运行的脚本。

以下内容直接摘自链接的博客:

当然,pdb 已经有函数可以在程序中间启动调试器,最著名的就是 pdb.set_trace()。不过,这需要你知道想从哪里开始调试,而且也意味着你不能把它留在生产代码中。

但我一直很羡慕 GDB 的功能:可以直接中断一个正在运行的程序并开始调试。这在某些情况下非常方便,比如你卡在一个循环里想要调查一下。今天我突然想到:只需注册一个信号处理器来设置跟踪函数!以下是概念验证代码:

import os
import signal
import sys
import time    

def handle_pdb(sig, frame):
    import pdb
    pdb.Pdb().set_trace(frame)    

def loop():
    while True:
        x = 'foo'
        time.sleep(0.2)

if __name__ == '__main__':
    signal.signal(signal.SIGUSR1, handle_pdb)
    print(os.getpid())
    loop()

现在我可以向正在运行的应用程序发送 SIGUSR1 信号并获得调试器。太好了!

我想你可以通过使用 Winpdb 来增强这个功能,以便在你的应用程序不再连接到终端时进行远程调试。上述代码的另一个问题是,在调用 pdb 后似乎无法恢复程序,退出 pdb 后你只会得到一个回溯信息就结束了(但由于这只是 bdb 引发的 bdb.BdbQuit 异常,我想这可以通过几种方式解决)。最后一个直接的问题是在 Windows 上运行这个,我对 Windows 了解不多,但我知道它们没有信号,所以我不确定你该如何在那儿做到这一点。

条件断点和循环

如果没有信号可用,你仍然可以使用 PDB,方法是将你的锁或信号量获取放在一个循环中,循环中增加一个计数器,只有当计数器达到一个非常大的数字时才暂停。例如,假设你有一个你怀疑是死锁部分的锁:

lock.acquire() # some lock or semaphore from threading or multiprocessing

可以这样重写:

count = 0
while not lock.acquire(False): # Start a loop that will be infinite if deadlocked
    count += 1

    continue # now set a conditional breakpoint here in PDB that will only trigger when
             # count is a ridiculously large number:
             # pdb> <filename:linenumber>, count=9999999999

当计数器非常大时,断点应该触发,(希望)表明在这里发生了死锁。如果你发现它在锁定对象似乎没有死锁的情况下触发,那么你可能需要在循环中插入一个短暂的时间延迟,这样它就不会增加得太快。你也可能需要调整断点的触发阈值,以便在正确的时间触发。我的例子中的数字是随意的。

另一种变体是不使用 PDB,而是在计数器变得很大时故意引发一个异常,而不是触发断点。如果你自己编写一个异常类,可以将所有本地信号量/锁的状态打包到异常中,然后在脚本的顶层捕获它,以便在退出之前打印出来。

文件指示器

另一种在不依赖于计数器的情况下使用死锁循环的方法是写入文件:

import time

while not lock.acquire(False): # Start a loop that will be infinite if deadlocked
    with open('checkpoint_a.txt', 'a') as fo: # open a unique filename
        fo.write("\nHit") # write indicator to file
        time.sleep(3)     # pause for a moment so the file size doesn't explode

现在让你的程序运行一两分钟。杀掉程序,然后查看那些“检查点”文件。如果死锁导致你的程序停滞,那么那些文件中写有“hit”字样的地方会指示哪些锁的获取导致了你的死锁。

你可以通过让循环打印变量或其他状态信息,而不仅仅是一个常量,来扩展这个方法的实用性。例如,你说你怀疑死锁发生在一个循环中,但不知道它处于哪个迭代。让这个锁循环转储你的循环控制变量或其他状态信息,以识别死锁发生的迭代。

撰写回答