POSIX Signals Handle SIGPIPE generated by write() in a thread-safe manner


Example

When write() is called for a named or unnamed pipe or stream socket whose reading end is closed, two things happen:

POSIX.1-2001
  1. SIGPIPE signal is sent to the process that called write()
POSIX.1-2004
  1. SIGPIPE signal is sent to the thread that called write()
  1. EPIPE error is returned by write()

There are several ways to deal with SIGPIPE:

  • For sockets, SIGPIPE may be disabled by setting platform-specific options like MSG_NOSIGNAL in Linux and SO_NOSIGPIPE in BSD (works only for send, but not for write). This is not portable.
  • For FIFOs (named pipes), SIGPIPE will not be generated if writer uses O_RDWR instead of O_WRONLY, so that reading end is always opened. However, this disables EPIPE too.
  • We can ignore SIGPIPE or set global handler. This is a good solution, but it's not acceptable if you don't control the whole application (e.g. you're writing a library).
  • With recent POSIX versions, we can use the fact that SIGPIPE is send to the thread that called write() and handle it using synchronous signal handling technique.

Code below demonstrates thread-safe SIGPIPE handling for POSIX.1-2004 and later.

It's inspired by this post:

  • First, add SIGPIPE to signal mask of current thread using pthread_sigmask().
  • Check if there is already pending SIGPIPE using sigpending().
  • Call write(). If reading end is closed, SIGPIPE will be added to pending signals mask and EPIPE will be returned.
  • If write() returned EPIPE, and SIGPIPE was not already pending before write(), remove it from pending signals mask using sigtimedwait().
  • Restore original signal mask using pthread_sigmask().

Source code:

#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <sys/signal.h>

ssize_t safe_write(int fd, const void* buf, size_t bufsz)
{
    sigset_t sig_block, sig_restore, sig_pending;

    sigemptyset(&sig_block);
    sigaddset(&sig_block, SIGPIPE);

    /* Block SIGPIPE for this thread.
     *
     * This works since kernel sends SIGPIPE to the thread that called write(),
     * not to the whole process.
     */
    if (pthread_sigmask(SIG_BLOCK, &sig_block, &sig_restore) != 0) {
        return -1;
    }

    /* Check if SIGPIPE is already pending.
     */
    int sigpipe_pending = -1;
    if (sigpending(&sig_pending) != -1) {
        sigpipe_pending = sigismember(&sig_pending, SIGPIPE);
    }

    if (sigpipe_pending == -1) {
        pthread_sigmask(SIG_SETMASK, &sig_restore, NULL);
        return -1;
    }

    ssize_t ret;
    while ((ret = write(fd, buf, bufsz)) == -1) {
        if (errno != EINTR)
            break;
    }

    /* Fetch generated SIGPIPE if write() failed with EPIPE.
     *
     * However, if SIGPIPE was already pending before calling write(), it was
     * also generated and blocked by caller, and caller may expect that it can
     * fetch it later. Since signals are not queued, we don't fetch it in this
     * case.
     */
    if (ret == -1 && errno == EPIPE && sigpipe_pending == 0) {
        struct timespec ts;
        ts.tv_sec = 0;
        ts.tv_nsec = 0;

        int sig;
        while ((sig = sigtimedwait(&sig_block, 0, &ts)) == -1) {
            if (errno != EINTR)
                break;
        }
    }

    pthread_sigmask(SIG_SETMASK, &sig_restore, NULL);
    return ret;
}