Tutorial 5

 

In this tutorial, we are going to introduce inter-process communication using pipes in C.

We will have some hands-on experience with ready to use C programs.

Use Vi, Emacs or Pico for editing the files.

Login to one of the following:

cs1.utdallas.edu

cs2.utdallas.edu

Use PuTTY or Xmanager for logging on as you learnt in Tutorial 1.

Communicating with Pipes

In our first example, we will see how we can use a pipe for communication between a parent and child process. This is done by the "pipe" system call. Open your favorite Unix editor (Vi / Emacs / Pico) and type the code:

// pipe.c
// This example shows interprocess communication (IPC) using a pipe
// This is called the "pipe and fork" approach

// Typical approach:
// create a pipe using the system call "pipe"
// create a child process using "fork"
// use system calls "read" and "write" to read/write from/to the pipe

// when a pipe is created in the parent process, two file descriptors
// are created; we need to use them to access the pipe
// fd[0] and fd[1]
// fd[0] is opened for reading, fd[1] is opened for writing.

// when the child process is created, the child also has two
// file descriptors fd[0] and fd[1] (total of 4 are available)
// fd[0] is opened for reading, fd[1] is opened for writing.


// In this example: Parent writes to the pipe, child reads from the pipe
// (Other combinations are also possible)

#include <stdio.h>

int main()
{

        printf("Starting the pipe.c program\n\n");

        // working variables for reading data from the pipe
        int n; /* store the number of characters read */
        char buf[100]; /* store the characters read */

        // pipe needs an integer array of size 2
        int fd[2];

        printf("Create the pipe\n\n");
        pipe(fd); /* fd[0] read; fd[1] write */

        printf("Create the child process using fork\n");

        if ( fork() == 0 ) /* create child, test return value */
        {
                close(fd[1]); /* close write end - not needed in this example */
                printf("child is running...\n");
                printf("about to read from the pipe\n");

                n = read( fd[0], buf, 100); /* child reads from pipe */

                printf("after reading from the pipe\n");
                printf("the value of n is: %d\n",n);
                printf("the value in the buffer is: %s\n\n", buf);
        }
        else /* parent*/
        {
                close(fd[0]); /* close read end - not needed for this example */
                printf("parent is running...\n");
                printf("about to write to the pipe\n");

                write( fd[1], "Writing to pipe\n", 16); /* parent writes to pipe */

                printf("after writing to the pipe\n\n");
        }

        printf("Ending the c program\n");
        exit(0);
}

Save the code as "pipe.c". Compile are run the code with following commands:

{cs1:~} gcc -o pipe pipe.c
{cs1:~} ./pipe
Starting the pipe.c program

Create the pipe

Create the child process using fork
parent is running...
about to write to the pipe
after writing to the pipe

Ending the c program
child is running...
about to read from the pipe
after reading from the pipe
the value of n is: 16
the value in the buffer is: Writing to pipe


Ending the c program

From the output, we can see that whatever we wrote from the parent process by "write" system call can be read by the child process by "read" system call. In this example we wrote once and read once. But what happens if we try to read more times than there is corresponding write? The following example illustrates the scenario:

// pipe_read_test.c
#include <stdio.h>

