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

Proper way to handle SIGTERM with multiple threads

本文关键字:方法 SIGTERM 处理 线程      更新时间:2023-10-16

我在Raspberry上有一个多线程程序,我想在其中处理SIGTERM并优雅地关闭一切。问题是我有一个后台线程在阻塞套接字上调用了recvfrom()。根据我从手册页中的理解,如果我退出我的处理程序,所有的系统调用都应该被唤醒,并返回-1和errno设置为EINTR。然而,在我的情况下,recvfrom呼叫一直处于挂起状态。

1) 总的来说,我理解得对吗?在这种情况下,所有有阻塞系统调用的线程都应该被信号唤醒?2) 可能是操作系统在我的电脑上设置了一些特殊的信号屏蔽吗?

有趣的是,我使用的是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

当我运行它,然后在运行的实例上调用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;
}

SIGTERM这样的信号只提交给进程中的一个线程。唯一的前提条件是所选线程必须没有屏蔽信号,或者必须使用sigwait等待信号。其他线程将不会被直接通知信号已经被传递。

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

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

signal的手册页显示:

如果在系统调用或库函数调用被阻止时调用了信号处理程序,则:

  • 在信号处理程序返回后,调用会自动重新启动;或

  • 调用失败,并出现错误EINTR。

发生这两种行为中的哪一种取决于接口以及信号处理程序是否使用SA_RESTART标志建立(请参见sigaction(2))。详细信息因UNIX系统而异<…>

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

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

处理阻塞套接字(参见套接字(7)-(甚至是非阻塞套接字)的一个好方法是使用类似poll(2)(或过时的select(2)…)的多路复用系统调用

关于信号,请务必阅读信号(7)和信号安全(7)。

用一些事件循环(使用poll(2))处理信号的一种常见方法是使用一个信号处理程序,它只需将管道(7)上的一个字节写(2)-s到self(您将在初始化时设置管道,并在事件循环中对其进行轮询)。Qt文档解释了如何以及为什么。您还可以使用特定于Linux的signalfd(2)。