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