9

I wonder about the behaviour of socket on blocking and nonblocking actions. What happens to threads blocking on socket when the socket blocking mode changes ? Here is the scenario; thread1(T1) creates a UDP socket and

fd = socket(AF_INET , SOCK_DGRAM, 0);

T1 waiting(sleeping) for receive

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

and thread2(T2) changes socket mode to non-blocking before socket receive any data

fcntl(fd, F_SETFL, O_NONBLOCK);

what happens to T1 ? Is it signalled/waked because the socket is no more blocking ?

Qxtrml
  • 177
  • 2
  • 7
  • I cannot find it in the docs, therefore I'd say that "behaviour is undefined" ;) – Antti Haapala -- Слава Україні Aug 11 '17 at 13:27
  • I would expect it to be unsupported because the socket api was made long before multithreading existed. – alain Aug 11 '17 at 13:27
  • 3
    Accessing a socket from multiple threads is a bad idea as it invites race conditions -- for example, consider the scenario where T1's recv() call returns with an error (-1), and T1 responds my calling close() on the socket, just before T2 calls fcntl() on it. In the best-case scenario, T2's fcntl() call will fail (with EBADF) because the socket handle is now invalid; in the worst-case scenario, some other (unrelated) thread called socket() during the interim period and was given the same fd value, and now T2 has made a random socket non-blocking. (That's a really hard bug to track down!) – Jeremy Friesner Aug 11 '17 at 13:47

4 Answers4

8

The behavior is literally unspecified: fcntl is not required to unblock any threads.

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

A thread already blocked in recv can be scheduled to run only if:

  • data to read becomes available;
  • or an error condition on the file descriptor is detected (FIN/RST, socket read timeout, TCP keep-alive failure, the file descriptor is closed by another thread);
  • or a signal is received and the signal disposition does not include SA_RESTART;
  • or it is pthread_cancelled.

The fact that you are trying to change flags of a file descriptor of another thread suggests that your design requires a review. Ideally, threads must not share any data and not poke at each other's state, rather they should use message passing to communicate with each other.

Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
  • Yes - I wish devs would stop attempting weird things when there are understandable workarounds with defined behaviour:( – Martin James Aug 11 '17 at 14:06
  • 1
    @MartinJames Just tell them nicely. – Maxim Egorushkin Aug 11 '17 at 14:07
  • Ehm. `SA_RESTART` you say? See my answer for a funny twist. – Art Aug 11 '17 at 14:38
  • @Art [`man signal 7`](http://man7.org/linux/man-pages/man7/signal.7.html): _The details vary across UNIX systems; below, the details for Linux. If a blocked call to one of the following interfaces is interrupted by a signal handler, then the call will be automatically restarted after the signal handler returns if the SA_RESTART flag was used; otherwise the call will fail with the error EINTR: ... **Socket interfaces: accept(2), connect(2), recv(2), recvfrom(2), recvmmsg(2), recvmsg(2), send(2), sendto(2), and sendmsg(2)**, unless a timeout has been set on the socket..._ – Maxim Egorushkin Aug 11 '17 at 14:44
  • @MaximEgorushkin I have very simple code in my answer that shows that given the scenario in the question `SA_RESTART` will make things misbehave. – Art Aug 11 '17 at 14:46
  • @Art You did not specify how exactly it _misbehaves_, no output either. – Maxim Egorushkin Aug 11 '17 at 14:47
  • Can't paste the output now since I'm no longer at a terminal, but in short: The way SA_RESTART is usually implemented the whole syscall returns to almost userland, then the signal handler is run and the syscall is rerun. But in this case the syscall will be rerun with the new fd flags. – Art Aug 11 '17 at 15:00
  • 1
    @Art True, interesting observation. However, I would not bet my money on an application that relies on such corner-case behaviours. – Maxim Egorushkin Aug 11 '17 at 15:03
  • No, of course not. Something like this would be an immediate cause for rejecting the code. If anything this little gotcha demonstrates why you should never mess with the same fd in different threads. But it's a fun curiosity. – Art Aug 11 '17 at 15:13
  • @MartinJames first of all this is not real life application usage question. I am reading Opengroup standards and various O/S man pages. Opengroup does not contain detailed information i think and other systems can have lot of different usage details. Think this just like a simple curious question :) – Qxtrml Aug 11 '17 at 16:02
3

You made me curious. First, it's quite obvious that since there is no standard that specifies that the socket should wake up, it will not be awoken because it would be quite a pain to implement this (since the non-blocking flag is in a different layer than where the socket is blocking). So we can say quite confidently that the socket will not wake up until we receive a packet. Or will it?

#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;
}

The code above (sorry, couldn't make it shorter). Blocks the thread in recv, waits a bit and then sets non-blocking on the file descriptor. As expected nothing happens. But then I added a twist. The receiving thread has a signal handler that wakes up once a second with SA_RESTART. Of course, since we have SA_RESTART recv should not wake up.

On OSX this will "misbehave" if we consider any behavior correct. I'm pretty sure this will behave the same way on all BSDs. Same on Linux. In fact, the way SA_RESTART is usually implemented I'm pretty sure this will "misbehave" pretty much everywhere.

Funny. This of course doesn't mean that the code above is useful for anything, but it's an interesting curiosity. To answer your question, this is unspecified, but in most cases it won't wake up, unless it will. Please don't do weird things like this.

Art
  • 19,807
  • 1
  • 34
  • 60
1

I think POSIX specification is pretty clear about this:

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].

You call recv when O_NONBLOCK is not yet set, to it should block until message arrives (not until mode changes).

myaut
  • 11,174
  • 2
  • 30
  • 62
0

Unblocked thread (didn't block yet due to reading when it was in blocking mode) will behave as if it was always a non-blocking mode.

Reading 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 threads (blocked while reading when in blocking mode) before changing to non-blocking. As pointed by @Maxim sharing the code of the function which doesn't awaken threads, The blocked threads will only be awaken after a write is done (data is available).

Tony Tannous
  • 14,154
  • 10
  • 50
  • 86