longjmp和RAII

10

我有一个库(非我编写),不幸的是它在处理某些错误时使用了 abort()。在应用程序级别上,这些错误是可恢复的,所以我想处理它们,而不是让用户看到崩溃。因此,我最终会编写如下代码:

static jmp_buf abort_buffer;
static void abort_handler(int) {
    longjmp(abort_buffer, 1); // perhaps siglongjmp if available..
}

int function(int x, int y) {

    struct sigaction new_sa;
    struct sigaction old_sa;

    sigemptyset(&new_sa.sa_mask);
    new_sa.sa_handler = abort_handler;
    sigaction(SIGABRT, &new_sa, &old_sa);

    if(setjmp(abort_buffer)) {
        sigaction(SIGABRT, &old_sa, 0);
        return -1
    }

    // attempt to do some work here
    int result = f(x, y); // may call abort!

    sigaction(SIGABRT, &old_sa, 0);
    return result;
}

代码不太优美。由于该模式最终需要在代码的几个位置重复使用,我希望稍微简化一下,并可能将其包装在可重用的对象中。我的第一次尝试涉及使用RAII来处理信号处理程序的设置/拆除(需要这样做是因为每个函数需要不同的错误处理)。所以我想到了这个:

template <int N>
struct signal_guard {
    signal_guard(void (*f)(int)) {
        sigemptyset(&new_sa.sa_mask);
        new_sa.sa_handler = f;
        sigaction(N, &new_sa, &old_sa);
    }

    ~signal_guard() {
        sigaction(N, &old_sa, 0);
    }
private:
    struct sigaction new_sa;
    struct sigaction old_sa;
};


static jmp_buf abort_buffer;
static void abort_handler(int) {
    longjmp(abort_buffer, 1);
}

int function(int x, int y) {
    signal_guard<SIGABRT> sig_guard(abort_handler);

    if(setjmp(abort_buffer)) {
        return -1;
    }

    return f(x, y);
}
当然,这种方式使得function的主体部分更简单、更清晰,但今天早上我想到了一些问题。这种方法是否保证可以正常工作?以下是我的想法:
  1. 在调用setjmp/longjmp期间,没有变量是易失性或发生变化。
  2. 我正在longjmp回到与setjmp相同的堆栈帧中的位置,并以正常方式return,因此我允许代码执行函数出口点处编译器生成的清理代码。
  3. 它似乎按预期工作。
但我仍然觉得这可能是未定义的行为。你们认为呢?

3
为什么你不使用 C++ 异常呢?它可以轻松解决你的问题。 - Hans Passant
1
@Hans - http://h21007.www2.hp.com/portal/site/dspp/menuitem.863c3e4cbcdc3f3515b49c108973a801?ciid=c5080055abe021100055abe02110275d6e10RCRD - Flexo
@Hans:C++ 异常无法捕获 abort 调用。 - Evan Teran
将它们抛到你的abort_handler()中,这没有任何区别。 - Hans Passant
@Hans 在信号处理程序中抛出异常并不能保证在所有平台上都能正常工作,因此本质上是不可移植的。 - Yariv
2个回答

8

我假设f是一个第三方库/应用程序,否则您可以修复它以避免调用abort。鉴于此,并且RAII可能不会在所有平台/编译器上可靠地产生正确的结果,您有几个选项。

  • 创建一个小型共享对象来定义abort并LD_PRELOAD它。然后您可以控制abort时发生的情况,而且不需要使用信号处理程序。
  • 在子进程中运行f。然后您只需检查返回代码,如果失败,请使用更新的输入再次尝试。
  • 不使用RAII,只需从多个调用点调用原始的function,并让它显式地进行设置/拆卸。这仍然可以消除复制粘贴的情况。

+1,我在考虑在我的回答中添加interposing或线程/分叉作为建议。 - Flexo

2

我实际上很喜欢你的解决方案,并且已经编写了类似的测试代码来检查目标函数是否按预期assert()

我看不出这段代码调用未定义行为的任何原因。 C标准似乎给它加持:由abort()产生的处理程序被豁免从处理程序中调用库函数的限制。(警告:这是C99的7.14.1.1(5) - 不幸的是,我没有C++标准引用的版本C90的副本。)

C ++ 03增加了一个进一步的限制:如果在抛出异常并将控制传输到程序中的另一点(目标)时将自动对象销毁,则在将控制传输到相同(目标)点的跳转点处调用longjmp(jbuf,val)的行为是未定义的。 我假设您的陈述“没有变量是易失性的或在setjmp / longjmp之间更改”包括实例化任何自动C ++对象。(我猜这是一些遗留的C库?)

POSIX异步信号安全(或缺乏安全性)也不是问题 - abort()与程序执行同步生成其SIGABRT。

最大的担忧是破坏第三方代码的全局状态:在abort()之前,作者不太可能会费心使状态保持一致。 但是,如果您正确地指出没有变量更改,则这不是问题。

如果有更好理解标准术语的人能证明我是错的,那我会感激启示。


我相当确定没有变量被改变。这是一个C库,只有在被要求执行需要malloc大量空间的操作(与不透明类型的初始化相关)时才会中止。如果失败了,那么我可以考虑该不透明类型未初始化,并尝试使用更合理的值创建一个新的。 - Evan Teran

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接