你如何 "buffer" UNIX 发出信号
How do you "buffer" UNIX signals
如何缓冲 UNIX 信号,使阻塞函数在调用时解除阻塞?在我们的软件中,我们使用套接字。我们现在想通过一个信号 unblock/cancel 一个 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:
我已经能够使用 Martin James 和 Darren Smith 提供的解决方案解决问题。我现在将 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()
,而是用阻塞调用基于文件的事件循环之一(例如,epoll
,或 select
)
阻塞调用将监听 两个 文件描述的事件:
(1)套接字的文件描述符(internSocket
)
(2) eventfd()
创建的新文件描述符
在您的主函数中,您将通过调用 eventfd()
创建事件文件描述符。当信号到达时,向事件文件描述符写入一个值;这将导致阻塞的线程从 select 等待中恢复。
基本示例:
#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;
}
还要考虑其他事件循环。
不要尝试 "unblock 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
,它 "converts" 向看起来像套接字的东西发送信号。
您可以创建 signalfd
来为您缓冲信号,无需安装信号处理程序。请参阅 man signalfd
中的示例。
你需要使用select
/poll
/epoll
事件循环来等待信号或你的套接字fd准备好读取,然后以非阻塞方式读取它们模式直到 EAGAIN
,就像这里建议的其他答案一样。
如何缓冲 UNIX 信号,使阻塞函数在调用时解除阻塞?在我们的软件中,我们使用套接字。我们现在想通过一个信号 unblock/cancel 一个 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:
我已经能够使用 Martin James 和 Darren Smith 提供的解决方案解决问题。我现在将 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()
,而是用阻塞调用基于文件的事件循环之一(例如,epoll
,或 select
)
阻塞调用将监听 两个 文件描述的事件:
(1)套接字的文件描述符(internSocket
)
(2) eventfd()
在您的主函数中,您将通过调用 eventfd()
创建事件文件描述符。当信号到达时,向事件文件描述符写入一个值;这将导致阻塞的线程从 select 等待中恢复。
基本示例:
#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;
}
还要考虑其他事件循环。
不要尝试 "unblock 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
,它 "converts" 向看起来像套接字的东西发送信号。
您可以创建 signalfd
来为您缓冲信号,无需安装信号处理程序。请参阅 man signalfd
中的示例。
你需要使用select
/poll
/epoll
事件循环来等待信号或你的套接字fd准备好读取,然后以非阻塞方式读取它们模式直到 EAGAIN
,就像这里建议的其他答案一样。