22

In my application, there is a io-thread, that is dedicated for

  1. Wrapping data received from the application in a custom protocol
  2. Sending the data+custom protocol packet over tcp/ip
  3. Receiving data+custom protocol packet over tcp/ip
  4. Unwrapping the custom protocol and handing the data to the application.

Application processes the data over a different thread. Additionally, the requirements dictate that the unacknowledged window size should be 1, i.e. there should be only one pending unacknowledged message at anytime. This implies that if io-thread has dispatched a message over the socket, it will not send any more messages, till it hears an ack from the receiver. Application's processing thread communicates to io-thread via pipe. Application needs to shut gracefully if someone from linux CLI types ctrl+C. Thus, given these requirements, i have following options

  1. Use PPoll() on socket and pipe descriptors
  2. Use Select()
  3. Use PSelect()

I have following questions

  1. The decision between select() and poll(). My application only deals with less than 50 file descriptors. Is it okay to assume there would be no difference whether i choose select or poll ?

    1. Decision between select() and pselect(). I read the linux documentation and it states about race condition between signals and select(). I dont have experience with signals, so can someone explain more clearly about the race condition and select() ? Does it have something to do with someone pressing ctrl+C on CLI and application not stopping?

    2. Decision between pselect and ppoll() ? Any thoughts on one vs the other

Thor
  • 230
  • 3
  • 14
Jimm
  • 8,165
  • 16
  • 69
  • 118

4 Answers4

30

I'd suggest by starting the comparison with select() vs poll(). Linux also provides both pselect() and ppoll(); and the extra const sigset_t * argument to pselect() and ppoll() (vs select() and poll()) has the same effect on each "p-variant", as it were. If you are not using signals, you have no race to protect against, so the base question is really about efficiency and ease of programming.

Meanwhile there's already a stackoverflow.com answer here: what are the differences between poll and select.

As for the race: once you start using signals (for whatever reason), you will learn that in general, a signal handler should just set a variable of type volatile sig_atomic_t to indicate that the signal has been detected. The fundamental reason for this is that many library calls are not re-entrant, and a signal can be delivered while you're "in the middle of" such a routine. For instance, simply printing a message to a stream-style data structure such as stdout (C) or cout (C++) can lead to re-entrancy issues.

