Yep. I've tried to keep the examples simple. Cache coherence related questions are kind of out of scope of the article.
There's also pselect/ppoll/epoll_pwait, which can be used together with non-blocking I/O to catch signals without race conditions.
How exactly would you use it to deal with the Ctrl+C problem described in the article?
set file descriptor to non-blocking;
sigprocmask() to block SIGINT;
if (stop) { // handle it }
while (1) {
pselect() with sigmask argument which doesn't block SIGINT;
if (stop) { // handle it }
recv();
}
The magic here is that signals are blocked except during the pselect(), and pselect() atomically unblocks them when it's called and blocks them when it returns. As a result, the signal handler can only execute during the pselect(), so it is safe to only check the flag after pselect() returns.
From linux.die.ne t/man/2/pselect
The reason that pselect() is needed is that if one wants to wait for either a signal or for a file descriptor to become ready, then an atomic test is needed to prevent race conditions. (Suppose the signal handler sets a global flag and returns. Then a test of this global flag followed by a call of select() could hang indefinitely if the signal arrived just after the test but just before the call. By contrast, pselect() allows one to first block signals, handle the signals that have come in, then call pselect() with the desired sigmask, avoiding the race.
As far as I understand, then the Ctrl+C would not work when we don't happen to be waiting in a blocking function. That seems even worse than doing it the other way round.
The only blocking function here is pselect(). If you get Ctrl+C during a pselect(), it will immediately return (because you have unblocked signals with the sigmask argument to pselect). On the other hand, if you get Ctrl+C while you're not in pselect(), the signal handler will not run until you call pselect() (because signals are blocked), and when you finally call pselect(), it will run and pselect() will return.
Yes. In the tight event loop it would work OK.
However, consider this example (pretty common ZeroMQ use case): User calls zmq_recv (a blocking function) to get some work. Then he processes the work. With the pselect solution, the program would be interruptible while waiting for a message (say 0.1 sec) but not interruptible while processing the work (say 1 hour).
I don't see a need to keep signals blocked outside zmq_recv(). When zmq_recv() is called:
1. block signals
2. check if signal is pending (this is atomic because signals are blocked)
3. pselect()
5. recv()
6. if we need more data, goto 2 (yes, 2, so we don't miss a signal)
7. else unblock signals and return data to user
It can never happen that a signal is received and we wait indefinitely in pselect().
Nice! I wasn't aware of the trick. I'll mention it in the article.
Still, it doesn't work for libraries. Unless, of course, ZeroMQ would expose zmq_precv (…, const sigset_t *sigmask) in addition to standard zmq_recv.
Huh?? How is the 3rd listing supposed to behave differently from the 2nd one??
You've just added a check on the return value & errno, but if the recv call is exiting with EINTR, you should have exact the same behaviour in both listings regardless of if you check its return value or not.
Besides that, this doesn't seem to work anyway. Both on a Linux (Debian) and a Darwin (OSX) system, recv doesn't seem to exit with EINTR once a signal handler has been installed, so I still get the same "stuck-until-some-data-arrives" condition.
Am I missing something?
The assumption for the two first listings is "let's pretend there's no EINTR and recv just continues waiting for data whatever happens".
The first two listings are trying to explain why EINTR error exists at all and explore what would happen if it doesn't exist. EINTR is considered only from 3rd listing on.
Ok, I think I found the problem: it seems that if you install a signal handler without setting the SA_RESTART flag to 0 (which you can only do if you use sigaction to install the handler) then at the exit of the signal handler, any interruptible syscall (such as recv) is restarted automatically instead of exiting with EINTR.
So, I think the example should be expanded showing how to install the SIGINT handler by using sigaction. In any case, I insist that listing2 and listing3 have no different behavior.
How would SA_RESTART help? The problem here is that you have to somehow force the blocking function to exit when signal happens. Setting SA_RESTART does the exact opposite.
Ah, sorry. I've misunderstood you. Yes, you are right. SA_RESTART has to be set to 0 to get the EINTR behaviour. I'll make a note in the article.
Another solution is to use the so-called "self-pipe trick" where you have the signal handler write a byte to a pipe. You use non-blocking sockets and in the select() you always monitor your sockets and the read end of the pipe. There is no race condition - when your signal happens, you very soon be able to read from this pipe (next time you call select, or immediately if select is being called). However, be sure to set both ends of the pipe to non-blocking. Otherwise the signal handler could block if the pipe buffer fills up and the program would deadlock.
But this is of course only applicable to your Python problem if there is a way to change what the signal handler does. Or if you're very evil you can try to override the signal handler temporarily.
Yes, that would work if you are in control of the handler (send is in the POSIX list of signal-safe functions). Unfortunately, that's not the case for libraries.
write(2) a single byte to a pipe file descriptor from the signal handler and use whatever async I/O tech in your event loop (select(2), poll(2), libev, libevent, ….).
Or give up on portability and use some API that solves this proble, like, say, Solaris event ports (just post an event from the handler, or put the port into alert mode from the handler).
As for condition variables… if only pthread_mutex_lock() were async-signal-safe… then you could acquire the mutex, signal/broadcast on the cv, and drop the lock. But it's not, so you can't. What you can do is resort to having a thread waiting for events on a pipe written to by the signal handler (see above!) then have that thread signal/broadcast on you condition variable.
See? The trick is to transmogrify async signals into events that you can wait for with traditional async I/O APIs.
Except that messing with signal handlers is not an option when you are implementing a library. Signal handlers are set by the main module and should not be changed randomly in the background by the libraries.
On Windows this is done with SetConsoleCtrlHandler() call.
Receive is the easy case.
What happens if you have a protocol where you call "write".
Is there any guarantee that you cannot get EINTR if the packet has been transmitted?
If you retry sending the packet, then it is sent TWICE, which may confuse the other end.
True. As far as I know there is no guarantee that the operation haven't [partially] succeeded when EINTR is returned.