【问题标题】:Recover from segfault in Python从 Python 中的段错误中恢复
【发布时间】:2021-02-15 01:04:58
【问题描述】:

我的代码中有一些随机导致SegmentationFault 错误的函数。我通过启用faulthandler 来识别它们。我有点卡住了,不知道如何可靠地消除这个问题。

我正在考虑一些解决方法。由于这些功能随机崩溃,我可能会在失败后重试它们。问题是无法从SegmentationFault 崩溃中恢复。
我现在最好的想法是稍微重写这些函数并通过子进程运行它们。这个解决方案将帮助我,崩溃的函数不会导致整个应用程序崩溃,并且可以重试。

其中一些功能非常小并且经常执行,因此它会显着降低我的应用程序的速度。有没有什么方法可以在单独的上下文中执行函数,比在发生段错误时不会使整个程序崩溃的子进程更快?

【问题讨论】:

  • 也许你可以提前打开备用进程,这样如果/当一个崩溃时下一个立即接管
  • 你的意思是,在进入危险功能之前不断地 fork 应用程序?我不知道在实践中我会如何做到这一点......
  • 我写了很多 Python 并且从未遇到过 SegmentationFault。你做错了什么。显示导致 SegmentationFault 的函数之一
  • 我的代码崩溃主要是因为涉及到一些 C 模块,例如Jinja 模板解析。那里没有什么花哨的。
  • 您在代码中的哪个位置获得SegmentationFault。是的,找到错误可能需要很长时间。将您的代码减少到最低限度并逐行添加,直到您获得 SegmentationFault's。可靠的代码没有 SegmentationFault

标签: python python-3.x python-3.8


【解决方案1】:

tl;dr:您可以使用signalsetjmplongjmp 编写 C 代码。


您有多种选择来处理SIGSEGV

  • 使用subprocess库生成子进程
  • 使用multiprocessing库分叉
  • 编写自定义信号处理程序

子进程和分叉已经描述过了,所以我将重点关注信号处理程序的观点。

编写信号处理程序

从内核的角度来看,SIGSEGVSIGUSR1SIGQUITSIGINT 等任何其他信号之间没有区别。 事实上,一些库(如 JVM)使用它们作为通信方式。

很遗憾,您不能从 python 代码中覆盖信号处理程序。见doc

捕获由 C 代码中的无效操作引起的同步错误(如 SIGFPE 或 SIGSEGV)几乎没有意义。 Python 将从信号处理程序返回到 C 代码,这很可能再次引发相同的信号,导致 Python 明显挂起。从 Python 3.3 开始,您可以使用 faulthandler 模块报告同步错误。

这意味着,错误管理应该在 C 代码中完成。

您可以编写自定义信号处理程序并使用setjmplongjmp 来保存和恢复堆栈上下文。

例如,这是一个简单的 CPython C 扩展:

#include <signal.h>
#include <setjmp.h>

#define PY_SSIZE_T_CLEAN
#include <Python.h>

static jmp_buf jmpctx;

void handle_segv(int signo)
{
    longjmp(jmpctx, 1);
}

static PyObject *
install_sig_handler(PyObject *self, PyObject *args)
{
    signal(SIGSEGV, handle_segv);
    Py_RETURN_TRUE;
}

static PyObject *
trigger_segfault(PyObject *self, PyObject *args)
{
    if (!setjmp(jmpctx))
    {
        // Assign a value to NULL pointer will trigger a seg fault
        int *x = NULL;
        *x = 42;

        Py_RETURN_TRUE; // Will never be called
    }

    Py_RETURN_FALSE;
}

static PyMethodDef SpamMethods[] = {
    {"install_sig_handler", install_sig_handler, METH_VARARGS, "Install SIGSEGV handler"},
    {"trigger_segfault", trigger_segfault, METH_VARARGS, "Trigger a segfault"},
    {NULL, NULL, 0, NULL},
};

static struct PyModuleDef spammodule = {
    PyModuleDef_HEAD_INIT,
    "crash",
    "Crash and recover",
    -1,
    SpamMethods,
};