Suppose you have code that uses a volatile sig_atomic_t flag variable, perhaps to catch SIGINT, something like this (see also http://pubs.opengroup.org/onlinepubs/007904975/functions/sigaction.html):

volatile sig_atomic_t got_interrupted = 0;
void caught_signal(int unused) {
    got_interrupted = 1;
}
...
    struct sigaction sa;
    sa.sa_handler = caught_signal;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    if (sigaction(SIGINT, &sa, NULL) == -1) ... handle error ...
    ...

Now, in the main body of your code, you might want to "run until interrupted":

    while (!got_interrupted) {
         ... do some work ...
    }

This is fine up until you start needing to make calls that wait for some input/output, such as select or poll. The "wait" action needs to wait for that I/O—but it also needs to wait for a SIGINT interrupt. If you just write:

    while (!got_interrupted) {
        ... do some work ...
        result = select(...); /* or result = poll(...) */
    }

then it's possible that the interrupt will happen just before you call select() or poll(), rather than afterward. In this case, you did get interrupted—and the variable got_interrupted gets set—but after that, you start waiting. You should have checked the got_interrupted variable before you started waiting, not after.

You can try writing:

    while (!got_interrupted) {
        ... do some work ...
        if (!got_interrupted)
            result = select(...); /* or result = poll(...) */
    }

This shrinks the "race window", because now you'll detect the interrupt if it happens while you're in the "do some work" code; but there is still a race, because the interrupt can happen right after you test the variable, but right before the select-or-poll.

The solution is to make the "test, then wait" sequence "atomic", using the signal-blocking properties of sigprocmask (or, in POSIX threaded code, pthread_sigmask):

sigset_t mask, omask;
...
while (!got_interrupted) {
    ... do some work ...
    /* begin critical section, test got_interrupted atomically */
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    if (sigprocmask(SIG_BLOCK, &mask, &omask))
        ... handle error ...
    if (got_interrupted) {
        sigprocmask(SIG_SETMASK, &omask, NULL); /* restore old signal mask */
        break;
    }
    result = pselect(..., &omask); /* or ppoll() etc */
    sigprocmask(SIG_SETMASK, &omask, NULL);
    /* end critical section */
}

(the above code is actually not that great, it's structured for illustration rather than efficiency -- it's more efficient to do the signal mask manipulation slightly differently, and place the "got interrupted" tests differently).

Until you actually start needing to catch SIGINT, though, you need only compare select() and poll() (and if you start needing large numbers of descriptors, some of the event-based stuff like epoll() is more efficient than either one).

Community
  • 1
  • 1
torek
  • 448,244
  • 59
  • 642
  • 775
  • "The fundamental reason for this is that many library calls are not re-entrant" So what happens if a signal is delivered while we're in the middle of say "read". Does this mean we can never use read again? – kptlronyttcna Feb 08 '17 at 06:54
  • @kptlronyttcna: no, and yet also slightly yes: you cannot safely call, e.g., `fread` on stdin in a signal handler. That doesn't mean you can *never* call `fread`, just that you cannot do it on a shared variable in a signal handler. The precise details of what you can do safely, and what you cannot, vary according to the library, system, and other details. (As a side point, when `read` is a system call, as it is on Linux and BSD and Mac, its precise behavior depends on the underlying file-system object. "Slow" devices may return EINTR errors or short reads.) – torek Feb 08 '17 at 07:50
8

Between (p)select and (p)poll is a rather subtle difference:

For select, you have to initialize and populate the ugly fd_set bitmaps everytime before you call select because select modifies them in-place in a "destructive" fashion. (poll distinguishes between the .events and .revents members in struct pollfd).

After selecting, the entire bitmap is often scanned (by people/code) for events even if most of the fds are not even watched.

Third, the bitmap can only deal with fds whose number is less than a certain limit (contemporary implementations: somewhere between 1024..4096), which rules it out in programs where high fds can be easibly attained (notwithstanding that such programs are likely to already use epoll instead).

Craig McQueen
  • 41,871
  • 30
  • 130
  • 181
jørgensen
  • 10,149
  • 2
  • 20
  • 27
  • 5
    My feeling is that `select` should no more be used in newly written programs, but only `poll` [or `ppoll`] (at least on systems like not too old Linuxes where both `select` and `poll` are native system calls). I believe `select` is only for legacy programs... – Basile Starynkevitch Mar 19 '12 at 19:15
  • http://www.kegel.com/c10k.html or simply the fact that the size of `fd_set` is limiting the maximal file descriptor much more tightly than what `poll` permits. – Basile Starynkevitch Nov 18 '12 at 17:25
  • Of course, using `ppoll` could be better, since there is no signal related race (but it is Linux specific). – Basile Starynkevitch Nov 18 '12 at 18:29
6

The accepted answer is not correct vis a vis difference between select and pselect. It does describe well how a race condition between sig-handler and select can arise, but it is incorrect in how it uses pselect to solve the problem. It misses the main point about pselect which is that it waits for EITHER the file-descriptor or the signal to become ready. pselect returns when either of these are ready.Select ONLY waits on the file-descriptor. Select ignores signals. See this blog post for a good working example: https://www.linuxprogrammingblog.com/code-examples/using-pselect-to-avoid-a-signal-race

drlolly
  • 157
  • 2
  • 6
  • 1
    Link above is gone. Web Archive link: https://web.archive.org/web/20190209212144/https://www.linuxprogrammingblog.com/code-examples/using-pselect-to-avoid-a-signal-race – Anemoia Feb 23 '23 at 17:54
1

To make the picture presented by the accepted answer complete following basic fact should be mentioned: both select() and pselect() may return EINTR as stated in their man pages:

EINTR A signal was caught; see signal(7).

This "caught" means that the signal should be recognized as "occurred during the system call execution":
1. If non-masked signal occurs during select/pselect execution then select/pselect will exit.
2. If non-masked signal occurs before select/pselect has been called this will not have any effect and select/pselect will continue waiting, potentially forever.

So if a signal occurs during select/pselect execution we are ok - the execution of select/pselect will be interrupted and then we can test the reason for the exit and discover that is was EINTR and then we can exit the loop.
The real threat that we face is a possibility of signal occurrence outside of select/pselect execution, then we may hang in the system call forever. Any attempt to discover this "outsider" signal by naive means:

if (was_a_signal) {
...
}

will fail since no matter how close this test will be to the call of select/pselect there is always a possibility that the signal will occur just after the test and before the call to select/pselect.
Then, if the only place to catch the signal is during select/pselect execution we should invent some kind of "wine funnel" so all "wine splashes" (signals), even outside of "bottle neck" (select/pselect execution period) will eventually come to the "bottle neck".
But how can you deceive system call and make it "think" that the signal has occurred during this system call execution when in reality it has occurred before?
Easy. Here is our "wine funnel": you just block the signal of interest and by that cause it (if it has occurred at all) waiting outside of the process "for the door to be opened" and you "open the door" (unmask the signal) only when you're prepared "to welcome the guest" (select/pselect is running). Then the "arrived" signal will be recognized as "just occurred" and will interrupt the execution of the system call.
Of course, "opening the door" is the most critical part of the plan - it cannot be done by the usual means (first unmask, then call to select/pselect), the only possibility is to do the both actions (unmask and system call) at once (atomically) - this is what pselect() is capable of but select() is not.

Varyag
  • 11
  • 1