簡體   English   中英

從信號處理程序中拋出異常

[英]Throwing an exception from within a signal handler

我們有一個處理錯誤報告許多方面的庫。 我的任務是將這個庫移植到 Linux。 通過我的小測試套件運行時,其中一個測試失敗了。 測試的簡化版本如下所示。

// Compiler: 4.1.1 20070105 RedHat 4.1.1-52
// Output: Terminate called after throwing an instance of 'int' abort

#include <iostream>
#include <csignal>
using namespace std;

void catch_signal(int signalNumber)
{
    signal(SIGINT, SIG_DFL);
    throw(signalNumber);
}

int test_signal()
{
    signal(SIGINT, catch_signal);

    try
    {
        raise(SIGINT);
    }
    catch (int &z)
    {
        cerr << "Caught exception: " << z << endl;
    }
    return 0;
}

int main()
{
    try
    {
        test_signal();
    }
    catch (int &z)
    {
        cerr << "Caught unexpected exception: " << z << endl;
    }
    return 0;
}

我的期望是會顯示Caught exception:消息。 實際發生的情況是程序終止,因為對於拋出的 int 似乎沒有捕獲處理程序。

有幾個關於 SO 的問題似乎相關。 我發現了許多相關的 Google 頁面。 '智慧'似乎歸結為。

  1. Ya 不能從信號處理程序中拋出異常,因為信號處理程序使用自己的堆棧運行,因此沒有在其上定義處理程序。
  2. Ya 可以從信號處理程序中拋出異常,只需在堆棧上重建一個假幀,就可以了。
  3. 是的,我們一直這樣做。 它在平台 X 上對我有用
  4. 是的,它曾經在 gcc 中可用,但似乎不再起作用了。 嘗試-fnon-call-exceptions選項,也許會奏效

    代碼在我們的 AIX/TRU64/MSVC 編譯器/環境中按預期工作。 它在我們的 Linux 環境中失敗了。


我正在尋找幫助解決此問題的建議,以便 Linux 上的庫行為與我的其他平台相匹配,或者可能實現相同功能的某種類型或變通方法。
讓程序核心轉儲信號,不是一個可行的選擇。

信號與 C++ 異常完全不同。 您不能使用 C++ try/catch 塊來處理信號。 具體來說,信號是 POSIX 概念,而不是 C++ 語言概念。 信號由內核異步傳送到您的應用程序,而 C++ 異常是由 C++ 標准定義的同步事件。

您在 POSIX 信號處理程序中可移植的操作非常有限。 一個常見的策略是有一個sig_atomic_t類型的全局標志,它將在信號處理程序中設置為 1,然后可能longjmp到適當的執行路徑。

有關編寫正確信號處理程序的幫助,請參見此處

此代碼演示了一種將異常的拋出從信號處理程序移到代碼中的技術。 我感謝查爾斯的想法。

#include <iostream>
#include <csignal>
#include <csetjmp>

using namespace std;

jmp_buf gBuffer;        // A buffer to hold info on where to jump to

void catch_signal(int signalNumber)
{
    //signal(SIGINT, SIG_DFL);          // Switch to default handling
    signal(SIGINT, catch_signal);       // Reactivate this handler.

    longjmp             // Jump back into the normal flow of the program
    (
        gBuffer,        // using this context to say where to jump to
        signalNumber    // and passing back the value of the signal.
    );
}


int test_signal()
{
    signal(SIGINT, catch_signal);

    try
    {
        int sig;
        if ((sig = setjmp(gBuffer)) == 0) 
        {
            cout << "before raise\n";
            raise(SIGINT);
            cout << "after raise\n";

        }
        else
        {
            // This path implies that a signal was thrown, and
            // that the setjmp function returned the signal
            // which puts use at this point.

            // Now that we are out of the signal handler it is
            // normally safe to throw what ever sort of exception we want.
            throw(sig);
        }
    }
    catch (int &z)
    {
        cerr << "Caught exception: " << z << endl;
    }

    return 0;
}

int main()
{
    try
    {
        test_signal();
    }
    catch (int &z)
    {
        cerr << "Caught unexpected exception: " << z << endl;
    }
    return 0;
}

我會屏蔽每個線程中的所有信號,除了一個會用sigwait ()等待信號的信號。 該線程可以不受限制地處理信號,例如拋出異常或使用其他通信機制。

拋出信號處理程序可能不是一個好主意,因為堆棧不一定以與函數調用相同的方式設置,因此從信號處理程序中展開可能無法按預期工作。

對於由信號處理機制保存和重用的 C++ ABI 使用的任何寄存器,必須注意重要事項。

谷歌 g++ 選項

-fnon-call-exceptions

