Mixing file IO and signals

Paul LeoNerd Evans leonerd at leonerd.org.uk
Tue Oct 2 14:16:12 BST 2007


I'm attempting to write a single-thread async. IO program, that is
trying to perform the "Holy Trinity" of async. behaviour... That is, to
handle file IO, signals, and timeouts all at once.

Traditionally, file IO and signals are hard to safely mix. My standard
solution in C code is to hold a pipe(), which the select() or poll()
loop tests for readability, and the signal handler writes signal numbers
into. This guarantees that a select() or poll() loop won't block if a
signal is "pending".

My problem is that Perl is somewhat getting in the way of these. Because
of the safe signal mechanism, when a real SIGCHLD (as is my case)
arrives, Perl simply notes that it has arrived, and will handle it later
in the PERL_ASYNC_CHECK() macro. If that arrives during the XS code that
implements the _poll() function, before the poll() syscall, my real
signal handler won't get called, so the pipe won't get written to, so
the poll() syscall will block. And block it does, causing a timeout,
only after which do I finally get my signal.

Some other solutions to this come to mind:

Solution 1: Unsafe signals

  This involves using POSIX::sigaction to install an unsafe signal
  handler to ensure the pipe gets written to when the signal arrives,
  rather than waiting for PERL_ASYNC_CHECK. I can't think of an easy
  way to do this safely, because it would at least involve a
  POSIX::write() which has the side effect of modifying $!. I can't just
  local'ise it during the handler, because that allocates memory.
  Perhaps the pad of the signal handler closure can help?

    my $dollarbang_save = 0;
    my $signum_str = pack( "C", $signum );

    my $sa = POSIX::SigAction->new( sub {
          $dollarbang_save = $!;
          POSIX::write( $sigpipe_write_fileno, $signum_str );
          $! = $dollarbang_save;
       }, $sigset );

    $sa->safe( 0 );

    POSIX::sigaction( $signum, $sa );

  I don't know enough about unsafe signals to know if that's "safe" to
  do there - perhaps someone can advise? If this isn't safe and can't be
  made safe, perhaps an XS function written in C could stand in for the
  signal handler; this though would have a downside of requiring a C
  compiler, and breaking the "pureperl"-ness of my current work.

Solution 2: Linux-specific ppoll()

  On Linux, there's a syscall ppoll(), which atomically replaces the
  signal process mask with one specified by the call, waits as poll()
  would, then switches the mask back. The usual way to use this is to
  have normally blocked all the signals you care about, then use ppoll()
  to enable them just during the poll(). This avoids the race condition
  when the might occur at some other time.

  Using ppoll() makes it safe to mix a poll()-like call with regular
  flag-based checking of whether signals have arrived. This then avoids
  the need for the signal pipe.

  This has the downside of being Linux-specific though. [1].

Solution 3: SIGIO

  An alternative to the signal pipes, is to turn the code upsidedown,
  and use SIGIO for filehandle notification, and use sigsuspend() as the
  primary blocking primative. Rather than calling select() or poll(),
  you use the SIGIO signal to inform when IO operations can be performed
  on filehandles.
  
  In a Linux C program, you can install a signal handler using
  sigaction() that includes the SA_SIGINFO flag, and when it is fired,
  the handler is passed a struct siginfo_t* which contains information
  about the signal (such as the file descriptor and type of IO
  available, in SIGIO's case). Or alternatively you can call
  sigwaitinfo() or sigtimedwait(), which return the siginfo_t* to you
  anyway. These are both unfortunately Linux-specific.

  To use SIGIO anywhere outside of Linux, you have to only use it as a
  notification that some file IO might be possible, and call poll() or
  select() with a zero timeout. You then install an alarm() timer if you
  want timeouts, then call sigsuspend(). When it comes back, see what
  signals you got. SIGALRM means timeout, SIGIO means you do the usual
  select/poll call, any other signal stands as itself. Seems a little
  messy, but it probably solves the problem.

Alternatively, are there any other solutions anyone might be able to
suggest?

-----

[1]: As a side note, I may at some point anyway implement an
     IO::Poll-like class, IO::Ppoll, for use on Linux. It would have an
     interface compatible with IO::Poll, so like IO::Epoll, can be a
     dropin replacement. It would though support an additional method,
     ->sigmask, which would be a way to specify the signal mask to pass
     to the ppoll() syscall.

-- 
Paul "LeoNerd" Evans

leonerd at leonerd.org.uk
ICQ# 4135350       |  Registered Linux# 179460
http://www.leonerd.org.uk/


More information about the london.pm mailing list