Steve Friedl's Unixwiz.net Tech Tips

Mapping UNIX pipe descriptors to stdin and stdout in C

A common technique in the UNIX world is to create a pair of pipe descriptors,
fork a child process, and then map the child's descriptors to its standard
input and standard output. This allows the child process to run normally,
with stdout and stdin routed to/from the parent process.

Lots of programs do this, but it's often not done correctly, leading to
maddening I/O errors that are hard to reproduce. This Tech Tip talks about
these issues involved and presents a library function that does this
remapping correctly.

UNIX Pipes

The pipe(2) system call returns two file descriptors that
form a "pipe", a one-way communication channel with a "read end"
and a "write end".

It's possible for a program to use a pipe all by itself, but it's
not common.

The "usual" way

A UNIX "pipe" is a pair of file descriptors such that what's written on one
shows up as available data on the other one. For a single process this would
be silly - writing to yourself? - but when this pipe crosses a process boundary
it gets a lot more interesting. To make a two-way communication, you need two
pipes (that's four file descriptors).

Descriptor Debacle

The above code works most of the time, but it's not guaranteed to do
so: pipes typically allocate the two lowest available file descriptors for
putting into the array, and if - for some reason - FD #0 and #1 were both
available (as in a daemon process that had previously closed all of its
unused files), then it's likely that this code will do the wrong thing.

Looking at the child portion of the code, let's imagine that this surprising
file-descriptor allocation happens. What else develops in the code? Let's augment
the "easy" FD macros with the imaginary file descriptors allocated:

The first dup2 attaches the CHILD_READ=2 pipe to the standard input
(fd#0), but fd#0 is actually PARENT_READ! This has the effect of shutting
down the pipe going back to the parent. And if that weren't enough,
the next line actually closes fd#0, which is the child read pipe. This
process has just closed both of its pipe descriptors leading to the
parent - though this is likely very rare, it's also very unhelpful.

Can we do something about this?

Getting it right

The solution here is to pay careful attention to both file descriptors
that are to be attached to the stdin and stdout streams. If neither
of them is 0 or 1 upon entry, it's just a routine pair of dup2
calls. But if either of the file descriptors are in our "target" range,
we have to step much more carefully.

When considering the read and write descriptors, there are only
three classes for each one:

it's 0 - this means it might be in the way of a shuffle

it's 1 - also might be in the way of a shuffle

it's >1 - this is out of danger.

Considering that the read and write file descriptors won't ever be the same
(in this bidirectional pipe example, at least), we end up with a matrix of
all the possible combinations. From this we can plot the actions required
in each specific case:

#define DUP2CLOSE(oldfd, newfd) ( dup2(oldfd, newfd), close(oldfd) )

read FD (should be 0)

write FD (should be 1)

Action

0

1

nothing - it's already done

>=1

>1

DUP2CLOSE(rfd, 0);
DUP2CLOSE(wfd, 1);

0

>1

DUP2CLOSE(wfd, 1);

>1

1

DUP2CLOSE(rfd, 0);

>1

0

DUP2CLOSE(wfd, 1);
DUP2CLOSE(rfd, 0);

1

0

tmp = dup(wfd); close(wfd);
DUP2CLOSE(rfd, 0);
DUP2CLOSE(tmp, 1);

Download

Other notes

There are actually quite a few variations on this line of thinking, such
as attaching a single read/write descriptor (a bidirectional socket)
to both stdin and stdout, or dealing with three separate descriptors that
must map to stdin, stdout, and stderr. In order to resolve them, a table
such as this must be created and the code derived from it. Though the code
is not presented here, the procedure for deriving it is.

In addition, some older UNIX systems don't implement the dup2() system
call, which makes this code substantially more complicated - but it's
possible. We've actually had to do this before - it's not pretty.