這基本上就是你想要的。 我認為這是由於蘋果對其操作系統的壓力而開發的。 我不確定它在 LINUX 上的支持程度。 而且我不確定是否可以捕捉到 SIGINT —— 但是所有 CPU 觸發的信號(aeh 異常)都可以被捕捉到。 需要此功能(並且不關心意識形態)的編碼人員應該對 LINUX 開發人員社區施加一些壓力,以便在 LINUX 上也能支持它——在 Windows 上已經支持了近 20 年之后。

這是一個潛在的解決方案。 實現起來可能相當復雜,當然至少其中一部分需要根據 CPU 架構和操作系統和/或 C 庫組合重新實現:

在信號處理程序中,堆棧包含被中斷代碼的所有寄存器的保存副本。 一旦信號處理程序退出,您可以操縱它來修改程序狀態。 你想在處理程序中做這樣的事情:

1) 在內存中向下移動堆棧的底部(當前堆棧幀、內核保存的 CPU 狀態、處理程序返回內核所需的任何內容)。

2) 在堆棧中間的空閑空間中,創建一個新的堆棧幀,就好像在信號發出時某些“異常調用”函數正在執行一樣。 該幀的布局方式應與中斷代碼以正常方式調用此函數的方式完全相同。

3)修改保存的CPU狀態的PC指向這個“異常調用”函數。

4) 退出信號處理程序。

信號處理程序將返回內核。 內核將返回到這個新的堆棧幀(“異常調用”函數)而不是原始代碼。 這個“異常調用”函數應該簡單地引發你想要引發的任何異常。

這里可能有一些細節; 例如:

1)“異常調用”函數可能需要將一堆通常不會的寄存器保存到堆棧中; 即被中斷的代碼可能一直在使用的所有被調用者保存的寄存器。 您可能需要在匯編中編寫(部分?)“異常調用”函數以在此處提供幫助。 也許上面的第 2 步可以將寄存器保存為設置堆棧幀的一部分。

2) 信號處理程序正在搞亂堆棧。 這會混淆編譯器生成的代碼。 您可能必須在匯編中編寫異常處理程序(或者可能只是它調用的某些函數,這將需要移動更多的堆棧幀)才能完成這項工作。

3) 您可能需要手動生成一些 C++ 異常處理程序展開信息,以便 C++ 異常處理代碼知道如何從這個“異常調用”函數中展開堆棧。 如果您可以用 C++ 編寫函數,則可能不會。 如果你不能,那么幾乎可以肯定。

4)可能是我忽略的各種討厭的細節:-)

至少在 Ubuntu 16.04 x86-64 中,拋出信號處理程序似乎工作正常。 這是否是設計使然(即保證工作,而不是以某種方式意外工作),我還沒有研究過。 我使用g++ -o sig-throw sig-throw.cpp編譯了下面的程序:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

extern "C" void handler(int sig, siginfo_t *info, void *xxx)
{
    throw "Foo";
}

int main(int argc, char **argv)
{
    struct sigaction sa = {0};

    sa.sa_sigaction = handler;
#if 0
    // To ensure SIGALRM doesn't remain blocked once the signal handler raises
    // an exception, either enable the following, or add enable the sigprocmask
    // logic in the exception handler below.
    sa.sa_flags = SA_NODEFER;
#endif
    sigaction(SIGALRM, &sa, NULL);

    alarm(3);

    try {
        printf("Sleeping...\n");
        sleep(10);
        printf("Awoke\n"); // syscall interrupted
    }
    catch (...) {
        printf("Exception!\n");
#if 1
        // To ensure SIGALRM doesn't remain blocked once the signal handler
        // raises an exception, either enable the following, or add enable
        // SA_NODEFER when registering the signal handler.
        sigset_t sigs_alarm;
        sigemptyset(&sigs_alarm);
        sigaddset(&sigs_alarm, SIGALRM);
        sigprocmask(SIG_UNBLOCK, &sigs_alarm, NULL);
#endif
    }

    alarm(3);

    try {
        printf("Sleeping...\n");
        sleep(10);
        printf("Awoke\n"); // syscall interrupted
    }
    catch (...) {
        printf("Exception!\n");
    }

    return 0;
}

這是它運行:

[swarren@swarren-lx1 sig-throw]$ ./sig-throw 
Sleeping...
Exception!

以供參考:

[swarren@swarren-lx1 sig-throw]$ lsb_release -a
...
Description:    Ubuntu 16.04.6 LTS
...

[swarren@swarren-lx1 sig-throw]$ dpkg -l libc6
...
ii  libc6:amd64  2.23-0ubuntu11  amd64  GNU C Library: Shared libraries

[swarren@swarren-lx1 sig-throw]$ g++ --version
g++ (Ubuntu 5.4.0-6ubuntu1~16.04.11) 5.4.0 20160609

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM