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