在多线程环境下,recv()函数不会被信号中断。

19

我有一个线程处于阻塞的recv()循环中,我想要终止它(假设无法改为select()或其他任何异步方法)。

我还有一个信号处理程序来捕获SIGINT,理论上它应该使recv()返回错误并将errno设置为EINTR

但实际上没有这样,我认为这与应用程序是多线程有关。同时也有另一个线程,正在等待pthread_join() 调用。

这里发生了什么?

编辑:

好的,现在我通过主线程使用pthread_kill() 显式地将信号传递给所有阻塞的recv()线程(尽管多次调用是无害的,因为它会导致相同的全局信号处理程序)。但是,recv()调用仍然没有被解除阻塞。

编辑:

我编写了一个代码示例来重现这个问题。

  1. 主线程连接到一个不正常工作的远程主机的套接字,它不会让连接断开。
  2. 阻塞所有信号。
  3. 启动读线程。
  4. 主线程取消阻塞并安装SIGINT信号的处理程序。
  5. 读取线程取消阻塞并安装SIGUSR1信号的处理程序。
  6. 主线程的信号处理程序向读线程发送SIGUSR1信号。

有趣的是,如果我用sleep()替换recv(),它就可以被中断。

PS

或者你可以打开一个UDP套接字,而不是使用服务器。

客户端

#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>

static void
err(const char *msg)
{
    perror(msg);
    abort();
}

static void
blockall()
{
    sigset_t ss;
    sigfillset(&ss);
    if (pthread_sigmask(SIG_BLOCK, &ss, NULL))
        err("pthread_sigmask");
}

static void
unblock(int signum)
{
    sigset_t ss;
    sigemptyset(&ss);
    sigaddset(&ss, signum);
    if (pthread_sigmask(SIG_UNBLOCK, &ss, NULL))
        err("pthread_sigmask");
}

void
sigusr1(int signum)
{
    (void)signum;
    printf("%lu: SIGUSR1\n", pthread_self());
}

void*
read_thread(void *arg)
{
    int sock, r;
    char buf[100];

    unblock(SIGUSR1);
    signal(SIGUSR1, &sigusr1);
    sock = *(int*)arg;
    printf("Thread (self=%lu, sock=%d)\n", pthread_self(), sock);
    r = 1;
    while (r > 0)
    {
        r = recv(sock, buf, sizeof buf, 0);
        printf("recv=%d\n", r);
    }
    if (r < 0)
        perror("recv");
    return NULL;
}

int sock;
pthread_t t;

void
sigint(int signum)
{
    int r;
    (void)signum;
    printf("%lu: SIGINT\n", pthread_self());
    printf("Killing %lu\n", t);
    r = pthread_kill(t, SIGUSR1);
    if (r)
    {
        printf("%s\n", strerror(r));
        abort();
    }
}

int
main()
{
    pthread_attr_t attr;
    struct sockaddr_in addr;

    printf("main thread: %lu\n", pthread_self());
    memset(&addr, 0, sizeof addr);
    sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (socket < 0)
        err("socket");
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8888);
    if (inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) <= 0)
        err("inet_pton");
    if (connect(sock, (struct sockaddr *)&addr, sizeof addr))
        err("connect");

    blockall();
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
    if (pthread_create(&t, &attr, &read_thread, &sock))
        err("pthread_create");
    pthread_attr_destroy(&attr);
    unblock(SIGINT);
    signal(SIGINT, &sigint);

    if (sleep(1000))
        perror("sleep");
    if (pthread_join(t, NULL))
        err("pthread_join");
    if (close(sock))
        err("close");

    return 0;
}

服务器

import socket
import time

s = socket.socket(socket.AF_INET)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('127.0.0.1',8888))
s.listen(1)
c = []
while True:
    (conn, addr) =  s.accept()
    c.append(conn)

我遇到了类似的问题(虽然在某些方面有所不同),使用单线程应用程序(一个没有其他线程的进程)。因此,我认为多线程不是这里行为的原因。 - Hibou57
5个回答

27
通常情况下,信号不会中断带有 EINTR 错误码的系统调用。在历史上,有两种可能的信号传递行为:BSD 行为(当被信号中断时,系统调用会自动重启),以及 Unix System V 行为(当被信号中断时,系统调用返回 -1,并设置 errnoEINTR)。Linux 内核采用了后者,但 GNU C 库的开发人员(正确地)认为 BSD 行为更加合理,因此在现代 Linux 系统上,调用 signal(它是一个库函数)会产生 BSD 行为。
POSIX 允许任何一种行为,所以建议在可以选择设置 SA_RESTART 标志或省略它的情况下,始终使用 sigaction 函数。请参阅此处的 sigaction 文档:

