Signals, job control and pipes

Basic interprocess communication - Pipes

The system call pipe creates a ``pipe'', a type of unnamed FIFO (First In First Out) file used as an I/O channel between two cooperating processes: one process writes onto the pipe, while the other reads from it. Most pipes are created by the shell, as in:

ls | pr

which connects the standard output of ls to the standard input of pr. Sometimes, however, it is most convenient for a process to set up its own plumbing; this section illustrates how to establish and use the pipe connection.

Since a pipe is both for reading and writing, pipe returns two file-descriptors as follows:

int  fd[2];

stat = pipe(fd);
if (stat == -1) /* there was an error ... */

where fd is an array of two file-descriptors, with fd[0] for the read end of the pipe and fd[1] for the write end of the pipe. These may be used in read, write and close calls just like any other file-descriptors.

Implementation of pipes consists of implied lseek operations before each read or write in order to implement first-in-first-out. The system looks after buffering the data and synchronizing the two processes to prevent the writer from grossly out-producing the reader and to prevent the reader from overtaking the writer. If a process reads a pipe which is empty, it will wait until data arrive; if a process writes into a pipe which is full, it will wait until the pipe empties somewhat. If the write end of the pipe is closed, a subsequent read will encounter end-of-file.

To illustrate the use of pipes in a realistic setting, consider a function popen(cmd,mode), which creates a process cmd, and returns a file-descriptor that will either read or write that process, according to mode; thus, the call

fout = popen("pr", WRITE);

creates a process that executes the pr command; subsequent write calls using the file-descriptor fout send data to that process through the pipe.

#include <stdio.h>

#define READ 0 #define WRITE 1 #define tst(a, b) (mode == READ ? (b) : (a)) static int popen_pid;

popen(cmd, mode) char *cmd; int mode; { int p[2];

if (pipe(p) < 0) return(NULL);

if ((popen_pid = fork( )) == 0) { close(tst(p[WRITE], p[READ])); close(tst(0, 1)); dup(tst(p[READ], p[WRITE])); close(tst(p[READ], p[WRITE])); execl("/bin/sh", "sh", "-c", cmd, 0); _exit(1) /* disaster occurred if we got here */ } if (popen_pid == -1) return(NULL);

close(tst(p[READ], p[WRITE])); return(tst(p[WRITE], p[READ])); }


The function popen first calls pipe to create a pipe, then calls fork to create two copies of itself. The child decides whether it is supposed to read or write, closes the other end of the pipe, then calls the shell (via execl) to run the desired process. The parent likewise closes the end of the pipe it does not use. These close operations are necessary to make end-of-file tests work properly. For example, if a child that intends to read fails to close the write end of the pipe, it will never encounter the end-of-file on the pipe, just because there is one writer potentially active. The sequence of close operations in the child is a bit tricky. Suppose that the task is to create a child process that will read data from the parent. Then the first close closes the write end of the pipe, leaving the read end open.

To associate a pipe with the standard input of the child, use the following:

   close(tst(0, 1));
   dup(tst(p[READ], p[WRITE]));

The close call closes file-descriptor 0, the standard input, then the dup call returns a duplicate of the open file-descriptor. File-descriptors are assigned in increasing order and dup returns the first available one, so the dup call effectively copies the file-descriptor for the pipe (read end) to file-descriptor 0 making the read end of the pipe the standard input. (Although somewhat tricky, it's a standard idiom.) Finally, the old read end of the pipe is closed. A similar sequence of operations takes place when the child process must write to the parent process instead of reading from it. To finish the job we need a function pclose to close a pipe created by popen.

#include <signal.h>

pclose(fd) /* close pipe descriptor */ int fd; { struct sigaction o_act, h_act, i_act, q_act; extern pid_t popen_pid; pid_t c_pid; int c_stat;


sigaction(SIGINT, SIG_IGN, &i_act); sigaction(SIGQUIT, SIG_IGN, &q_act); sigaction(SIGHUP, SIG_IGN, &h_act);

while ((c_pid=wait(&c_stat))!=-1 && c_pid!=popen_pid); if (c_pid == -1) c_stat = -1;

sigaction(SIGINT, &i_act, &o_act); sigaction(SIGQUIT, &q_act, &o_act); sigaction(SIGHUP, &h_act, &o_act);

return(c_stat); }


The main reason for using a separate function rather than close is that it is desirable to wait for the termination of the child process. First, the return value from pclose indicates whether the process succeeded. Equally important when a process creates several children is that only a bounded number of unwaited-for children can exist, even if some of them have terminated; performing the wait lays the child to rest. The calls to sigaction make sure that no interrupts, etc., interfere with the waiting process (see sigaction(2)).

The routine as written has the limitation that only one pipe may be open at once, because of the single shared variable popen_pid; it really should be an array indexed by file-descriptor. A popen function, with slightly different arguments and return value is available as part of the Standard I/O Library (see Intro(3S)).

Next topic: STREAMS-based pipes and FIFOs
Previous topic: Accessing the controlling terminal

© 2004 The SCO Group, Inc. All rights reserved.
UnixWare 7 Release 7.1.4 - 27 April 2004