==============================================================================
C-Scene Issue #04
Pipes in Unix
Cameron Zwarich
==============================================================================


Everyone once and a while (more like 5 minutes) someone asks the question: 
"How do I send stuff to a program's input" or "How do I read a program's 
output". In this article, I hope to answer both of them.

All the input in a program is read on the standard input (fd number is 
typically 0), and all the output is written on the standard output 
(typically 1). Make a note, that all errors should be written on standard 
error (typically 2).

What is to be done to send/capture on these descriptors? The easiest (and 
really the only portable) way is to replace them with your own. If you 
replace the input descriptor with your own, it will read from it, and if 
you replace the output descriptor with your own, it will write to it.

A pipe, in Unix, is just a pair of file descriptors that work like a pipe, 
you send data in one end, it comes out the other. They work just like normal 
file descriptors, so your victims (hehe) won't suspect a thing.

Now, we have to find a means to make these descriptors. pipe() is the 
easiest way. It takes one argument, int filedes[2] (an array of two integers). 
It creates a pair of file descriptors, where one can be written to, and 
the input is read on the other. filedes[0] is for reading. filedes[1] is for 
writing.

Now, we need to execute a child program. We first fork() a child process, 
which sets its file descriptors to the end(s) of the pipe(s) we want them 
to be. The actual copying of the descriptors is accomplished using dup2(). We 
then use an exec-family function to load the executable we want to execute, 
which inherits the childs altered file descriptor table.

The parent, which has the end of the pipe you gave it still in the filedes 
array you passed to pipe(). You may want to make your end of the pipe into a 
stream using fdopen() now, so that you can use standard I/O functions on it. 
Remember that if you want the data to arrive quickly, you need to use 
fflush() on the stream to make sure it isn't stuck in the buffer.

Here is an example which sends a string into a specified program's input and 
writes its output to a file, character by character:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main (int argc, char **argv)
{
  int filedes1[2], filedes2[2];
  int pid;
  
  /* Argument checking. */

  if (argc < 4)
    {
      puts("Wrong number of arguments given");
      exit(1);
    }

  /* Make our first pipe. */

  if (pipe(filedes1) == -1)
    {
      perror ("pipe");
      exit(1);
    }

  /* Make our second pipe. */
  
  if (pipe(filedes2) == -1)
    {
      perror ("pipe");
      exit(1);
    }

  /* Fork a child */

  if ((pid = fork()) == 0)
    {
      dup2(filedes1[0], fileno(stdin)); /* Copy the reading end of the pipe. */
      dup2(filedes2[1], fileno(stdout)); /* Copy the writing end of the pipe */
      
      /* Uncomment this if you want stderr sent too.

      dup2(filedes2[1], fileno(stderr));

      */

      /* If execl() returns at all, there was an error. */
      
      if (execl(argv[1], argv[1] == -1)) /* Bye bye! */
	{
	  perror("execl");
	  exit(128);
	}
    }
  else if (pid == -1)
    {
      perror("fork");
      exit(1);
    }
  else
    {
      int output;
      char c;

      write(filedes1[1], argv[2], strlen(argv[2])); /* Write the string */

      if((output = open(argv[3], O_WRONLY)) == -1)
	{
	  perror("open");
	  exit(1);
	}

      while (read(filedes2[0], &c, 1) != 0)
	  write(output, &c, 1);
    
      exit(0);
    }
}

Look at the commented se

Here is another version of that example, this time, using the standard I/O 
stream facilities:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main (int argc, char **argv)
{
  int filedes1[2], filedes2[2];
  int pid;
  
  /* Argument checking. */

  if (argc < 4)
    {
      fputs("Insufficient number of arguments given\n", stderr);
      exit(1);
    }

  /* Make our first pipe. */

  if (pipe(filedes1) == -1)
    {
      perror ("pipe");
      exit(1);
    }

  /* Make our second pipe. */
  
  if (pipe(filedes2) == -1)
    {
      perror ("pipe");
      exit(1);
    }

  /* Fork a child */

  if ((pid = fork()) == 0)
    {
      dup2(filedes1[0], fileno(stdin)); /* Copy the reading end of the pipe. */
      dup2(filedes2[1], fileno(stdout)); /* Copy the writing end of the pipe */
      
      /* Uncomment this if you want stderr sent too.

      dup2(filedes2[1], fileno(stderr));

      */

      /* If execl() returns at all, there was an error. */
      
      if (execl(argv[1], argv[1] == -1)) /* Bye bye! */
	{
	  perror("execl");
	  exit(128);
	}
    }
  else if (pid == -1)
    {
      perror("fork");
      exit(128);
    }
  else
    {
      FILE *program_input, *program_output, output_file;
      int c;

      if ((program_input = fdopen(filedes1[1], "w")) == NULL)
	{
	  perror("fdopen");
	  exit(1);
	}

      if ((program_output = fdopen(filedes2[0], "r")) == NULL)
	{
	  perror("fdopen");
	  exit(1);
	}

      if ((output_file = fopen(argv[3], "w")) == NULL)
	{
	  perror ("fopen");
	  exit(1);
	}

      fputs(argv[2], program_input); /* Write the string */

      while ((c = fgetc(program_ouput)) != EOF)
	  fputc(c, output_file);

      exit(0);
    }
}

The former version is much simpler, but for real-world applications, the 
latter version's method of creating streams is more useful.

Function Prototypes
-------------------

#include <unistd.h>

int pipe(int filedes[2])
int fork(void)
int execl(const char *path, const char *arg0, ...);


This page is Copyright © 1997 By C Scene. All Rights Reserved