int main()
{

        printf("Starting the pipe.c program\n\n");

        // working variables for reading data from the pipe
        int n; /* store the number of characters read */
        char buf[100]; /* store the characters read */

        // pipe needs an integer array of size 2
        int fd[2];

        printf("Create the pipe\n\n");
        pipe(fd); /* fd[0] read; fd[1] write */

        printf("Create the child process using fork\n");

        if ( fork() == 0 ) /* create child, test return value */
        {
                close(fd[1]); /* close write end - not needed in this example */
                printf("child is running...\n");
                printf("before the first read from the pipe\n");

                n = read( fd[0], buf, 100); /* child reads from pipe */

                printf("after the first read from the pipe\n");
                printf("the value of n is: %d\n",n);
                printf("the value in the buffer is: %s\n\n", buf);

                // Question: is the data still in the pipe after I read it?
                // experiment
                // change the contents of the working array of char buf
                buf[0]= 'a';
                buf[1]= 'p';
                buf[2]= 'p';
                buf[3]= 'l';
                buf[4]= 'e';
                buf[5]= NULL;

                // change the value of working variable n
                n=99;

                printf("the value of n has been changed to: %d\n", n);
                printf("the value in buf has been changed to: %s\n\n", buf);
                printf("just before trying to re-read from the pipe\n");

                // re-read from the pipe and see what happens
                n = read(fd[0], buf, 100); /* child reads from pipe again */

                printf("just after trying to re-read from the pipe\n");
                printf("the value of n is: %d\n",n);
                printf("the value in the buffer is: %s\n", buf);
        }
        else /* parent*/
        {
                close(fd[0]); /* close read end  - not needed for this example */
                printf("parent is running...\n");
                printf("before writing to the pipe\n");

                write( fd[1], "Writing to pipe\n", 16); /* parent writes to pipe */

                printf("after writing to the pipe\n");
        }

        printf("Ending the c program\n");
        exit(0);
}

Save the file as "pipe_read_test.c". Compile and execute the file with following commands:

{cs1:~} gcc -o pipe_read_test pipe_read_test.c
{cs1:~} ./pipe_read_test
Starting the pipe.c program

Create the pipe

Create the child process using fork
parent is running...
before writing to the pipe
after writing to the pipe
Ending the c program
child is running...
before the first read from the pipe
after the first read from the pipe
the value of n is: 16
the value in the buffer is: Writing to pipe


the value of n has been changed to: 99
the value in buf has been changed to: apple

just before trying to re-read from the pipe
just after trying to re-read from the pipe
the value of n is: 0
the value in the buffer is: apple
Ending the c program

From the output, we see that the second read returned zero in "n" as there was no more data available. So, data can be read only once. Now, we will see the opposite case. The parent will write multiple times but child will read once:

// pipe_write_test.c

#include <stdio.h>

int main()
{

        printf("Starting the pipe.c program\n\n");

        // working variables for reading data from the pipe
        int n; /* store the number of characters read */
        char buf[100]; /* store the characters read */

        // pipe needs an integer array of size 2
        int fd[2];

        printf("Create the pipe\n\n");
        pipe(fd); /* fd[0] read; fd[1] write */

        printf("Create the child process using fork\n");

        if ( fork() == 0 ) /* create child, test return value */
        {
                close(fd[1]); /* close write end - not needed in this example */
                printf("child is running...\n");
                sleep(1); /* give the parent time to write to the buffer */
                printf("before the first read from the pipe\n");

                n = read( fd[0], buf, 100); /* child reads from pipe */

                printf("after the first read from the pipe\n");
                printf("the value of n is: %d\n",n);
                printf("the value in the buffer is below: \n %s\n\n", buf);
        }
        else /* parent*/
        {
                close(fd[0]); /* close read end - not needed for this example */
                printf("parent is running...\n");

                // what about multiple writes to the pipe?
                printf("before the first write to the pipe\n");
                write( fd[1], "Writing to pipe ", 16); /* parent writes to pipe */

                printf("before the second write to the pipe\n");
                write( fd[1], "Writing to pipe_2 ", 18); /* parent writes to pipe */

                printf("before the third write to the pipe\n");
                write( fd[1], "Writing to pipe_3 ", 18); /* parent writes to pipe */

                printf("before the fourth write to the pipe\n");
                write( fd[1], "Writing to pipe_4 ", 40); /* parent writes to pipe */

                printf("total number of characters written is 16+18+18+40 = 92\n");
        }

        printf("Ending the c program\n");
        exit(0);
}

Exercise

Modify the child process code with a simple loop so that it prints out the characters in buf one at a time.