使用多线程处理 SIGTERM 的正确方法

Proper way to handle SIGTERM with multiple threads

我在 Raspberry 上有一个多线程程序,我想在其中处理 SIGTERM 并正常关闭所有内容。问题是我有一个在阻塞套接字上调用 recvfrom() 的后台线程。根据我对手册页的理解,如果我退出我的处理程序,所有系统调用都应该被唤醒,并且 return 和 -1 和 errno 设置为 EINTR。但是在我的情况下,recvfrom 呼叫一直挂起。

1) 总的来说,我的理解是否正确,即所有具有能够被信号唤醒的阻塞系统调用的线程都应该在这种情况下唤醒? 2) 会不会是操作系统在我的thead上设置了一些特殊的信号掩码?

有趣的是我使用的是 VideoCore 基元,而不是 pthread,也许这就是原因?这是一个小测试示例:

#include <iostream>

#include <cstdlib>
#include <cstring>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>


#include "interface/vcos/vcos.h"

void SignalHandler(int nSignalNumber)
{
    std::cout << "received signal " << nSignalNumber << std::endl;
}

void* ThreadMain(void* pArgument)
{
    int nSocket = socket(AF_INET, SOCK_DGRAM, 0);
    if (nSocket >= 0)
    {
        sockaddr_in LocalAddress;
        memset(&LocalAddress, 0, sizeof(LocalAddress));
        LocalAddress.sin_family = AF_INET;
        LocalAddress.sin_addr.s_addr = INADDR_ANY;
        LocalAddress.sin_port = htons(1234);
        if (bind(nSocket, reinterpret_cast<sockaddr *>(&LocalAddress), sizeof(LocalAddress)) == 0)
        {
            sockaddr_in SenderAddress;
            socklen_t nSenderAddressSize = sizeof(SenderAddress);
            unsigned char pBuffer[512];
            std::cout << "calling recvfrom()" << std::endl;
            int nBytesReceived = recvfrom(nSocket, pBuffer, sizeof(pBuffer), 0, reinterpret_cast<struct sockaddr *>(&SenderAddress), &nSenderAddressSize);
            if (nBytesReceived == -1)
            {
                if (errno == EINTR)
                {
                    std::cout << "recvfrom() was interrupred by a signal" << std::endl;
                }
                else
                {
                    std::cout << "recvfrom() failed with " << errno << std::endl;
                }
            }
        }
        else
        {
            std::cout << "bind() failed with " << errno << std::endl;
        }
        close(nSocket);
    }
    else
    {
        std::cout << "socket() failed with " << errno << std::endl;
    }
    return NULL;
}

int main(int argc, char** argv)
{
    struct sigaction SignalAction;
    memset(&SignalAction, 0, sizeof(SignalAction));
    SignalAction.sa_handler = SignalHandler;
    sigaction(SIGTERM, &SignalAction, NULL);
    VCOS_THREAD_T Thread;
    VCOS_STATUS_T nVcosStatus = vcos_thread_create(&Thread, "", NULL, ThreadMain, NULL);
    if (nVcosStatus == VCOS_SUCCESS)
    {
        void* pData = NULL;
        vcos_thread_join(&Thread, &pData);
    }
    else
    {
        std::cout << "vcos_thread_create() failed with " << nVcosStatus << std::endl;
    }
    return EXIT_SUCCESS;
}

可以这样编译:

g++ test.cpp -I/opt/vc/include -L/opt/vc/lib -lvcos  -o test

当我 运行 它然后在 运行ning 实例上调用 kill 时,输出是:

calling recvfrom()
received signal 15

进程挂起。如果 pthread 的行为不同,我会尝试。

更新

好的,我更新了示例以生成一个 pthread 线程,并且那个线程也没有退出。所以我假设信号没有填充到所有线程?

#include <iostream>

#include <cstdlib>
#include <cstring>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>

#include "interface/vcos/vcos.h"

void SignalHandler(int nSignalNumber)
{
    std::cout << "received signal " << nSignalNumber << std::endl;
}

