【问题标题】:How to interrupt blocking accept() call in C++?如何中断 C++ 中的阻塞 accept() 调用?
【发布时间】:2020-12-28 11:42:11
【问题描述】:

我正在尝试使用alarm() 信号在发生超时后退出accept() 调用。信号注册为例外,但不会中断阻塞的accept() 进程。如何让信号中断循环或中断阻塞过程?

volatile sig_atomic_t time_out_flag = false;

void handleSig(int sig)
{
    std::cout << "signal\n";
    time_out_flag = true;
    return;
}

void Server::start(ClientHandler &ch) // throw(const char *)
{
    t = new std::thread([&] { // Main thread
        sockaddr.sin_family = AF_INET;
        sockaddr.sin_addr.s_addr = INADDR_ANY;
        sockaddr.sin_port = htons(_port);

        if (bind(_socket, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0) // Bind socket (socket, sockaddr, addrlen)
            throw("Failed to bind to port.\n");

        if (listen(_socket, 2) < 0) // Passivly listen on socket (socket, backlog aka max users queued)
            throw("Failed to listen on socket.\n");

        auto addrlen = sizeof(sockaddr);
        signal(SIGALRM, handleSig); // What to do after alarm(k)
        while (!time_out_flag)
        {
            alarm(3);
            int connection = accept(_socket, (struct sockaddr *)&sockaddr, (socklen_t *)&addrlen);
            if (connection < 0) {
                throw("accept() error\n");
            }
            alarm(0);
            ch.handle(connection);
        }
        std::cout << "after";
    });
}

如果可能的话,我真的很想和accept() 合作,而不是select()

更新

我已按照建议将signal() 更改为sigaction(),但accept() 继续阻塞线程。这是修改后的代码:


volatile sig_atomic_t alarmed = 0;

void handle_alarm(int)
{
    std::cout << "alarm\n";
    alarmed = 1;
    return;
}

void Server::start(ClientHandler &ch) // throw(const char *)
{
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGALRM);

    if (pthread_sigmask(SIG_BLOCK, &mask, nullptr) < 0)
        throw("sigaction pthread failed\n");

#ifdef USE_SIGACTION
    // Set up sigaction() with alarmer
    struct sigaction sigbreak;
    std::memset(&sigbreak, 0, sizeof sigbreak);
    sigbreak.sa_handler = &handle_alarm;

    if (sigaction(SIGALRM, &sigbreak, NULL) != 0)
        throw("sigaction() failed\n");

#else
    if (signal(SIGALRM, handle_alarm) == SIG_ERR)
        throw("signal() failed\n");
#endif

    t = new std::thread([&] { // Main thread
        if (pthread_sigmask(SIG_UNBLOCK, &mask, nullptr) < 0)
            std::cout << "alarmos\n";

        struct sockaddr_in server_sockaddr;
        struct sockaddr_in sockaddr = {0};
        sockaddr.sin_family = AF_INET;
        sockaddr.sin_addr.s_addr = INADDR_ANY;
        sockaddr.sin_port = htons(_port);
        if (bind(_socket, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0) // Bind socket (socket, sockaddr, addrlen)
            throw("Failed to bind to port.\n");

        if (listen(_socket, 2) < 0) // Passivly listen on socket (socket, backlog aka max users queued)
            throw("Failed to listen on socket.\n");

        auto addrlen = sizeof(sockaddr);
        while (1)
        {
            if (alarmed)
                return;
            try
            {
                alarm(3);
                int connection = accept(_socket, (struct sockaddr *)&sockaddr, (socklen_t *)&addrlen);
                if (alarmed)
                    return;
                if (connection == -1)
                    throw("accept() error\n");
                alarm(0);
                ch.handle(connection);
            }

            catch (const char *msg)
            {
                throw(msg);
            }
        }
    });
    alarm(3);
}

更新 + 解决方案

定义USE_SIGACTION 可以解决问题并按照答案中的建议中断accept()

【问题讨论】:

  • 这正是select 及其变体、poll、epoll 等所擅长的。为什么不使用其中之一?
  • @TedLyngmo 讲师明确要求 alarm() 作为超时解决方案的一部分。
  • 请澄清"...the interrupt isn't working"的确切含义。
  • 您可以使用poll(2)。另见time(7)signal(7)signal-safety(7)signalfd(2)。您的 SIGALRM 信号处理程序将设置一个全局 volatile sigatomic_t flag

标签: c++ linux sockets


【解决方案1】:

问题在于signal() 安装信号处理程序的方式是在捕获信号后恢复像accept() 这样的系统调用*。请改用sigaction() 来禁用该行为:

#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <thread>

#include <signal.h>
#include <unistd.h>

//#define USE_SIGACTION

volatile sig_atomic_t alarmed = 0;

void handle_alarm(int) {
  alarmed = 1;
}

int main(void) {
  sigset_t mask;
  sigemptyset(&mask);
  sigaddset(&mask, SIGALRM);

  if (pthread_sigmask(SIG_BLOCK, &mask, nullptr) < 0) {
    std::perror("pthread_sigmask");
    return 1;
  }

#ifdef USE_SIGACTION
  struct sigaction alarmer;
  std::memset(&alarmer, 0, sizeof alarmer);
  alarmer.sa_handler = handle_alarm;
  // For resumable syscalls you'd have
  // alarmer.sa_flags = SA_RESTARTT;
  // but we don't want that.
  if (sigaction(SIGALRM, &alarmer, nullptr) < 0) {
    std::perror("sigaction");
    return 1;
  }
  std::cout << "Using sigaction()\n";
#else
  if (signal(SIGALRM, handle_alarm) == SIG_ERR) {
    std::perror("signal");
    return 1;
  }
  std::cout << "Using signal()\n";
#endif

  std::thread t{[&mask](){
      char dummy[5];

      if (pthread_sigmask(SIG_UNBLOCK, &mask, nullptr) < 0) {
        perror("pthread_sigmask");
        std::exit(1);
      }

      std::cout << "Waiting...\n";
      auto bytes = read(0, dummy, sizeof dummy);
      if (bytes < 0) {
        std::perror("read");
        if (alarmed) {
          std::cerr << "SIGALRM was caught.\n";
          return;
        }
        std::exit(1);
      }
    }};

  alarm(1);
  t.join();

  return 0;
}

示例用法:

$ g++ -pthread -O -Wall -Wextra -DUSE_SIGACTION foo.cpp
$ ./a.out                                     
Using sigaction()
Waiting...
read: Interrupted system call
SIGALRM was caught.
$ g++ -pthread -O -Wall -Wextra foo.cpp
$ ./a.out                     
Using signal()
Waiting...
^C

signal() 上的注释:POSIX 没有指定是否打开可重新启动的系统调用,并且不同的操作系统可以并且确实有所不同。 Linux/Glibc 行为在其signal(2) man page 中进行了描述:

默认情况下,在 glibc 2 及更高版本中,signal() 包装函数不会调用内核系统调用。相反,它使用提供 BSD 语义的标志调用 sigaction(2)。只要定义了合适的功能测试宏,就会提供此默认行为:glibc 2.19 及更早版本中的_BSD_SOURCE 或 glibc 2.19 及更高版本中的_DEFAULT_SOURCE。 (默认情况下,这些宏已定义;详情请参阅feature_test_macros(7)。)如果未定义此类功能测试宏,则signal() 提供 System V 语义。

BSD 语义包括重新启动系统调用,而 System V 不包括。根据文档的建议,最好的方法是永远不要使用 signal() 并坚持使用 sigaction() 和朋友。

关于线程和信号:当多线程应用程序接收到不是针对特定线程的信号时,它会被传递给没有阻塞该信号的随机线程。所以这段代码首先阻塞SIGALRM,然后在示例阻塞线程中解除阻塞。如果你不这样做,可能不是你的accept() 被打断,而是在不同的线程中。

【讨论】:

  • 这似乎以某种方式导致相同的结果。我已经更新了我的帖子。
  • @Nix 你在某处定义USE_SIGACTION吗?
  • 完全超出了我的想象。一旦定义它就可以完美运行,谢谢。
【解决方案2】:

如果没有select(),例如,您可以在另一个线程中关闭套接字。在这种情况下,accept() 将结束。但这种情况不正常。

顺便说一句,您的代码存在抛出异常的问题。在线程内的这种情况下,您的程序将被终止。为避免这种情况,您应该在线程内编写 try-catch。例如:

t = new std::thread([&] { // Main thread
    sockaddr.sin_family = AF_INET;
    sockaddr.sin_addr.s_addr = INADDR_ANY;
    sockaddr.sin_port = htons(_port);
try
{
 ....
}
catch(const char* msg)
{
    std::cout << msg << std::endl;
}});

最好使用命名空间 std 中的异常类而不是字符串。

【讨论】:

  • 我认为这会导致未定义的行为 - 除非有文档明确说明此场景的定义行为。
  • @TedLyngmo 我同意你的看法。
猜你喜欢
  • 2016-12-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-29
  • 2014-10-26
  • 2013-11-16
  • 1970-01-01
相关资源
最近更新 更多