Reputation: 3
How does one buffer a UNIX signal in such a way blocking functions will unblock upon being called? In our software we use sockets. We now want to unblock/cancel a recv()
call through a signal. The issue is that if the signal is being sent before recv()
is entered, then it is lost and recv()
never unblocks.
The main function looks like this:
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;
}
The secondary thread just creates a socket and attempts to read from it:
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);
}
As you can see the signal is explicitly being sent before the recv()
function is entered to illustrate the problem.
Apart from buffering, there might be other solutions to this but there are drawbacks to each of these:
EDIT: The socket should not be closed just to unblock the secondary thread. We might want to operate on the same socket again after unblocking.
EDIT 2:
I've been able to solve the problem with the solution provided by Martin James and Darren Smith. I am using the select()
function in combination with an event file descriptor now. For reference here is the working solution:
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;
}
Here is the secondary thread:
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);
}
Upvotes: 0
Views: 307
Reputation: 136395
You can create signalfd
that buffers the signals for you, no need to install a signal handler. See the example in man signalfd
.
You will need to use select
/poll
/epoll
event loop to wait till the signal or your socket fd are ready for read and then read them in non-blocking mode until EAGAIN
, like the other answers here advise.
Upvotes: 0
Reputation: 2498
You can consider doing the following (similar to MartinJames suggestion in comments).
Restructure your secondary thread so that instead of doing direct blocking calls of recv()
, you replace it with blocking call to one of the file-based event loops (e.g., epoll
, or select
)
The blocking call will listen for events on two file descriptions:
(1) the file descriptor of the socket (internSocket
)
(2) a new file descriptor created by eventfd()
In your main function you will create the event file descriptor by calling eventfd()
. When a signal arrives, write a value to the event file descriptor; that will cause the blocked thread to resume out of the select wait.
Basic example:
#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;
}
Also consider the other event loops.
Upvotes: 1
Reputation: 58898
Don't try to "unblock recv
". Instead, use non-blocking recv
, and use ppoll
to block which is designed for this purpose. (pselect
can also be used)
Set up a struct pollfd
:
struct pollfd pollfd = {.fd = internSocket, .events = POLLIN};
Block the signal until you are ready to receive it:
sigset_t signalSet;
sigemptyset(&signalSet);
sigaddset(&signalSet, SIGUSR1);
pthread_sigmask(SIG_BLOCK, &signalSet, NULL);
Prepare a signal set with the signal unblocked:
pthread_sigmask(SIG_SETMASK /* ignored when second parameter is null */, NULL, &signalSet);
sigdelset(&signalSet, SIGUSR1);
Call ppoll
to block:
int ppoll_result = ppoll(&pollfd, 1, NULL /* no timeout */, &signalSet);
Check whether ppoll
was interrupted by a signal, or whether you got any data:
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
}
Note: generally when using poll
/ppoll
we would check pollfd.events
to see what type of events caused it to wake up, but this is not necessary since you are only waiting for one socket. It is possible to wait for more than one socket at a time when using poll
/ppoll
.
Note: This is not the only way to wait for a signal with poll
. You could use signalfd
, which "converts" signals into something that looks like a socket.
Upvotes: 1