C套接字阻塞调用

C socket blocking call

我想知道套接字在阻塞和非阻塞操作上的行为。当套接字阻塞模式改变时,线程阻塞在套接字上会发生什么?这是场景; thread1(T1) 创建一个 UDP 套接字并且

fd = socket(AF_INET , SOCK_DGRAM, 0);

T1 等待(休眠)接收

recv(fd, buf , sizeof(buf) , 0);

并且线程 2(T2) 在套接字接收任何数据之前将套接字模式更改为非阻塞

fcntl(fd, F_SETFL, O_NONBLOCK);

T1 会怎样?是不是 signalled/waked 因为套接字不再阻塞?

Unblocked 线程(由于处于阻塞模式时读取而未阻塞)将表现得好像它始终是非阻塞模式。

阅读recv(2) manual page

If no messages are available at the socket, and if the socket is nonblocking, the value -1 is returned and the external variable errno is set to EAGAIN or EWOULDBLOCK.


Blocked 个线程(在阻塞模式下读取时阻塞)在更改为非阻塞之前。 正如@Maxim 分享的不唤醒线程的函数代码所指出的,被阻塞的线程只有在写入完成后才会被唤醒(数据可用)。

行为实际上是未指定的:fcntl 不需要解锁任何线程。

Linux just sets the flag in the file description struct file and returns without unblocking any blocked threads.

线程 已经recv 中阻塞,只有在以下情况下才能安排到 运行:

  • 要读取的数据可用;
  • 或检测到文件描述符的错误条件(FIN/RST,套接字读取超时,TCP 保持活动失败,文件描述符被另一个 closed线程);
  • 或收到信号且信号配置不包含SA_RESTART;
  • 或者是pthread_cancelled。

您正在尝试更改另一个线程的文件描述符的标志这一事实表明您的设计需要审查。理想情况下,线程不得共享任何数据,也不得查看彼此的状态,而应该使用消息传递来相互通信。

我认为POSIX specification对此很清楚:

If no messages are available at the socket and O_NONBLOCK is not set on the socket's file descriptor, recv() shall block until a message arrives. If no messages are available at the socket and O_NONBLOCK is set on the socket's file descriptor, recv() shall fail and set errno to [EAGAIN] or [EWOULDBLOCK].

O_NONBLOCK 尚未设置时调用 recv,它应该阻塞直到消息到达(直到模式改变)。

你让我很好奇。首先,很明显,因为没有指定套接字应该唤醒的标准,所以它不会被唤醒,因为实现它会很痛苦(因为非阻塞标志与套接字阻塞)。所以我们可以非常自信地说套接字在我们收到数据包之前不会唤醒。还是会?

#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <err.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <string.h>
#include <time.h>
#include <fcntl.h>

int sock;

static void
sighand(int s)
{
        write(1, "sig\n", 4);
}

static void *
rcv(void *v)
{
        struct sigaction sa;

        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = sighand;
        sa.sa_flags = SA_RESTART;
        if (sigaction(SIGUSR1, &sa, NULL) == -1)
                err(1, "sigaction");

        char buf[64];
        ssize_t sz = recv(sock, buf, sizeof(buf), 0);
        printf("recv %d\n", (int)sz);
        return NULL;
}

pthread_t t1, t2;

static void *
beeper(void *v)
{
        for (;;) {
                nanosleep(&((struct timespec){.tv_sec = 1}), NULL);
                if (pthread_kill(t1, SIGUSR1))
                        errx(1, "pthread_kill");
                printf("beep\n");
        }
}

int
main(int argc, char **argv)
{
        if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) == -1)
                err(1, "socket");

        struct sockaddr_in sin;
        memset(&sin, 0, sizeof(sin));
        sin.sin_family = AF_INET;
        sin.sin_port = htons(4711);
        sin.sin_addr.s_addr = htonl(INADDR_ANY);
        if (bind(sock, (struct sockaddr *)&sin, sizeof(sin)) == -1)
                err(1, "bind");

        if (pthread_create(&t1, NULL, rcv, NULL))
                errx(1, "pthread_create");

        if (pthread_create(&t2, NULL, beeper, NULL))
                errx(1, "pthread_create");

        /* pretend that this is correct synchronization. */
        nanosleep(&((struct timespec){.tv_sec = 3}), NULL);

        printf("setting non-block\n");
        if (fcntl(sock, F_SETFL, O_NONBLOCK) == -1)
                err(1, "fcntl");
        printf("set\n");

        nanosleep(&((struct timespec){.tv_sec = 3}), NULL);

        return 0;
}

上面的代码(抱歉,无法缩短)。在 recv 中阻塞线程,稍等片刻,然后在文件描述符上设置非阻塞。正如预期的那样,没有任何反应。但后来我加了一个转折。接收线程有一个信号处理程序,每秒用 SA_RESTART 唤醒一次。当然,既然已经SA_RESTARTrecv应该不会醒来了。

在 OSX 这将 "misbehave" 如果我们认为任何行为是正确的。我很确定这在所有 BSD 上的表现都是一样的。 Linux 也一样。事实上,通常 SA_RESTART 的实现方式我很确定这将 "misbehave" 几乎无处不在。

有趣。这当然并不意味着上面的代码对任何东西都有用,但这是一个有趣的好奇心。要回答你的问题,这是未指定的,但在大多数情况下它不会醒来,除非它会。请不要做这种奇怪的事情。