The material developed for this lab was developed by Prof. L. Felipe Perrone. Permission to reuse this material in parts or in its entirety is granted provided that this “credits” note is not removed. Additional students files associated with this lab, as well as any existing solutions can be provided upon request by e- mail to perrone[at]bucknell[dot]edu
The objective of this lab is to help you internalize a couple of important facts about Unix pipes:
Copy the pipes-test.c program you wrote for Pre-lab 2 to another file named pipes.c.
Now, modify your new program so that the parent process writes to the pipe one character of the message at a time. Obviously, you must use a loop to send the entire message in this fashion – if you consider that the message is a C “string,” you can figure out what sentinel value you can use as the termination condition for the loop.
The message exchanged between the two processes is hard-coded into the program, but you should still test your program by experimenting with texts of different lengths (don’t forget to adjust the buffer size!)
When the entire message has been sent, have the sender process close the file descriptor on the write-end of the pipe. Closing the write end of a pipe will always cause an end-of-file (EOF) character to be sent to the reader on the other side.
Next, modify the child process so that it reads one character from the pipe at a time and writes each character individually to the standard output. This will require another loop, but one that must terminate when the EOF character is received from the pipe (see the man page for read(2) to understand how this system call reacts to receiving EOF).
Aside: Many system calls have both return values and side effects. In the case of read(2), the side effect is placing read bytes into the specified buffer argument, while the return value is communicating something about the execution of the system call itself (number of characters read, or errors encountered).
Important: When you read the “RETURN VALUES” section of the man page for read(2) and write(2), you will notice that when something goes really wrong, “-1 is returned and the global variable errno is set to indicate the error.” Similarly to what you did in Lab 1, you will write a wrapper for the pipe to call perror and exit the function in case of error. Use the same prototype from the system call pipe(2), but call the function Pipe, that is:
int Pipe(int pipefd[2]);
From now on, your programs should always define a wrapper for system calls and library functions that set the errno variable. Use the Fork wrapper created last week and create three additional wrappers for the system calls used in this problem following the function prototypes given below:
int Read(int fd, void *buf, size_t count);
int Write(int fd, const void *buf, size_t count);
When you are done with this problem, you need to:
Copy your pipes.c program to another file named upper.c. Modify your program so that it defines two pipes: one for communication from parent to child and another for communication from child to parent. Make sure to close the correct ends of each pipe. Ultimately, your goal is to have two pipes, one in each direction, so that you have bi-directional communication between the two processes. It helps a lot to use names for the pipes file descriptors that indicate their direction. For instance: p-to-c and c-to-p tell us the processes that the pipes interconnect and the direction of the flow of information.
This new version of the program must behave as follows.
Make sure to reason carefully about when the write ends of the two pipes should be closed, so that your processes can terminate their loops gracefully and reach their termination state when appropriate. Also, make sure to use the wrappers defined previously (Fork, Pipe, Read, and Write).
When you are done with this problem, you need to:
Copy your upper.c program to another file named tokens.c. Your parent process will work on an infinite loop, in which it reads a line from the standard input that will contain various words (or tokens) separated by one or more blank spaces. After reading an entire line, the parent process sends it to the child process, which will work to substitute the possibly multiple instances of a space by a single instance of a space. For instance, if the child process receives a C string like:
“This is a test of the alert system“
it sends back to the parent the C string, which should then be printed out:
“This is a test of the alert system“
The communication between parent and child processes is implemented by one pipe in each direction, as in Problem 2. The child process will now eliminate the repetitions of blank spaces instead of converting to uppercase the characters it receives.
Although there are different ways in which you can implement this functionality in the child process, your solution will rely on two library functions: strtok(3) and strcat(3). If you read its man page, you will see that the strtok function tokenizes the line it receives. That is, given a line, each invocation of strtok returns to the caller the next token it finds, skipping over one or multiple occurrences of a chosen delimiter character (space, in this case). If you repeatedly call strtok to extract tokens one at a time, you can progressively build a cleaned up line using strcat to concatenate each new token into an “accumulator” string.
Important: in this new program, your communications between parent and child process must not happen one byte at a time. Instead, the sending process will write on the pipe a message according to the following format:
<int, C string>
That is, when the sender is sending a message on the pipe, it first sends an integer using a single invocation of write for all the bytes in the int, and then sends the complete C string using a single invocation of write for all the characters in the string (including the NULL byte). Make sure not to send the <,> characters in the illustration of the message format above! This protocol must be followed in the communication from parent to child and also in the communication from child to parent.
Do the best you can in reading and interpreting the man pages for these two functions to learn how to use them. If you need clarifications on the logic for the parent and child processes, be sure to ask for them!
Note: reading a line of text that possibly contains white spaces in C isn’t as straightforward as one might think. (Think of the functions or system calls you already know that you might use to read a line of text. Consider what the would read from the keyboard when the user types a space in between any two words.) It turns out that the readline(3) function provided in our Linux system makes things easier. Read the manual page on this function and use it in this problem. In particular, think about how one might figure out the length of the string read by the readline() function. One point this particular manual page doesn’t raise is that in order to use the readline(3) function in a program, one must link it with the library readline because it’s not part of the GNU standard C library. (Add -lreadline to your gcc compilation command.)
Make sure to use the wrappers defined previously (Fork, Pipe, Read, and Write).
When you are done with this problem, you need to:
Extend the Makefile you started in Pre-lab 2 to build the programs in problems 1, 2 and 3. Not having a working Makefile will incur a 10 point penalty in the overall grade for this week’s pre-lab and lab.
When you are done with this problem, you need to:
Before turning in your work for grading, create a text file in your Lab 2 directory called submission.txt. In this file, provide a list to indicate to the grader, problem by problem, if you completed the problem and whether it works to specification. Wrap everything up by turning in this file:
Note: Up to 10 points may be deducted if not providing a correct Makefile to build all three programs in lab and pre-lab.