void* ThreadMain(void* pArgument)
{
    const char* pThreadType = reinterpret_cast<const char*>(pArgument);
    int nSocket = socket(AF_INET, SOCK_DGRAM, 0);
    if (nSocket >= 0)
    {
        sockaddr_in LocalAddress;
        memset(&LocalAddress, 0, sizeof(LocalAddress));
        LocalAddress.sin_family = AF_INET;
        LocalAddress.sin_addr.s_addr = INADDR_ANY;
        LocalAddress.sin_port = htons(pThreadType[0] * 100);
        if (bind(nSocket, reinterpret_cast<sockaddr *>(&LocalAddress), sizeof(LocalAddress)) == 0)
        {
            sockaddr_in SenderAddress;
            socklen_t nSenderAddressSize = sizeof(SenderAddress);
            unsigned char pBuffer[512];
            std::cout << "calling recvfrom()" << std::endl;
            int nBytesReceived = recvfrom(nSocket, pBuffer, sizeof(pBuffer), 0, reinterpret_cast<struct sockaddr *>(&SenderAddress), &nSenderAddressSize);
            if (nBytesReceived == -1)
            {
                if (errno == EINTR)
                {
                    std::cout << "recvfrom() was interrupred by a signal" << std::endl;
                }
                else
                {
                    std::cout << "recvfrom() failed with " << errno << std::endl;
                }
            }
        }
        else
        {
            std::cout << "bind() failed with " << errno << std::endl;
        }
        close(nSocket);
    }
    else
    {
        std::cout << "socket() failed with " << errno << std::endl;
    }
    std::cout << pThreadType << " thread is exiting" << std::endl;
    return NULL;
}

int main(int argc, char** argv)
{
    struct sigaction SignalAction;
    memset(&SignalAction, 0, sizeof(SignalAction));
    SignalAction.sa_handler = SignalHandler;
    sigaction(SIGTERM, &SignalAction, NULL);
    VCOS_THREAD_T VcosThread;
    VCOS_STATUS_T nVcosStatus = vcos_thread_create(&VcosThread, "", NULL, ThreadMain, const_cast<char*>("vcos"));
    bool bJoinVcosThread = false;
    if (nVcosStatus == VCOS_SUCCESS)
    {
        bJoinVcosThread = true;
    }
    else
    {
        std::cout << "vcos_thread_create() failed with " << nVcosStatus << std::endl;
    }
    pthread_t PthreadThread;
    int nPthreadStatus = pthread_create(&PthreadThread, NULL, ThreadMain, const_cast<char*>("pthread"));
    bool bJoinPthreadThread = false;
    if (nPthreadStatus == 0)
    {
        bJoinPthreadThread = true;
    }
    else
    {
        std::cout << "pthread_create() failed with " << nPthreadStatus << std::endl;
    }
    if (bJoinVcosThread)
    {
        void* pData = NULL;
        vcos_thread_join(&VcosThread, &pData);
    }
    if (bJoinPthreadThread)
    {
        void* pData = NULL;
        pthread_join(PthreadThread, &pData);
    }
    return EXIT_SUCCESS;
}

signalmanpage 为:

If a signal handler is invoked while a system call or library function call is blocked, then either:

  • the call is automatically restarted after the signal handler returns; or

  • the call fails with the error EINTR.

Which of these two behaviors occurs depends on the interface and whether or not the signal handler was established using the SA_RESTART flag (see sigaction(2)). The details vary across UNIX systems<...>

下面几行,recvfrom 列在默认使用 SA_RESTART 行为的函数中。 (注意:如果套接字超时,此行为将被禁用。)

因此,您应该填写 sigaction 结构的 sa_flags 字段以小心 避免 设置 SA_RESTART 标志。

诸如SIGTERM的信号仅提交给进程中的一个线程。唯一的前提是所选线程必须没有屏蔽信号,或者必须使用 sigwait 等待它。不会直接通知其他线程信号已经送达。

将信号与线程相结合的一种常见方法是拥有一个单独的线程,该线程仅处理信号并使用线程同步机制(例如条件变量)通知其他线程。

对于中断文件 I/O,这可能还不够,因为在检查终止请求和进行系统调用以执行 I/O 操作之间存在竞争条件。一些语言 运行-time 库使用非阻塞 I/O 和 pollepoll 和一个特殊的文件描述符,它在信号传递时就绪(使用前面提到的线程-based 方法,或某些 Linux-特定的方法,如 signalfd)。其他人试图通过使用 readwrite 系统调用直接使用复杂的舞蹈来避免这种开销,该舞蹈使用 dup2 将文件描述符替换为总是导致 I/O 的文件描述符失败,从而避免竞争条件(但为此所需的簿记相当复杂)。

处理阻塞套接字的好方法-请参阅socket(7)- (and even non blocking ones) is to use a multiplexing syscall like poll(2) (or the obsolete select(2)...)

关于信号,请务必阅读signal(7) and signal-safety(7)

用一些 event loop (using poll(2)) is to have a signal handler which simply write(2)-s a byte on a pipe(7) to self (you'll setup the pipe at initialization, and you'll poll it in your event loop). The Qt documentation explains how and why. You might also use the Linux specific signalfd(2).

处理信号的常用方法