Communications Via Pipes
Once we got our processes to run, we suddenly realize that they cannot
communicate. After all, often when we start one process from another, they are
supposed to accomplish some related tasks. One of the mechanisms that allow
related-processes to communicate is the pipe, or the anonymous pipe.
A D V E R T I S E M E N T
What Is A Pipe?
One of the mechanisms that allow related-processes to communicate is the
pipe, or the anonymous pipe. A pipe is a one-way mechanism that allows two
related processes (i.e. one is an ancestor of the other) to send a byte stream
from one of them to the other one. Naturally, to use such a channel properly,
one needs to form some kind of protocol in which data is sent over the pipe.
Also, if we want a two-way communication, we'll need two pipes, and a lot of
caution...
The system assures us of one thing: The order in which data is written to the
pipe, is the same order as that in which data is read from the pipe. The system
also assures that data won't get lost in the middle, unless one of the processes
(the sender or the receiver) exits prematurely.
The pipe() System Call
This system call is used to create a read-write pipe that may later be used
to communicate with a process we'll fork off. The call takes as an argument an
array of 2 integers that will be used to save the two file descriptors used to
access the pipe. The first to read from the pipe, and the second to write to the
pipe. Here is how to use this function:
/* first, define an array to store the two file descriptors */
int pipes[2];
/* now, create the pipe */
int rc = pipe(pipes);
if (rc == -1) { /* pipe() failed */
perror("pipe");
exit(1);
}
If the call to pipe() succeeded, a pipe will be created,
pipes[0] will contain the number of its read file descriptor, and pipes[1] will
contain the number of its write file descriptor.
Now that a pipe was created, it should be put to some real use. To do this,
we first call fork() to create a child process, and then use the
fact that the memory image of the child process is identical to the memory image
of the parent process, so the pipes[] array is still defined the same way in
both of them, and thus they both have the file descriptors of the pipe. Further
more, since the file descriptor table is also copied during the fork, the file
descriptors are still valid inside the child process.
Lets see an example of a two-process system in which one (the parent process)
reads input from the user, and sends it to the other (the child), which then
prints the data to the screen. The sending of the data is done using the pipe,
and the protocol simply states that every byte passed via the pipe represents a
single character typed by the user.
#include <stdio.h> /* standard I/O routines. */
#include <unistd.h> /* defines pipe(), amongst other things. */
/* this routine handles the work of the child process. */
void do_child(int data_pipe[]) {
int c; /* data received from the parent. */
int rc; /* return status of read(). */
/* first, close the un-needed write-part of the pipe. */
close(data_pipe[1]);
/* now enter a loop of reading data from the pipe, and printing it */
while ((rc = read(data_pipe[0], &c, 1)) > 0) {
putchar(c);
}
/* probably pipe was broken, or got EOF via the pipe. */
exit(0);
}
/* this routine handles the work of the parent process. */
void do_parent(int data_pipe[])
{
int c; /* data received from the user. */
int rc; /* return status of getchar(). */
/* first, close the un-needed read-part of the pipe. */
close(data_pipe[0]);
/* now enter a loop of read user input, and writing it to the pipe. */
while ((c = getchar()) > 0) {
/* write the character to the pipe. */
rc = write(data_pipe[1], &c, 1);
if (rc == -1) { /* write failed - notify the user and exit */
perror("Parent: write");
close(data_pipe[1]);
exit(1);
}
}
/* probably got EOF from the user. */
close(data_pipe[1]); /* close the pipe, to let the child know we're done. */
exit(0);
}
/* and the main function. */
int main(int argc, char* argv[])
{
int data_pipe[2]; /* an array to store the file descriptors of the pipe. */
int pid; /* pid of child process, or 0, as returned via fork. */
int rc; /* stores return values of various routines. */
/* first, create a pipe. */
rc = pipe(data_pipe);
if (rc == -1) {
perror("pipe");
exit(1);
}
/* now fork off a child process, and set their handling routines. */
pid = fork();
switch (pid) {
case -1: /* fork failed. */
perror("fork");
exit(1);
case 0: /* inside child process. */
do_child(data_pipe);
/* NOT REACHED */
default: /* inside parent process. */
do_parent(data_pipe);
/* NOT REACHED */
}
return 0; /* NOT REACHED */
}
As we can see, the child process closed the write-end of the pipe (since it
only needs to read from the pipe), while the parent process closed the read-end
of the pipe (since it only needs to write to the pipe). This closing of the
un-needed file descriptor was done to free up a file descriptor entry from the
file descriptors table of the process. It isn't necessary in a small program
such as this, but since the file descriptors table is limited in size, we
shouldn't waste unnecessary entries.
Two-Way Communications With Pipes
In a more complex system, we'll soon discover that this one-way
communications is too limiting. Thus, we'd want to be able to communication in
both directions - from parent to child, and from child to parent. The good news
is that all we need to do is open two pipes - one to be used in each direction.
The bad news, however, is that using two pipes might cause us to get into a
situation known as 'deadlock':
- Deadlock
- A situation in which a group of two or more processes are all waiting
for a set of resources that are currently taken by other processes in the
same group, or waiting for events that are supposed to be sent from other
processes in the group.
Such a situation might occur when two processes communicate via two pipes.
Here are two scenarios that could led to such a deadlock:
- Both pipes are empty, and both processes are trying to read from their
input pipes. Each one is blocked on the read (cause the pipe is empty), and
thus they'll remain stuck like this forever.
- This one is more complicated. Each pipe has a buffer of limited size
associated with it. When a process writes to a pipe, the data is placed on
the buffer of that pipe, until it is read by the reading process. If the
buffer is full, the write() system call gets blocked until the
buffer has some free space. The only way to free space on the buffer, is by
reading data from the pipe.
Thus, if both processes write data, each to its 'writing' pipe, until the
buffers are filled up, both processes will get blocked on the write()
system call. Since no other process is reading from any of the pipes, our
two processes have just entered a deadlock.
Lets see an example of a (hopefully) deadlock-free program in which one
process reads input from the user, writes it to the other process via a pipe.
the second process translates each upper-case letter to a lower-case letter and
sends the data back to the first process. Finally, the first process writes the
data to standard output.
#include <stdio.h> /* standard I/O routines. */
#include <unistd.h> /* defines pipe(), amongst other things. */
#include <ctype.h> /* defines isascii(), toupper(), and other */
/* character manipulation routines. */
/* function executed by the user-interacting process. */
void user_handler(int input_pipe[], int output_pipe[])
{
int c; /* user input - must be 'int', to recognize EOF (= -1). */
char ch; /* the same - as a char. */
int rc; /* return values of functions. */
/* first, close unnecessary file descriptors */
close(input_pipe[1]); /* we don't need to write to this pipe. */
close(output_pipe[0]); /* we don't need to read from this pipe. */
/* loop: read input, send via one pipe, read via other */
/* pipe, and write to stdout. exit on EOF from user. */
while ((c = getchar()) > 0) {
/* note - when we 'read' and 'write', we must deal with a char, */
/* rather then an int, because an int is longer then a char, */
/* and writing only one byte from it, will lead to unexpected */
/* results, depending on how an int is stored on the system. */
ch = (char)c;
/* write to translator */
rc = write(output_pipe[1], &ch, 1);
if (rc == -1) { /* write failed - notify the user and exit. */
perror("user_handler: write");
close(input_pipe[0]);
close(output_pipe[1]);
exit(1);
}
/* read back from translator */
rc = read(input_pipe[0], &ch, 1);
c = (int)ch;
if (rc <= 0) { /* read failed - notify user and exit. */
perror("user_handler: read");
close(input_pipe[0]);
close(output_pipe[1]);
exit(1);
}
/* print translated character to stdout. */
putchar(c);
}
/* close pipes and exit. */
close(input_pipe[0]);
close(output_pipe[1]);
exit(0);
}
/* now comes the function executed by the translator process. */
void translator(int input_pipe[], int output_pipe[])
{
int c; /* user input - must be 'int', to recognize EOF (= -1). */
char ch; /* the same - as a char. */
int rc; /* return values of functions. */
/* first, close unnecessary file descriptors */
close(input_pipe[1]); /* we don't need to write to this pipe. */
close(output_pipe[0]); /* we don't need to read from this pipe. */
/* enter a loop of reading from the user_handler's pipe, translating */
/* the character, and writing back to the user handler. */
while (read(input_pipe[0], &ch, 1) > 0) {
/* translate any upper-case letter to lower-case. */
c = (int)ch;
if (isascii(c) && isupper(c))
c = tolower(c);
ch = (char)c;
/* write translated character back to user_handler. */
rc = write(output_pipe[1], &ch, 1);
if (rc == -1) { /* write failed - notify user and exit. */
perror("translator: write");
close(input_pipe[0]);
close(output_pipe[1]);
exit(1);
}
}
/* close pipes and exit. */
close(input_pipe[0]);
close(output_pipe[1]);
exit(0);
}
/* and finally, the main function: spawn off two processes, */
/* and let each of them execute its function. */
int main(int argc, char* argv[])
{
/* 2 arrays to contain file descriptors, for two pipes. */
int user_to_translator[2];
int translator_to_user[2];
int pid; /* pid of child process, or 0, as returned via fork. */
int rc; /* stores return values of various routines. */
/* first, create one pipe. */
rc = pipe(user_to_translator);
if (rc == -1) {
perror("main: pipe user_to_translator");
exit(1);
}
/* then, create another pipe. */
rc = pipe(translator_to_user);
if (rc == -1) {
perror("main: pipe translator_to_user");
exit(1);
}
/* now fork off a child process, and set their handling routines. */
pid = fork();
switch (pid) {
case -1: /* fork failed. */
perror("main: fork");
exit(1);
case 0: /* inside child process. */
translator(user_to_translator, translator_to_user); /* line 'A' */
/* NOT REACHED */
default: /* inside parent process. */
user_handler(translator_to_user, user_to_translator); /* line 'B' */
/* NOT REACHED */
}
return 0; /* NOT REACHED */
}
A few notes:
- Character handling: isascii() is a function that
checks if the given character code is a valid ASCII code. isupper()
is a function that checks if a given character is an upper-case letter.
tolower() is a function that translates an upper-case
letter to its equivalent lower-case letter.
- Note that both functions get an input_pipe and an output_pipe array.
However, when calling the functions we must make sure that the array we give
one as its input pipe - we give the other as its output pipe, and vice
versa. Failing to do that, the user_handler function will write a character
to one pipe, and then both functions will try to read from the other pipe,
thus causing both of them to block, as this other pipe is still empty.
- Try to think: what will happen if we change the call in line 'A' above
to:
translator(user_to_translator, user_to_translator);
/* line 'A' */
and the code of line 'B' above to:
user_handler(translator_to_user, translator_to_user);
/* line 'B' */
- Think harder now: what if we leave line 'A' as it was in the original
program, and only modify line 'B' as in the previous question?
|