tech-kern archive

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

Re: [PATCH] SIGPIPE-equivalent before writing?



> Date: Mon, 21 Sep 2026 16:08:01 +0000
> From: Taylor R Campbell <riastradh%NetBSD.org@localhost>
> 
> 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?

The attached program demonstrates a technique that seems to work in
both Linux and NetBSD, by accepting either POLLERR _or_ POLLHUP.  Pipe
the output of `./yesdelay' (which works like yes(1) but with 5sec
delays after every page of output to simulate computation) into
less(1), and `q' kills the pipeline instantaneously.

I'm convinced NetBSD is wrong to return POLLHUP, and I suspect it is
because of the historic confusion of the variables `rpipe' and `wpipe'
in the sys_pipe.c code, both of which refer at different times to the
reading side of a pipe, and both of which refer at different times to
the writing side of a pipe.

I'm also mostly convinced that Linux is right to return POLLERR for a
unidirectional pipe, following the argument I gave at
<https://mail-index.NetBSD.org/tech-kern/2026/09/21/msg031255.html>.

I drafted a patch series to disentangle the sys_pipe.c confusion,
delete some unnecessary logic (and replace it by more extensive
comments), make it easier to reason about, deliver POLLERR (via
poll(2) or SIGIO) to the writer when the reader is closed, and
incidentally fix several pipe/poll bugs I had found in the past and
added tests for but never got around to fixing:

- Change-by-change patch series:
  https://www.NetBSD.org/~riastradh/tmp/20260923/pr57659-pr59056-pipehacks-v2.patch
- End-to-end giant diff:
  https://www.NetBSD.org/~riastradh/tmp/20260923/pr57659-pr59056-pipehacks-v2.diff

Two bugs that this patch series fixes:

- PR kern/57659: closing pipe writefd fails to wake concurrent write
  on same writefd
  https://gnats.NetBSD.org/57659

- PR kern/59056: poll POLLHUP bugs
  https://gnats.NetBSD.org/59056

However, that leaves open the question for sockets, named fifos, and
ttys, which are bidirectional all behave slightly differently.

Also, SIGIO will be delivered whenever output buffer space becomes
available, not just when the reading end of stdout has been closed.
So this technique might have a performance impact on high-volume
pipelines or on processes with many file descriptors (delivering SIGIO
requires iterating over the process's fd table in order to fill in
siginfo_t::si_fd).
#include <sys/ioctl.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 <time.h>
#include <unistd.h>

int ttyfd;

static void
on_sigio(int signo, siginfo_t *si, void *ctx)
{
	struct pollfd pfd;

	memset(&pfd, 0, sizeof(pfd));
	pfd.fd = STDOUT_FILENO;
	pfd.events = POLLOUT|POLLWRNORM;
	if (poll(&pfd, 1, 0) == 1 && pfd.revents & (POLLERR|POLLHUP))
		_exit(1);
}

static void
exit_on_stdout_done(void)
{
	struct sigaction sa;
	int flags;

	/*
	 * Register a SIGIO signal handler to exit if STDOUT_FILENO's
	 * receiving end has been closed.
	 */
	memset(&sa, 0, sizeof(sa));
	sa.sa_sigaction = &on_sigio;
	sa.sa_flags = SA_SIGINFO|SA_RESTART;
	if (sigemptyset(&sa.sa_mask) == -1)
		err(EXIT_FAILURE, "sigemptyset");
	if (sigaction(SIGIO, &sa, NULL) == -1)
		err(EXIT_FAILURE, "sigaction(SIGIO)");

	/*
	 * Request SIGIO from STDOUT_FILENO.
	 */
	if (fcntl(STDOUT_FILENO, F_SETOWN, getpid()) == -1)
		err(EXIT_FAILURE, "fcntl(F_SETOWN)");
	flags = fcntl(STDOUT_FILENO, F_GETFL);
	if (flags == -1)
		err(EXIT_FAILURE, "fcntl(F_GETFL)");
	if (fcntl(STDOUT_FILENO, F_SETFL, flags|O_ASYNC) == -1)
		err(EXIT_FAILURE, "fcntl(F_SETFL)");
}

int
main(int argc, char **argv)
{
	const char *const yes = (argc > 1 ? argv[1] : "y");
	struct winsize ws;
	unsigned i;
	int timo;

	/*
	 * Get the terminal dimensions so we can estimate how to fill a
	 * page of output in the pager.
	 */
	ttyfd = open("/dev/tty", O_RDWR);
	if (ttyfd == -1)
		err(EXIT_FAILURE, "open(/dev/tty)");
	if (ioctl(ttyfd, TIOCGWINSZ, &ws) == -1)
		err(EXIT_FAILURE, "ioctl(TIOCGWINSZ)");

	/*
	 * Ask to exit promptly if the reading end of stdout has been
	 * closed.
	 */
	exit_on_stdout_done();

	/*
	 * Print a page of output at a time.  Then pretend to do some
	 * heavy computation (but in reality just take a nap) before
	 * producing any further output.
	 */
	for (;;) {
		for (i = 0; i < ws.ws_row; i++)
			printf("%s\n", yes);
		fflush(stdout);
		if (ferror(stdout))
			err(EXIT_FAILURE, "fflush");

		for (timo = 5; timo > 0; timo = sleep(timo))
			continue;
	}
}


Home | Main Index | Thread Index | Old Index