tech-kern archive
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]
Re: SIGPIPE-equivalent before writing?
> Date: Mon, 21 Sep 2026 09:25:27 -0700
> From: Adrian Chadd <adrian%freebsd.org@localhost>
>
> [snip] yup, this is something that's been annoying both in POSIX pipes
> and sockets (think half closed sockets, sigh) since forever.
>
> You're right, you don't get a notification about the write end of a
> pipe or socket being unavailable until (a) you're bidirectional
> and get read ready + EOF (but for half closed sockets that's not
> always a guarantee that you should finish writing), or (b) it's time
> to write a byte and the other end is finally seen as dead.
>
> So I don't think there's a POSIX way to do it.
Can you argue that POSIX does not require poll(2) with .events=0 on
the write side to wake with POLLERR in .revents when the read side is
closed?
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. 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
Argument for: Before the read side is closed, write(2) would not fail
immediately -- it may succeed, or it may block, but it won't fail
immediately. But we're not asking about POLLOUT, so poll(2) should
block instead of returning immediately. When the read side is closed,
this state changes, so poll(2) should wake up then with .revents =
POLLERR.
Argument against: By the same logic, before the read side is closed,
read(2) would fail immediately, so poll(2) should return immediately
without blocking and with .revents = POLLERR in this case even though
we didn't ask about POLLOUT. (Counter-argument against: the write
side of a pipe is only open for write, so surely whatever read(2)
would do shouldn't affect this logic anyway -- that would make it
impossible to use poll(2) on any unidirectional pipes.)
> You may be able to craft something up using kqueue? I wonder if
> FreeBSD's kqueue implementation does something when this happens,
> like set KQ_EOF or similar on the write side of a pipe.
How would you use kqueue to do this?
- Would you use EVFILT_READ on the write side of a pipe?
Shouldn't that fail to do anything because read(2) will _always_
fail immediately with EBADF because the file descriptor is not open
for reading?
[EBADF]
The fildes argument is not a valid file descriptor open
for reading.
https://pubs.opengroup.org/onlinepubs/9799919799/functions/read.html#tag_17_476
- Would you use EVFILT_WRITE?
Doesn't that entail a busy-wait for EV_EOF, because -- unless the
pipe buffer is full -- the producer can often write without blocking
while it's doing computation?
- Would it make a difference if you're using a socket, named pipe,
tty, or pty?
I attached a change to the pipehup.c example program to try kevent
too, and while EVFILT_READ empirically seems to produce the desired
behaviour, I'm still not convinced it's not an accident.
#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 KEVENT_FILTER EVFILT_READ
#define KEVENT_FLAGS 0
#define USE_SIGIO 0
#define SIGIO_SIGACTION_FLAGS (SA_RESTART)
#define WAIT_WITH_KEVENT 1
#define WAIT_WITH_POLL 0
#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_KEVENT + WAIT_WITH_POLL + WAIT_WITH_SELECT + WAIT_WITH_SIGSUSPEND != 1
# error Set one of WAIT_WITH_KEVENT, WAIT_WITH_POLL, WAIT_WITH_SELECT< or WAIT_WITH_SIGSUSPEND.
#endif
#define _NETBSD_SOURCE
#define _XOPEN_SOURCE 600
#include <sys/event.h>
#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_KEVENT
int kq;
struct kevent kev[2];
int i, nev;
if ((kq = kqueue()) == -1)
err(EXIT_FAILURE, "kqueue");
nev = 0;
EV_SET(&kev[nev++], pipefd[1], KEVENT_FILTER, EV_ADD|KEVENT_FLAGS,
/*fflags*/0, /*data*/0, /*udata*/0);
nev = kevent(kq, kev, nev, kev, __arraycount(kev), NULL);
if (nev == -1)
err(EXIT_FAILURE, "kevent");
fprintf(stderr, "pipefd[1]=%d\n", pipefd[1]);
for (i = 0; i < nev; i++) {
fprintf(stderr, "ev[%d] ident=%lld filter=%d flags=0x%x"
" fflags=0x%x data=%p udata=%p\n",
i, (long long)kev[i].ident, kev[i].filter, kev[i].flags,
kev[i].fflags, (void *)kev[i].data, (void *)kev[i].udata);
}
#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