Introduction to Pipes

If you’re like me, you’re probably more familiar with the concept of pipes at the command prompt than in a programming environment. Or maybe you don’t care either way. Regardless, both types of pipe are similar forms of communications, but programming pipes seem specifically weird to me.

Before crawling into it, be aware that the programming pipe covered in this Lesson is a POSIX thing: Unix, Linux, and macOS. Windows implements pipe programming differently, more akin to server programming. This aspect of pipe programming isn’t covered here.

All major operating systems do support the command line pipe, the | operator, which flows the output from one program into the input for another:

grep "something" *.txt | less

The grep command’s output is redirected — or piped — into the less command as input. The output stream is diverted from standard output and provided as input. In fact, if you think of I/O as a stream of water, the pipe concept might begin to make sense.

Pipes in C programming work kinda sorta like pipes at the command prompt. What a pipe does is to create a pair of file descriptors, one for input and another for output. These file descriptors aren’t connected to any file or I/O device; they stand alone, but are connected with each other: One file descriptor handles input, which is sent to the second file descriptor as output.

Here is the man page (2) format for the pipe() function:

int pipe(int fildes[2]);

The sole argument is an integer array containing two elements. Upon success, the function returns zero. Array element filedes[0] is opened for reading and filedes[1] is used for writing. The element order and numbers parallel the standard I/O file descriptors, where stdin is assigned to zero and stdout is assigned to one.

Upon failure, the pipe() function returns -1, with the errno variable set accordingly.

The pipe() function is prototyped in the unistd.h header file.

One it’s created, you use file I/O commands to send and retrieve data to and from the pipe. The following code demonstrates how this communications takes place.

2022_06_25-Lesson.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define FD_WRITE 1
#define FD_READ 0

int main()
{
    const int size = 32;
    int x,r;
    int fp[2];
    const char *text = "This is a test";
    char buffer[size];

    /* initialize the buffer */
    for( x=0; x<size; x++ )
        buffer[x] = 0;

    /* create the pipe */
    r = pipe(fp);
    if( r==-1 )
    {
        perror("Pipe");
        exit(1);
    }

    /* write to the pipe */
    r = write(fp[FD_WRITE], text, strlen(text) );
    printf("Wrote '%s', %d bytes\n",text,r);
    /* read from the pipe */
    r = read(fp[FD_READ], buffer, size );
    printf("Read '%s', %d bytes\n",buffer,r);

    return(0);
}

The pipe() function is called at Line 22. Its argument is int array fp[], which holds two elements. Upon success, write and read file descriptors are placed into the array’s elements zero and one. These file descriptors are open and ready for I/O. At this point, the write() and read() functions send and fetch data through the pipe.

At Line 30, the write() function writes to the pipe’s FD_WRITE element, the file descriptor for writing. It’s sent a chunk of text, which is stored internally in the pipe’s I/O buffer.

At Line 33, data is read from the pipe’s FD_READ element. The retrieved text is stored in a buffer, which is output at Line 34:

Wrote 'This is a test', 14 bytes
Read 'This is a test', 14 bytes

The programming pipe’s I/O takes place as a low-level; it’s not streaming I/O. Therefore, the low-level functions read() and write() are used instead of fread() and fwrite() or the other high-level file I/O functions.

In next week’s Lesson, I show how to use a pipe to communicate with a thread.

Leave a Reply