tech-kern archive

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]

SIGPIPE-equivalent before writing?



Sometimes I run (say) git log or hg log with a query that takes a long
time to produce output into my pager, less(1).

When I type `q' to exit the pager, sometimes the producer is still
_computing_ but not _writing_ output.  In this case, it doesn't notice
that its job is done until it next tries to _write_ output and the
write fails with EPIPE/SIGPIPE.  So the producer just hangs doing
computation until it has another byte of output to write -- which will
be summarily discarded as soon as it is written.

This is annoying: after I hit `q', if I want the producer to exit
promptly, I sometimes have to hit ^C or ^\ to interrupt it manually.

How can or should applications do this right, so that the producer can
subscribe to a notification of when the consumer of a pipeline has
(exited and) closed the reading side of the pipe, instead of only
getting a notification when the producer next tries to write output?


Answers, with references, accepted for either: (a) what actually works
reliably (not just by accident) on NetBSD and other operating systems,
(b) what is justified by the letter of POSIX, or (c) what is justified
by historical design intent.  Bonus if it also works reliably for
sockets, named pipes, ptys, and ttys.


Here are a few options I considered:

1. Do select(2) in another thread in the producer.

   But this doesn't make sense to me because the writer side of the
   pipe is:

   - not even open for reading, so surely it should never be readable,
     and the readability state doesn't change when the peer is closed;

   - often available for writing, when the producer isn't yet ready to
     write, so this would turn into a busy-wait; and

   - inapplicable for the select(2) exceptional conditions (which are,
     as far as I can tell, exclusively used for the obscure TCP OOB
     data feature which we appear to have zero tests for so I would be
     a little surprised if it works at all).

   Surprisingly, this does deliver a notification for readable!  But
   I'm not convinced this isn't an accident.  Why would select(2) on
   the _writer_ side for _readability_ report readable or wake on
   changes to the readability state?  It might make sense for a
   bidirectional socket, but surely not for a unidirectional pipe!

2. Do poll(2) in another thread in the producer.

   But with which events requested?

   - 0 simply waits forever because it doesn't subscribe to any
     notifications[2].

   - POLLOUT is often ready, so this would lead to a busy-wait.

   - POLLHUP is, under POSIX[1], ignored in struct pollfd::events.
     And in NetBSD, setting it alone doesn't lead to any wakeups[2].

   - Ditto POLLERR.

   - POLLWRNORM/POLLRDNORM are treated equivalently to POLLOUT/POLLIN
     by pipes so they don't affect anything.

   - POLLWRBAND/POLLRDBAND aren't used by pipes, so they just hang.

   - POLLIN (or POLLRDNORM) works to receive a POLLHUP notification
     but I think it only works by accident.  In particular:
     (a) I see no intentional reason why the pipe logic even accepts
         POLLIN for the writer side of a pipe[3].
     (b) POLLHUP on a pipe is, under POSIX[4], only for a _reader_
         to receive when all _writers_ have been closed.  And that's
         the opposite of this scenario.
     (c) read() would return -1 before and after the event, so I think
         under POSIX, POLLERR should be immediately reported in this
         case[5].
     And it seems that pipe close issues pipeselwakeup(..., POLL_HUP)
     for _either_ side of the pipe, which delivers POLLHUP, and, if
     SIGIO is enabled, SIGIO with .si_code = POLL_HUP[6].

