如何"buffer" UNIX 信号

How do you "buffer" UNIX signals

本文关键字:信号 UNIX buffer 如何      更新时间:2023-10-16

如何以阻塞函数在被调用时解封的方式缓冲UNIX信号?在我们的软件中,我们使用套接字。我们现在想通过信号取消阻止/取消recv()呼叫。问题是,如果在输入recv()之前发送信号,那么它就会丢失,recv()永远不会解锁。

主函数如下所示:

bool signalHandlerSetup;
bool signalSent;
int main(int argc, char* argv[])
{
pthread_t thread;
signalHandlerSetup = false;
signalSent = false;
// Block the SIGUSR1 signal in the main thread
sigset_t signalSet;
sigemptyset(&signalSet);
sigaddset(&signalSet, SIGUSR1);
pthread_sigmask(SIG_BLOCK, &signalSet, NULL);
// Setup the signal handler for all future threads
struct sigaction signalAction;
signalAction.sa_flags = 0;
signalAction.sa_handler = [](int signalNumber) {};
sigaction(SIGUSR1, &signalAction, NULL);
pthread_create(&thread, NULL, secondaryThreadFunction, NULL);
std::cout << "Started thread" << std::endl;
// Wait until the signal handler is setup
while (!signalHandlerSetup);
pthread_kill(thread, SIGUSR1);
signalSent = true;
pthread_join(thread, NULL);
std::cout << "Joined thread" << std::endl;
}

辅助线程只是创建一个套接字并尝试从中读取:

void* secondaryThreadFunction(void *arg)
{
// Setup socket
int internSocket = setupSocket();
// Setup the signal handling
sigset_t signalSet;
sigemptyset(&signalSet);
sigaddset(&signalSet, SIGUSR1);
pthread_sigmask(SIG_UNBLOCK, &signalSet, NULL);
signalHandlerSetup = true;
while (!signalSent);
char buffer;
std::cout << "recv()..." << std::endl;
ssize_t bytesRead = recv(internSocket, static_cast<void*>(&buffer), sizeof(buffer), 0);
std::cout << "recv() canceled" << std::endl;
close(internSocket);
}

如您所见,在输入recv()函数之前显式发送信号以说明问题。

除了缓冲之外,可能还有其他解决方案,但每种解决方案

都有缺点:
  • 等到辅助线程被阻塞(导致主线程也阻塞或繁忙循环(
  • 不断发送信号(另一个繁忙循环(

编辑: 不应仅仅为了取消阻塞辅助线程而关闭套接字。我们可能希望在解锁后再次在同一套接字上运行。

编辑2: 我已经能够用马丁·詹姆斯和达伦·史密斯提供的解决方案来解决这个问题。我现在正在将select()函数与事件文件描述符结合使用。作为参考,这里是工作解决方案:

int eventDescriptor;
int main(int argc, char* argv[])
{
pthread_t thread;
// Create the event file descriptor
eventDescriptor = eventfd(
0,  // Initial value
0   // Flags
);
if (eventDescriptor == -1)
{
std::cout << "Failed to create event file descriptor" << std::endl;
return -1;
}
pthread_create(&thread, NULL, secondaryThreadFunction, NULL);
std::cout << "Started thread" << std::endl;
// Notify the event descriptor
uint64_t valueToWrite = 1;
if (write(eventDescriptor, &valueToWrite, sizeof(uint64_t)) == -1)
{
std::cout << "Failed to write to event file descriptor" << std::endl;
return -1;
}
pthread_join(thread, NULL);
std::cout << "Joined thread" << std::endl;
close(eventDescriptor);
return 0;
}

这是辅助线程:

void* secondaryThreadFunction(void *arg)
{
// Setup socket
int internSocket = setupSocket();
// Set up the file descriptor set
fd_set readFileDescriptorSet;
FD_ZERO(&readFileDescriptorSet);
FD_SET(internSocket, &readFileDescriptorSet);
FD_SET(eventDescriptor, &readFileDescriptorSet);
char buffer;
std::cout << "select()..." << std::endl;
int fileDescriptorsSet = select(std::max(internSocket, eventDescriptor) + 1, &readFileDescriptorSet, NULL, NULL, NULL);
if (FD_ISSET(eventDescriptor, &readFileDescriptorSet))
{
std::cout << "select() canceled via event" << std::endl;
}
else if (FD_ISSET(internSocket, &readFileDescriptorSet))
{
std::cout << "select() canceled through socket" << std::endl;
ssize_t bytesRead = recv(internSocket, static_cast<void*>(&buffer), sizeof(buffer), 0);
}
close(internSocket);
}

您可以考虑执行以下操作(类似于评论中的MartinJames建议(。

重构你的辅助线程,以便不是直接阻塞recv()调用,而是用对基于文件的事件循环之一(例如,epollselect(的阻塞调用来替换

它阻止调用将侦听两个文件描述中的事件:

(1( 套接字的文件描述符 (internSocket(

(2( 由eventfd()创建的新文件描述符

在 main 函数中,您将通过调用eventfd()来创建事件文件描述符。 当信号到达时,将值写入事件文件描述符;这将导致被阻止的线程从选择等待中恢复。

基本示例:

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/eventfd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
void write_to_eventfd(int fd) {
/* we must write an 8 byte integet to the eventfd. */
uint64_t u = 1;
if (-1 == write(fd, &u, sizeof(uint64_t)))
perror("write()");
}
int main()
{
/* create event file descriptor */
int efd = eventfd(0, 0);
if (efd == -1)
perror("eventfd()");
/* For example purpose, do an immediate write; this causes select() to
* immediately return (simulate signal arrive before socket read). Later
* perform this in separate thread upon signal arrival. */
write_to_eventfd(efd);
/* Watch stdin (fd 0) to see when it has input.  Watch the eventfd for an
* event.  Add other socket file descriptors etc. */
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(0, &rfds);
FD_SET(efd, &rfds);
/* Blocking read */
int retval = select(efd+1, &rfds, NULL, NULL, NULL);
if (retval == -1)
perror("select()");
if (FD_ISSET(efd, &rfds))
printf("event!n"); /* next: read the value from efd */
if (FD_ISSET(0, &rfds))
printf("some data on fd(0)n");
return EXIT_SUCCESS;
}

还要考虑其他事件循环。

不要试图"取消阻止recv"。相反,请使用非阻塞recv,并使用为此目的而设计的ppoll来阻止。(也可以使用pselect(

设置struct pollfd

struct pollfd pollfd = {.fd = internSocket, .events = POLLIN};

阻止信号,直到您准备好接收它:

sigset_t signalSet;
sigemptyset(&signalSet);
sigaddset(&signalSet, SIGUSR1);
pthread_sigmask(SIG_BLOCK, &signalSet, NULL);

准备一个信号集,信号畅通无阻:

pthread_sigmask(SIG_SETMASK /* ignored when second parameter is null */, NULL, &signalSet);
sigdelset(&signalSet, SIGUSR1);

呼叫ppoll以阻止:

int ppoll_result = ppoll(&pollfd, 1, NULL /* no timeout */, &signalSet);

检查ppoll是否被信号中断,或者您是否获得了任何数据:

if (ppoll_result < 0) {
if (errno == EINTR) {
// interrupted by signal
} else {
// error occurred
}
} else {
assert(ppoll_result == 1); // Should always be true, but it's a good idea to check anyway
// call recv
}

注意:通常,在使用poll/ppoll时,我们会检查pollfd.events以查看导致其唤醒的事件类型,但这不是必需的,因为您只等待一个套接字。使用poll/ppoll时,可以一次等待多个套接字。

注意:这不是等待带有poll信号的唯一方法。您可以使用signalfd,它将信号"转换"成看起来像套接字的东西。

您可以创建缓冲信号的signalfd,而无需安装信号处理程序。请参阅man signalfd中的示例。

您需要使用select/poll/epoll事件循环来等待信号或套接字 fd 准备好读取,然后在非阻塞模式下读取它们直到EAGAIN,就像这里的其他答案建议的那样。