PyMODINIT_FUNC
PyInit_crash(void)
{
    return PyModule_Create(&spammodule);
}

和调用者应用程序:

import crash

print("Install custom sighandler")
crash.install_sig_handler()

print("bad_func: before")
retval = crash.trigger_segfault()
print("bad_func: after (retval:", retval, ")")

这将产生以下输出:

Install custom sighandler
bad_func: before
bad_func: after (retval: False )

优点和缺点

优点:

  • 从操作系统的角度来看,应用程序只是将SIGSEGV 捕获为常规信号。错误处理会很快。
  • 它不需要分叉(如果您的应用程序包含各种文件描述符、套接字等,则并非总是可能)
  • 它不需要生成子进程(并非总是可行,而且速度慢得多)。

缺点:

  • 可能会导致内存泄漏。
  • 可能会隐藏未定义/危险的行为

请记住,分段错误是一个非常严重的错误! 始终尝试先修复它而不是隐藏它。

很少的链接和参考:

【讨论】:

  • 正如我解释的那样,我只是想找到比分叉/子处理更快的替代解决方案。此方法可用于在原始 python 代码之上创建更复杂的包装器。但我尽量让我的例子尽可能简单。
  • 这里你可以找到一个更复杂的例子,它包装了调用gist.github.com/arthurlm/d358e04781b6308e83be7fcbd1d7e05f
  • 这是一个非常有趣的解决方案,我有点担心未定义的行为,尤其是我的代码使用线程。我试试看。
  • 线程、堆管理、GC 和文件描述符是可能遭受未定义行为影响的主要资源。如果你有一些线程,我建议从 python 端使用 lock 。它将避免有太多复杂的 C 代码。请记住,此方法应该主要用于触发整个应用程序的干净退出/重启。
  • 多处理似乎是最安全的解决方案,但是对于我经常执行且不使用任何外部资源的小功能,此解决方案实际上可能只是重试调用很好。
【解决方案2】:

我有一些不可靠的 C 扩展每隔一段时间就会抛出段错误,因为我无法修复它,我所做的是创建一个装饰器,它将在单独的进程中运行包装的函数.这样你就可以阻止段错误杀死主进程。

类似这样的: https://gist.github.com/joezuntz/e7e7764e5b591ed519cfd488e20311f1

我的有点简单,它为我完成了这项工作。此外,它还允许您选择超时和默认返回值,以防出现问题:

#! /usr/bin/env python3

# std imports
import multiprocessing as mp


def parametrized(dec):
    """This decorator can be used to create other decorators that accept arguments"""

    def layer(*args, **kwargs):
        def repl(f):
            return dec(f, *args, **kwargs)

        return repl

    return layer


@parametrized
def sigsev_guard(fcn, default_value=None, timeout=None):
    """Used as a decorator with arguments.
    The decorated function will be called with its input arguments in another process.

    If the execution lasts longer than *timeout* seconds, it will be considered failed.

    If the execution fails, *default_value* will be returned.
    """

    def _fcn_wrapper(*args, **kwargs):
        q = mp.Queue()
        p = mp.Process(target=lambda q: q.put(fcn(*args, **kwargs)), args=(q,))
        p.start()
        p.join(timeout=timeout)
        exit_code = p.exitcode

        if exit_code == 0:
            return q.get()

        logging.warning('Process did not exit correctly. Exit code: {}'.format(exit_code))
        return default_value

    return _fcn_wrapper

所以你会像这样使用它:


@sigsev_guard(default_value=-1, timeout=60)
def your_risky_function(a,b,c,d):
    ...

【讨论】:

  • 嗯,多进程确实应该比子进程快,不过我不知道它会防止段错误。
  • 我不确定速度(我使用多处理的主要原因是使用他们不错的队列/处理工作流程)。但我可以确认主进程将在子进程中的 SEGFAULT 中存活并返回 default_value
猜你喜欢
  • 2012-01-21
  • 1970-01-01
  • 2022-01-10
  • 1970-01-01
  • 2012-01-14
  • 2017-12-06
  • 2015-02-17
  • 2012-04-09
  • 2016-07-01
相关资源
最近更新 更多