3. Subscribe to SIGIO with fcntl(F_SETOWN) and O_ASYNC.

   But, while for a pipe it does deliver a siginfo_t with .si_code =
   POLL_HUP (for a socket, it's POLL_IN, and for a pty and maybe any
   tty, it's SI_NOINFO!), I'm also not convinced this isn't an
   accident, for the same reasons as with poll(2) and POLLHUP.

The attached program demonstrates these use cases, depending on the
choice of cpp macros set at the top.

We have a collection of tests for cases similar to this in poll(2) at
tests/lib/libc/sys/t_poll.c, with various tests marked xfail, but I'm
not yet convinced the tests are right anyway -- they're more of a
starting point for recording and testing what is right once Someone^TM
figures it out.


[1] `POLLHUP  ... This flag is only valid in the revents bitmask; it
     shall be ignored in the events member.'

    https://pubs.opengroup.org/onlinepubs/9799919799/functions/poll.html

[2] 850 		if (events & (POLLIN | POLLRDNORM))
    851 			selrecord(curlwp, &rpipe->pipe_sel);
    852 
    853 		if (events & (POLLOUT | POLLWRNORM))
    854 			selrecord(curlwp, &wpipe->pipe_sel);

    https://nxr.NetBSD.org/xref/src/sys/kern/sys_pipe.c?r=1.168#850

    I.e., in order to receive wakeups for poll(2) on a pipe, one of
    POLLIN, POLLOUT, POLLRDNORM, or POLLWRNORM must be set in struct
    pollfd::events.

[3] The logic is, however, very confusing, with variables `rpipe' and
    `wpipe' each used to represent _either_ side of the pipe -- rpipe
    for the reader side in pipe_read but the writer side in
    pipe_write, and wpipe for the writer side in pipe_read but the
    reader side in pipe_write!

    Pipe creation -- note that which side is assigned to which:

    206 int
    207 pipe1(struct lwp *l, int *fildes, int flags)
    208 {
    209 	struct pipe *rpipe, *wpipe;
    210 	struct timespec nt;
    211 	file_t *rf, *wf;
...
    238 	rf->f_flag = FREAD | flags;
    239 	rf->f_type = DTYPE_PIPE;
    240 	rf->f_pipe = rpipe;
    241 	rf->f_ops = &pipeops;
...
    245 	wf->f_flag = FWRITE | flags;
    246 	wf->f_type = DTYPE_PIPE;
    247 	wf->f_pipe = wpipe;
    248 	wf->f_ops = &pipeops;

    https://nxr.NetBSD.org/xref/src/sys/kern/sys_pipe.c?r=1.168#203

    But in pipe_read and pipe_write alike, fp->f_pipe is called
    `rpipe', and in pipe_write, fp->f_pipe->pipe_peer is called `wpipe':

    406 static int
    407 pipe_read(file_t *fp, off_t *offset, struct uio *uio, kauth_cred_t cred,
    408     int flags)
    409 {
    410 	struct pipe *rpipe = fp->f_pipe;

    https://nxr.NetBSD.org/xref/src/sys/kern/sys_pipe.c?r=1.168#406

    563 static int
    564 pipe_write(file_t *fp, off_t *offset, struct uio *uio, kauth_cred_t cred,
    565     int flags)
    566 {
    567 	struct pipe *wpipe, *rpipe;
...
    573 	/* We want to write to our peer */
    574 	rpipe = fp->f_pipe;
    575 	lock = rpipe->pipe_lock;
    576 	error = 0;
    577 
    578 	mutex_enter(lock);
    579 	wpipe = rpipe->pipe_peer;

    https://nxr.NetBSD.org/xref/src/sys/kern/sys_pipe.c?r=1.168#563

    I'm frankly amazed that the code works at all for pipe_write where
    the variables are named backwards, and even moreso for pipe_poll,
    pipe_kqfilter, and pipe_ioctl which are used for both the reader
    side and the writer side!

    Amazingly, this baffling use of variable names is the same in
    NetBSD, FreeBSD, and OpenBSD, apparently all having come from John
    Dyson's 1996 sys_pipe.c in FreeBSD:

    commit 10c5615c1dac5fc0f66d25b47b5590ec0fdd37a6
    Author: John Dyson <dyson%FreeBSD.org@localhost>
    Date:   Sun Jan 28 23:38:26 1996 +0000

        Added new files to support the new fast pipes.  After the follow-on
        commits, pipe performance should increase significantly.  The pipe(2)
        system call is currently supported, while fifofs will be added later.

    diff --git a/sys/kern/sys_pipe.c b/sys/kern/sys_pipe.c
    new file mode 100644
    index 00000000000..ea456880416
    --- /dev/null
    +++ b/sys/kern/sys_pipe.c
    @@ -0,0 +1,573 @@
...
    +int
    +pipe(p, uap, retval)
    +	struct proc *p;
    +	struct pipe_args /* {
    +		int	dummy;
    +	} */ *uap;
    +	int retval[];
    +{
...
    +	rf->f_flag = FREAD | FWRITE;
    +	rf->f_type = DTYPE_PIPE;
    +	rf->f_ops = &pipeops;
    +	rf->f_data = (caddr_t)rpipe;
...
    +	wf->f_flag = FREAD | FWRITE;
    +	wf->f_type = DTYPE_PIPE;
    +	wf->f_ops = &pipeops;
    +	wf->f_data = (caddr_t)wpipe;
...
    +static int
    +pipe_read(fp, uio, cred)
    +	struct file *fp;
    +	struct uio *uio;
    +	struct ucred *cred;
    +{
    +
    +	struct pipe *rpipe = (struct pipe *) fp->f_data;
...
    +static int
    +pipe_write(fp, uio, cred)
    +	struct file *fp;
    +	struct uio *uio;
    +	struct ucred *cred;
    +{
    +	struct pipe *rpipe = (struct pipe *) fp->f_data;
    +	struct pipe *wpipe = rpipe->pipe_peer;

    https://cgit.freebsd.org/src/commit/?id=10c5615c1dac5fc0f66d25b47b5590ec0fdd37a6

[4] `POLLHUP  ... a pipe or FIFO has been closed by the last process
     that had it open for writing.  Once set, the hangup state of a
     FIFO shall persist until some process opens the FIFO for writing
     or until all read-only file descriptors for the FIFO are closed.'

    https://pubs.opengroup.org/onlinepubs/9799919799/functions/poll.html

[5] `POLLERR  An error condition is present on the file descriptor.
     All error conditions that arise solely from the state of the
     object underlying the open file description and would be
     diagnosed by a return of -1 from a read() or write() call on the
     file descriptor shall be reported as a POLLERR event.'

    https://pubs.opengroup.org/onlinepubs/9799919799/functions/poll.html

[6] 368 /*
    369  * Select/poll wakeup. This also sends SIGIO to peer connected to
    370  * 'sigpipe' side of pipe.
    371  */
    372 static void
    373 pipeselwakeup(struct pipe *selp, struct pipe *sigp, int code)
    374 {
...
    377 	switch (code) {
...
    384 	case POLL_HUP:
    385 		band = POLLHUP;
    386 		break;
...
    396 	}
    397 
    398 	selnotify(&selp->pipe_sel, band, NOTE_SUBMIT);
    399 
    400 	if (sigp == NULL || (sigp->pipe_state & PIPE_ASYNC) == 0)
    401 		return;
    402 
    403 	fownsignal(sigp->pipe_pgid, SIGIO, code, band, selp);
    404 }
...
    956 /*
    957  * Shutdown the pipe.
    958  */
    959 static void
    960 pipeclose(struct pipe *pipe)
    961 {
...
    979 	pipeselwakeup(pipe, pipe, POLL_HUP);

    https://nxr.NetBSD.org/xref/src/sys/kern/sys_pipe.c?r=1.168#956
#define	OPEN_PIPE		1
#define	OPEN_PTYAPP		0
#define	OPEN_PTYHOST		0
#define	OPEN_SOCKET		0

#define	POLL_FOR_EVENTS		(POLLIN|POLLRDNORM)

#define	SELECT_FOR_EXCEPT	0
#define	SELECT_FOR_READ		1
#define	SELECT_FOR_WRITE	0

#define	USE_SIGIO		0
#define	SIGIO_SIGACTION_FLAGS	(SA_RESTART)

#define	WAIT_WITH_POLL		1
#define	WAIT_WITH_SELECT	0
#define	WAIT_WITH_SIGSUSPEND	0

#if OPEN_PIPE + OPEN_PTYAPP + OPEN_PTYHOST + OPEN_SOCKET != 1
#  error Set one of OPEN_PIPE, OPEN_PTYHOST, OPEN_PTYAPP, or OPEN_SOCKET.
#endif
#if SELECT_FOR_READ + SELECT_FOR_WRITE + SELECT_FOR_EXCEPT != 1
#  error Set one of SELECT_FOR_READ, SELECT_FOR_WRITE, or SELECT_FOR_EXCEPT.
#endif
#if WAIT_WITH_POLL + WAIT_WITH_SELECT + WAIT_WITH_SIGSUSPEND != 1
#  error Set one of WAIT_WITH_POLL, WAIT_WITH_SELECT< or WAIT_WITH_SIGSUSPEND.
#endif

#define	_NETBSD_SOURCE
#define	_XOPEN_SOURCE	600

#include <sys/select.h>
#include <sys/socket.h>
#include <sys/wait.h>

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>

void
on_sigio(int signo, siginfo_t *si, void *ctx)
{

	fprintf(stderr, "SIGIO si_code=%d\n", si->si_code);
}

#if OPEN_PTYHOST + OPEN_PTYAPP > 0
static void
openptypair(int *hostfd, int *appfd)
{
	struct termios t;
	char *pts;

	if ((*hostfd = posix_openpt(O_RDWR|O_NOCTTY)) == -1)
		err(EXIT_FAILURE, "posix_openpt");
	if (grantpt(*hostfd) == -1)
		err(EXIT_FAILURE, "grantfd");
	if (unlockpt(*hostfd) == -1)
		err(EXIT_FAILURE, "unlockpt");
	if ((pts = ptsname(*hostfd)) == NULL)
		err(EXIT_FAILURE, "ptsname");
	if ((*appfd = open(pts, O_RDWR|O_NOCTTY)) == -1)
		err(EXIT_FAILURE, "open(%s)", pts);
	if (tcgetattr(*appfd, &t) == -1)
		err(EXIT_FAILURE, "tcgetattr");
	t.c_lflag &= ~ICANON;	/* block rather than drop input */
	if (tcsetattr(*appfd, TCSANOW, &t) == -1)
		err(EXIT_FAILURE, "tcsetattr");
}
#endif

int
main(void)
{
	int pipefd[2];
	pid_t child;

#if OPEN_SOCKET
	if (socketpair(AF_LOCAL, SOCK_STREAM, 0, pipefd) == -1)
		err(EXIT_FAILURE, "socketpair");
#elif OPEN_PIPE
	if (pipe(pipefd) == -1)
		err(EXIT_FAILURE, "pipe");
#elif OPEN_PTYHOST
	openptypair(&pipefd[1], &pipefd[0]);
#elif OPEN_PTYAPP
	openptypair(&pipefd[0], &pipefd[1]);
#else
#  error Set OPEN_SOCKET, OPEN_PIPE, or OPEN_PTY.
#endif
	if ((child = fork()) == -1)
		err(EXIT_FAILURE, "fork");
	if (child == 0) {
		if (close(pipefd[1]) == -1)
			err(EXIT_FAILURE, "[child] close write side");
		fprintf(stderr, "[child] sleep\n");
		sleep(1);
		fprintf(stderr, "[child] close\n");
		if (close(pipefd[0]) == -1)
			err(EXIT_FAILURE, "[child] close read side");
		fprintf(stderr, "[child] exit\n");
		_exit(0);
	}
	if (close(pipefd[0]) == -1)
		err(EXIT_FAILURE, "close read side");

	alarm(5);

#if USE_SIGIO
	struct sigaction sa;

	sa.sa_sigaction = &on_sigio;
	sa.sa_flags = SA_SIGINFO|SIGIO_SIGACTION_FLAGS;
	if (sigemptyset(&sa.sa_mask) == -1)
		err(EXIT_FAILURE, "sigfillset");
	if (sigaddset(&sa.sa_mask, SIGIO) == -1)
		err(EXIT_FAILURE, "sigaddset");
#  if WAIT_WITH_SIGSUSPEND
	sigset_t omask;
	if (sigprocmask(SIG_BLOCK, &sa.sa_mask, &omask) == -1)
		err(EXIT_FAILURE, "sigprocmask");
#  endif
	if (sigaction(SIGIO, &sa, NULL) == -1)
		err(EXIT_FAILURE, "sigaction");

	if (fcntl(pipefd[1], F_SETOWN, getpid()) == -1)
		err(EXIT_FAILURE, "fcntl(F_SETOWN)");

	int fl;
	if ((fl = fcntl(pipefd[1], F_GETFL)) == -1)
		err(EXIT_FAILURE, "fcntl(F_GETFL)");
	fl |= O_ASYNC;
	if (fcntl(pipefd[1], F_SETFL, fl) == -1)
		err(EXIT_FAILURE, "fcntl(F_SETFL)");

	fprintf(stderr, "SIGIO requested\n");
#endif

#if WAIT_WITH_SELECT
	fd_set rfd, wfd, efd;

	for (;;) {
		FD_ZERO(&rfd);
		FD_ZERO(&wfd);
		FD_ZERO(&efd);
#  if SELECT_FOR_READ
		FD_SET(pipefd[1], &rfd);
#  elif SELECT_FOR_WRITE
		FD_SET(pipefd[1], &wfd);
#  elif SELECT_FOR_EXCEPT
		FD_SET(pipefd[1], &efd);
#  else
#    error Set SELECT_FOR_READ, SELECT_FOR_WRITE, or SELECT_FOR_EXCEPT.
#  endif
		if (FD_ISSET(pipefd[1], &rfd))
			fprintf(stderr, "ask readable\n");
		if (FD_ISSET(pipefd[1], &wfd))
			fprintf(stderr, "ask writable\n");
		if (FD_ISSET(pipefd[1], &efd))
			fprintf(stderr, "ask errable\n");
		if (select(pipefd[1] + 1, &rfd, &wfd, &efd, NULL) == -1) {
			if (errno != EINTR)
				err(EXIT_FAILURE, "select");
			fprintf(stderr, "restart interrupted select\n");
			continue;
		}
		break;
	}
	fprintf(stderr, "selected\n");
	if (FD_ISSET(pipefd[1], &rfd))
		fprintf(stderr, "readable\n");
	if (FD_ISSET(pipefd[1], &wfd))
		fprintf(stderr, "writable\n");
	if (FD_ISSET(pipefd[1], &efd))
		fprintf(stderr, "errable\n");
#elif WAIT_WITH_POLL
	struct pollfd pfd;

	memset(&pfd, 0, sizeof(pfd));
	pfd.fd = pipefd[1];
	pfd.events = POLL_FOR_EVENTS;
	fprintf(stderr, "poll\n");
	while (poll(&pfd, 1, -1) == -1) {
		if (errno != EINTR)
			err(EXIT_FAILURE, "poll");
		fprintf(stderr, "restart interrupted poll\n");
	}
	fprintf(stderr, "events 0x%x\n", pfd.revents);
#elif WAIT_WITH_SIGSUSPEND
	if (sigsuspend(&omask) == -1) {
		if (errno != EINTR)
			err(EXIT_FAILURE, "sigsuspend");
	}
#else
#  error Set WAIT_WITH_SELECT, WAIT_WITH_POLL, or WAIT_WITH_SIGSUSPEND.
#endif

	int status;
	fprintf(stderr, "wait for child\n");
	if (waitpid(child, &status, 0) == -1)
		err(EXIT_FAILURE, "waitpid");
	fprintf(stderr, "child exited 0x%x\n", status);

	return 0;
}


Home | Main Index | Thread Index | Old Index