你如何 "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,就像这里建议的其他答案一样。