CSE 422S: Lab 3

Multiplexed Inter-Process Communication


Multiplexed I/O thus becomes the pivot point for the application, designed similarly to the following activity:

  1. Multiplexed I/O: Tell me when any of these file descriptors become ready for I/O.
  2. Nothing ready? Sleep until one or more file descriptors are ready.
  3. Woken up! What is ready?
  4. Handle all file descriptors ready for I/O, without blocking.
  5. Go back to step 1.

Robert Love, Linux System Programming, 2nd Edition, Chapter 2, pp. 52.

Interprocess communication allows data to bypass memory protection boundaries and other restrictions imposed by processes, in a selective way that preserves the integrity of the processes themselves. This allows processes to adopt and implement application-specific protocols for how they connect with each other and how those data are exchanged, which in turn allows multiple processes to cooperate in solving problems more effectively and/or efficiently.

Managing multiple connections at once requires careful attention to both performance and correctness, and different alternatives for doing so concurrently are particularly important. In this lab you will use event multiplexing to allow a server to accept and use connections concurrently, with only a single thread of execution. Clients also will be single-threaded and each will perform its operations sequentially. Together the clients and server will complete a form of distributed merge sort, of shuffled numbered fragments of a file back into the original file.

In this lab, you will:

  1. Write client and server code that can perform a useful distributed collaborative activity - in this case re-assembling a text file from numbered fragments of text that are scattered among different files.
  2. Use low-level inter-process communication mechanisms to enable the server and clients to communicate with each other.
  3. Use event multiplexing mechanisms to allow the server to interact with multiple clients concurrently.
  4. Verify and evaluate the operation of your server and client programs using files of different lengths broken into different numbers of fragments.

Create a readme.txt file for your project, in which you should record your observations as you go, and then refine your readme.txt file into a cohesive lab report before submitting your lab solution.

Make sure that the name of each person who worked on this lab assignment is listed at the beginning of it, and make sure that in addition to completing the code for this project, you also carefully document your work in that readme.txt file, as indicated in the Assignment section below.


Readings and other resources

The following sources of information are likely to be useful as you work on this project.

Please read through the grading rubric for the lab assignments in this course, and please keep the points it makes in mind as you design, implement, and test your lab solution.

