Lab 2

Unix Interprocess Communication: Pipes

Credits

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:

  • In order to send structured data through a pipe, you have to serialize it to push it from sending to receiving process as a stream of bytes. For instance, imagine you are sending a struct type: you have to turn the struct into a sequence of bytes in order to send them over the pipe.
  • When you send two different instances of serialized data structures over a serial channel such as a Unix pipe, you have to be able to distinguish where one datum ends and the next one begins. What you use as the “marker” or “sentinel value” that distinguishes the two instances is up to you to define.

Problem 1 [30 points]

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).

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:

  • cd ~/csci315/Labs/Lab2
  • git add pipes.c
  • git commit -m “Lab 2.1 completed”
  • git push

Problem 2 [20 points]

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.

  • The parent sends a message to the child (byte-by-byte as in Problem 1) and when it is done, it enters a loop to read characters from the pipe coming from the child (also byte-by-byte), terminating the loop on the receipt of EOF.
  • The child receives the message also byte-by-byte, printing each character to standard out as it arrives. For each character received, the child converts it to uppercase (using toupper(3), make sure to read its man page) and sends it back to the parent using your second pipe. As the parent reads the characters received from the child, it prints them to standard out.

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:

  • cd ~/csci315/Labs/Lab2
  • git add upper.c
  • git commit -m “Lab 2.2 completed”
  • git push

Problem 3 [20 points]

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 spaceFor 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:

“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 that reading a line of text containing white spaces in C isn’t as straightforward as one might think. (Think what functions or system calls you’d use to accomplish this task.) To make things a bit easier, a GCC library function named readline() is provided on the Linux system. Read the manual page about this function and make use of it. One thing this particular manual page didn’t provide is that in order to use the readline() function, one must link the user program with the library readline because this function is not in the standard C library.

Make sure to use the wrappers defined previously (Fork, Pipe, Read, and Write).

When you are done with this problem, you need to:

  • cd ~/csci315/Labs/Lab2
  • git add tokens.c
  • git commit -m “Lab 2.3 completed”
  • git push

Important!

Extend the Makefile you started in Pre-lab 2 to build the programs in problems 1, 2 and 3. Remember that 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:

  • cd ~/csci315/Labs/Lab2
  • git add Makefile
  • git commit -m “added Makefile”
  • git push

Hand In

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:

  • git add ~/csci315/Labs/Lab2/submission.txt
  • git commit -m “Lab 2 completed”
  • git push

Rubric

  1. Using the Fork wrapper and writing the three additional ones [10 points]. Implementing the correct functionality for sending and receiving messages byte-by-byte over the pipe [20 points].
  2. Using the wrappers defined previously, implementing the correct functionality for sending and receiving messages byte-by-byte over the pipe and using the child process to do the case conversion for the characters in the message [20 points].
  3. Using the wrappers defined previously, implementing the correct functionality for sending and receiving messages between processes according the required protocol, implementing the correct functionality for tokenizing and rebuilding strings. [20 points].
  4. Not providing a correct Makefile to build all three programs in lab and pre-lab. [up to -10 points].
Print Friendly