http://www.opengroup.org/onlinepubs/9699919799/functions/sigaction.html


@Alex - 那么使用 sigaction 并设置 SA_RESTART 标志解决了你的问题吗? - Hanno Fietz
省略 SA_RESTART 标志将会得到 OP 想要的行为。 - R.. GitHub STOP HELPING ICE
实际上,在Linux / Ubuntu 12.04上,这两种行为都是公开的:recv将在Ctrl-C上重新启动,而poll将在同一Ctrl-C上返回EINT。但是,使用sigaction而不是signal确实解决了该问题。这意味着sigaction是实现信号在系统调用期间发生可预测行为的方法;因此建议始终使用sigaction(我会从现在开始这样做)。 - Hibou57
2
“poll”和“select”是特殊的,它们永远不会重新启动。这是由POSIX规定的。 - R.. GitHub STOP HELPING ICE

6

在多线程应用程序中,正常信号可以随意传递到任何线程。 使用 pthread_kill 将信号发送到特定的感兴趣的线程。


由于某些原因,这个程序不起作用(recv调用没有被中断)。我需要为线程信号处理程序和/或信号掩码做任何特殊的处理吗? - Alex B
你确认了信号处理程序是否被调用,如果是的话,是从哪个线程调用的吗?recv() 调用完成后是否收到了信号? - bdonlan

4

信号处理程序是否在等待recv()的同一线程中调用?您可能需要通过pthread_sigmask()在所有其他线程中显式屏蔽SIGINT信号。


2
正确 - 一个面向进程的信号被传递到一个没有被阻塞的任意线程,因此解决方案是在所有其他线程中阻塞它。 - caf
1
为了完整起见:新线程会继承其父线程的信号屏蔽集。您可以在父线程中阻塞它,并在线程创建后解除阻塞。如果您依赖创建线程的库,而无法更改该库,则这可能是唯一的解决方案(据我所知)。 - jweyrich
我认为这对我没有用。我需要主线程停止阻塞线程,这意味着信号处理程序应该安装在不调用“recv()”的线程中。 - Alex B

1
正如<R..>在帖子中提到的那样,改变信号活动确实是可能的。 我经常创建自己的“信号”函数,利用sigaction。这是我使用的:

typedef void Sigfunc(int);

static Sigfunc* 
_signal(int signum, Sigfunc* func)
{
    struct sigaction act, oact;

    act.sa_handler = func;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    if (signum != SIGALRM)
        act.sa_flags |= SA_NODEFER; //SA_RESTART;

    if (sigaction(signum, &act, &oact) < 0)
        return (SIG_ERR);
    return oact.sa_handler;
}

上述属性是 sa_flags 字段的“或”操作。这来自于“sigaction”的手册页面:SA_RESTART 提供了类似 BSD 的行为,允许系统调用在信号中断后重新启动。SA_NODEFER 意味着允许从其自身的信号处理程序中接收信号。
当信号调用被替换为“_signal”时,线程会被中断。输出打印出“中断的系统调用”,并且当 SIGUSR1 被发送时,recv 返回 -1。当 SIGINT 被发送时,程序完全停止,并输出相同的内容,但在最后调用了 abort。
我没有编写代码的服务器部分,我只是将套接字类型更改为“DGRAM、UDP”,以便客户端可以启动。

0

你可以在Linux的recv函数上设置超时时间:Linux: is there a read or recv from socket with timeout?

当你收到一个信号时,调用正在接收数据的类的done方法。

void* signalThread( void* ptr )
{
    CapturePkts* cap=(CapturePkts*)ptr;
    sigset_t sigSet=cap->getSigSet();
    int sig=-1;
    sigwait(&sigSet,&sig); //signalThread: signal capture thread enabled;
    cout << "signal=" << sig << " caught,ending process" << endl;
    cap->setDone();
    return 0;
}

class CapturePkts
{
     CapturePkts() : _done(false) {}

     sigset_t getSigSet() { return _sigSet; }

     void setDone() {_done=true;}

     bool receive( uint8_t *buffer, int32_t bufSz, int32_t &nbytes)
     {
         bool ret=true;
         while( ! _done ) {
         nbytes = ::recv( _sockid, buffer, bufSz, 0 );
         if(nbytes < 1 ) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
               nbytes=0; //wait for next read event
            else
               ret=false;
         }
         return ret;
     }

     private:
     sigset_t _sigSet;
     bool _done;
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接