Assignment

  1. At the top of your readme.txt file, list the names of the people who worked together on this lab, and a working e-mail address for each of those people. You are encouraged to work in groups of 2 or 3 people on this lab assignment, though you may work individually instead if you prefer: groups larger than 3 are not allowed for this assignment.

  2. Write a server program in the C programming language that takes two command line arguments: (1) the name of a file (for example, jabberwocky_spec) that resides in the server's working directory - i.e., the directory from which the server was launched; and (2) a port number at which it should listen for and accept connection requests from clients. The server should open that file and read lines of text from it: the first line should give the name of an output file that the server should produce in its working directory (for example, "jabberwocky" could be given to hold the text of Lewis Carroll's poem, "Jabberwocky"), and the subsequent lines of the file should give the names of files that contain numbered lines for fragments of the main file (for example, jabberwocky_1, jabberwocky_2, jabberwocky_3, jabberwocky_4, and jabberwocky_5).

    I've written a simple C++ program, file_shuffle_cut.cpp that may be helpful in testing your code (it generates fragments from a file, which your lab solution should then be able to reconstitute to reproduce the original file). Use g++ (which is provided in the module for the C compiler version you should be using) to build this program, as in:

    g++ -o file_shuffle_cut file_shuffle_cut.cpp

    and then you can test the compiled binary on the source file itself, as in:

    ./file_shuffle_cut file_shuffle_cut.cpp 3

  3. Your server should first make sure that it received two command line arguments, that it can open the file given in the first argument, that it can open an output file whose name is given in the first line of the input file, and that it can open each of the data input files that are named in the subsequent lines. If it cannot do any of these things (including if it was given too few or too many command line arguments), it should print an appropriate error message (or in the case of the wrong number of command line arguments it should print a usage message showing how to run the program correctly) and then end (returning an appropriate error value for each distinct case). If any subsequent step of the program fails in a manner that cannot be worked around, the server program should also output an appropriate error message and end, returning a unique non-zero error value that corresponds to the kind of problem that occurred.

    The program should be able to work correctly for arbitrary numbers of fragment files. This means that the server will need to use variable length arrays, or use dynamically allocated heap memory to hold onto the file descriptors for the open fragment files.

    The server code also should be sure to check all function return values, especially from the system calls that open the files, since that is how it can determine whether or not each file was opened successfully.

    Last, as you work on this assignment you are strongly encouraged to use debugging tools like gdb to step through the behavior of your program - in fact you can use separate copies of gdb to run your server and your clients and in doing so step through the entire distributed behavior of your code. Working incrementally, and compiling and testing (including stepping through your code in gdb) each time you complete a new part of the assignment can help avoid problems and lead you more quickly to a working lab solution.

  4. Once the server has opened up all of those files successfully, it should create and bind an internet domain socket at the port specified in the second command line argument, as in the internet domain socket exercise of the Linux Sockets studio, and should print (to the standard output stream) both the internet port and address at which clients should request to connect to it. The server should first listen for connections on that socket, and then should wait for connection requests using one of the Linux event demultiplexing mechanisms, as in the I/O event handling studio.

    Whenever a client connects to the server, the server should accept the connection, and prepare to send data from the current fragment file to that client (Note: in doing so it is essential that the line delimiters are preserved and not lost). The server should then return to waiting on relevant (1) connection request events from the socket on which it is listening, and (2) read and write events from that new socket as well as from any other sockets it has already opened with other clients; as noted below, as the server completes different phases of its execution some of the events will no longer be relevant and the server will cease waiting on them.

  5. The server should maintain a data structure (or data structures) into which it can read each line from the input file and hold onto it until it has successfully sent it to the appropriate client - each client should be sent its own fragment file, and will sort it and send it back over the same socket to the server. The server also should maintain a data structure (or data structures) for data that it reads back from the client, in the form of sorted lines of text (with the line delimiters preserved). When the server has read in a line number and the line of text that accompanies it, it should insert them (together) into a single common data structure that holds line/number pairs in sorted order according to the line numbers - using a heap or a balanced binary tree (an AVL tree or red-black tree for example) of a simple struct type that contains both a line number and a line of text would be reasonably efficient for this purpose, though if you choose to use another data structure you should please explain why and what cost it may have for performance, in your readme.txt file.

    Watch out for "short reads" and "short writes" in which only part of a sequence of characters is read or written in a single call to read() or write() (and make sure to check the return values from those calls so you can handle that) - a single such call per event is all you can do without blocking on a given socket before the server returns to waiting for further events. Please also note that a server may have multiple distinct actions available at once - it could have data ready from a client to which it had connected earlier, even as it is sending data to a new client - it should interleave such enabled reads and writes as much as it can, to allow as much concurrency as possible within a single thread.

  6. When the server has established connections with as many clients as there are fragment files, it should stop waiting for connection requests (i.e., removing the listening socket from the set of IPC endpoints on which it waits for events). When the server has sent all of the data from a fragment file to a client, it should close the fragment file and stop waiting for write events on the socket to that client (but should still continue waiting for read events on that socket). When the server has read back all the data from a client, it should close the socket and stop waiting for read events on it. When the server has read back all the data from all the clients, it should end its event loop, and write the lines (in sorted order, but without the line numbers) into the output file, should close the output file, and should return a value that indicates success.

    Add a section titled "Server Design" to your readme.txt file, and in it describe briefly (in a few paragraphs) how you implemented these requirements, and identify any design choices that you made along with the rationale behind those decisions.

  7. Write a client program in the C programming language that takes two command line arguments, for the internet address and port at which it should request to connect with the server. If the client was given the wrong number of command line arguments it should print its own (client-side) usage message showing how to run the client program correctly and then return an appropriate error value from the main function. If any subsequent step of the program fails in a manner that cannot be worked around, the client program should also output an appropriate error message and return a unique non-zero error value that corresponds to the kind of problem that occurred.

    The client should first create a socket, and then connect to the server using the address and port it received from the command line. Once the connection is established, the client program should repeatedly read data from the newly connected socket. The client should preserve the formatting of the data from the server (a line number followed by a line of text including the delimiter, repeatedly) as it does that.

  8. The client should maintain a data structure (or data structures) into which it can use repeated reads (potentially multiple per line) to get line numbers and lines from the socket, and should hold onto each partially read line until it has successfully read the entire line. The client should do that repeatedly, storing each complete combination of a line number and a line of text in a data structure until it has received the entire stream of data from the server. To manage this data dynamically (and potentially also allow the client and server to share code for common data structures they both may use), you should use dynamic memory allocation and deallocation, and data structures that can deal with variable numbers and sizes of elements, as was needed for your server code. As with the server, using heap or a balanced binary tree (an AVL tree or red-black tree for example) is reasonably efficient for this purpose though if you choose to use another data structure you should please explain why and what cost it may have for performance, in your readme.txt file.

    Again watch out for "short reads" and "short writes" in which only part of a sequence of characters is read or written in a single call to read() or write(). One option on the client side (since it does not need to multiplex different I/O events concurrently) is to use higher level library functions for reading and writing data, though even when calling those it is essential to check their return values and other indicators of how much data was read or written.

  9. When the client has received all of the lines from the socket, it should then ensure the lines are ordered by their line numbers in the data structure. If you used a data structure that orders its entries at insertion (a heap or an AVL tree or red-black tree that uses the line number as its ordering key, for example) then the data are already in that order. Otherwise please sort the data in the data structure so that they become ordered in that way. Either way, please document your design for this in your readme.txt file.

    The client program should then go through the data structure in that order, using repeated writes (or higher level library functions to write data to the socket) to send the lowest line number and its line back to the server over the connected socket, then sending the next higher line number and its line, etc. (note that with high probability the line numbers will not all be sequential).

  10. When the client has written back all its sorted data to the server, it should close the socket and end, returning a value that indicates success (returning a manifest constant whose value is 0 is conventional for that in C).

    Add a section titled "Client Design" to your readme.txt file, and in it describe briefly (in a few paragraphs) how you implemented these requirements, and identify any design choices that you made along with the rationale behind those decisions.

  11. Compile your server and client programs, and use those compiled binaries to run different trials on your Raspberry Pi to test their correct behavior and performance with different sized files broken into different numbers of fragments. One especially helpful way to do this (in addition to using gdb as noted above) is to write a bash (or other Linux shell environment) script that launches a given number of clients at once - this creates a situation with high concurrency which is one of the conditions under which your lab solution will be graded. Think about other situations and how they can help to establish other important conditions for testing your programs, and run those tests as well.

    Add a section titled "Build Instructions" to your readme.txt file, which explains briefly how to compile and run your server and client programs. This can involve command lines that invoke the compiler directly, or if you wrote and submitted a Makefile it could be a command line that invokes make, etc.

    Add a section titled "Testing and Evaluation" to your readme.txt file, and in it describe all the test cases you ran, and your observations about how your client and server programs performed in each of those cases. Be sure to test the edge cases noted in the assignment when the server (or possibly client) may return an error code, and make sure it handles those as specified, as well as the cases where it runs successfully to completion. If you run into any issues during testing where your server or client program isn't working correctly, in this section of your report please decribe briefly what the issue was, how testing helped to identify it, and how you fixed that issue.

  12. At the bottom of your readme.txt file please add a section titled "Development Effort" and in it please provide an estimate of the amount of time (in total person-hours) that your team (or you if you didn't work with anyone else) spent completing this assignment. This will allow us to calibrate and possibly refine the content of this course in future semesters.

Things to turn in:

Please make sure your readme.txt file lists the names of everyone who worked on the project. If you are working in a team, if possible have just one person submit the files - however, if you must submit your solution from multiple Canvas accounts, please make sure that exactly the same files (and versions of those files) are submitted in each case.