5

Question

Is it possible to disable the raising of a signal (SIGPIPE) when writing to a pipe() FD, without installing my own signal handler or disabling/masking the signal globally?


Background

I'm working on a small library that occasionally creates a pipe, and fork()s a temporary child/dummy process that waits for a message from the parent. When the child process receives the message from the parent, it dies (intentionally).


Problem

The child process, for circumstances beyond my control, runs code from another (third party) library that is prone to crashing, so I can't always be certain that the child process is alive before I write() to the pipe.

This results in me sometimes attempting to write() to the pipe with the child process' end already dead/closed, and it raises a SIGPIPE in the parent process. I'm in a library other customers will be using, so my library must be as self-contained and transparent to the calling application as possible. Installing a custom signal handler could break the customer's code.


Work so far

I've got around this issue with sockets by using setsockopt(..., MSG_NOSIGNAL), but I can't find anything functionally equivalent for pipes. I've looked at temporarily installing a signal handler to catch the SIGPIPE, but I don't see any way to limit its scope to the calling function in my library rather than the entire process (and it's not atomic).

I've also found a similar question here on SO that is asking the same thing, but unfortunately, using poll()/select() won't be atomic, and there's the remote (but possible) chance that the child process dies between my select() and write() calls.


Question (redux)

Is there any way to accomplish what I'm attempting here, or to atomically check-and-write to a pipe without triggering the behavior that will generate the SIGPIPE? Additionally, is it possible to achieve this and know if the child process crashed? Knowing if it crashed lets me build a case for the vendor that supplied the "crashy" library, and lets them know how often it's failing.

Community
  • 1
  • 1
Cloud
  • 18,753
  • 15
  • 79
  • 153
  • 1
    You could set `SIGPIPE` to `SIG_IGN`. – EOF Dec 16 '16 at 18:16
  • 2
    You can use a Unix Domain Socket instead of a pipe too, in that case you will be able to use `setsockopt()`. And I think the resulting code will be simpler and easier to maintain, keeping the functionality intact. – Iharob Al Asimi Dec 16 '16 at 18:18
  • @EOF I'll update the question. Unfortunately, masking off that signal could affect the parent application using my library. – Cloud Dec 16 '16 at 18:21
  • 1
    A a glance, you could do the following: Use `sigaction()` to (atomically AFAIK) change the old signal-handler to a new one that catches the `SIGPIPE`. Inside the handler, check whether the the pipe responsible was the one your library created. If yes, log. Otherwise, change back the signal handler to the old one, `raise(SIGPIPE);`. – EOF Dec 16 '16 at 18:26
  • @EOF So, would the easiest way to do this simply be to use `sigaction()` before my `write()` call, check that the PID of the sender is my own via `getpid()`, and then check if the `FD` in question in `sa.si_fd` is equal to my pipe's write FD? – Cloud Dec 16 '16 at 18:35
  • 1
    @DevNull: That sounds plausible. The main ugliness is that the old signalhandler must be made available to the new signalhandler, which means a global. Also, there *might* be a race condition if, after setting up the signalhandler, another thread causes a `SIGPIPE` *before* the `read()` in your thread. OTOH, I don't think `SIGPIPE` is anywhere *near* that popular. – EOF Dec 16 '16 at 18:38
  • @EOF I just realized that now. So close. – Cloud Dec 16 '16 at 18:40
  • @DevNull: Note that if you were to use an [Unix domain](http://man7.org/linux/man-pages/man7/unix.7.html) [socket pair](http://man7.org/linux/man-pages/man2/socketpair.2.html), you could use [send()](http://man7.org/linux/man-pages/man2/send.2.html) with `MSG_NOSIGNAL` flag to avoid raising `SIGPIPE`. This is standardized in POSIX.1-2008, so it's portable, too. – Nominal Animal Dec 16 '16 at 19:01
  • 1
    Note that `si_fd` is not a member of `siginfo_t` that is mandated by POSIX. – Jonathan Leffler Dec 16 '16 at 19:23

3 Answers3

2

Is it possible to disable the raising of a signal (SIGPIPE) when writing to a pipe() FD [...]?

The parent process can keep its copy of the read end of the pipe open. Then there will always be a reader, even if it doesn't actually read, so the condition for a SIGPIPE will never be satisfied.

The problem with that is it's a deadlock risk. If the child dies and the parent afterward performs a blocking write that cannot be accommodated in the pipe's buffer, then you're toast. Nothing will ever read from the pipe to free up any space, and therefore the write can never complete. Avoiding this problem is one of the purposes of SIGPIPE in the first place.

You can also test whether the child is still alive before you try to write, via a waitpid() with option WNOHANG. But that introduces a race condition, because the child could die between waitpid() and the write.

However, if your writes are consistently small, and if you get sufficient feedback from the child to be confident that the pipe buffer isn't backing up, then you could combine those two to form a reasonably workable system.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • 1
    You could also set the write end as `O_NONBLOCK` -- that way writes will never deadlock, but they might return `EWOULDBLOCK` or `EAGAIN` if the child has died or fallen behind. – Chris Dodd Dec 17 '16 at 22:16
1

After going through all the possible ways to tackle this issue, I discovered there were only two venues to tackle this problem:

  • Use socketpair(PF_LOCAL, SOCK_STREAM, 0, fd), in place of pipes.
  • Create a "sacrificial" sub-process via fork() which is allowed to crash if SIGPIPE is raised.

I went the socketpair route. I didn't want to, since it involved re-writing a fair bit of pipe logic, but it's wasn't too painful.

Thanks!

Cloud
  • 18,753
  • 15
  • 79
  • 153
0

Not sure I follow: you are the parent process, i.e. you write to the pipe. You do so to send a message after a certain period. The child process interprets the message in some way, does what it has to do and exits. You also have to have it waiting, you can't get the message ready first and then spawn a child to handle it. Also just sending a signal would not do the trick as the child has to really act on the content of the message, and not just the "do it" call.

First hack which comes to mind would be that you wont close the read side of the pipe in the parent. That allows you to freely write to the pipe, while not hurting child's ability to read from it.

If this is not fine, please elaborate on the issue.