The directory
/homes/park/pub/cs422
(symbolic link /homes/park points to /u/riker/u3/park)
accessible from our lab machines in HAAS G050
contains mysh.c which implements a prototypical concurrent server:
a simple, minimalist shell. A concurrent server receives and parses a
client request -- in this case, from stdin (by default the keyboard
attached to a PC in the lab) --
then delegates the actual execution of a requested
task to a worker process or thread.
Many apps in the real-world, including network software, follow the
general structure of concurrent client/server code which are multithreaded programs.
They constitute a baseline for implementing network software.
Another class of multithreaded programs are parallel computing apps
that implement SIMD/MIMD code that run concurrently on multiple processors.
For example, GPUs with hundreds of processing units are instances where
parallel speed-up is achieved for graphics and AI related computations.
Multithreading as used in systems, by default, follows a
concurrent client/server design which includes peer-to-peer systems
where a process is both server and client.
Software engineering techniques utilized in the two areas are different,
with the former focused on algorithmic aspects of organizing computations.
In networking we are focused on systems aspects of implementing protocols.
Create a directory, lab1/, somewhere under your home directory, and a subdirectory v1/ therein. Copy mysh.c to v1/, compile, run and check that you understand how it works. Run the shell with command "ps -g -a" and explain why it does not work. Describe the logical steps of a solution that extends mysh.c to allow "ps -g -a" to be executed. Put your answer in lab1ans.pdf and place the pdf file under lab1/. Modify mysh.c, call it mysh_v1.c, that calls execvp() instead of execlp() to implement your solution. Test and verify that it works correctly with other commands such as "ls -ld -a -F".
Implement an iterative server version of mysh_v1.c, call it mysh_v2.c, where a requested command is executed by the process that parses the request without spawning a child process to carry out the task. It seems obvious why an iterative server design is not well-suited for the command execution server (i.e., shell). Discuss for what servers an iterative design may be appropriate. Provide a couple of examples. Place the code in a new directory v2/.
Modify the concurrent server mysh.c so that entering character 'q' on stdin terminates the app. Call the updated version mysh_v3.c and place it in v3/. A user attempting to terminate the app by entering CTRL-C should fail, with the message "Use 'q' to quit." output to stdout. That is, your app ignores CTRL-C. To achieve this, install a signal handler, void ctrlc_handler(int), coded in v3/ctrlc_handler.c that is invoked when the SIGINT signal is raised. Another default way to terminate a process running in the foreground is to enter CTRL-\ which generates the SIGQUIT signal. Add a signal handler to mysh_v3.c so that SIGQUIT is ignored and suitable message output to stdout. Put the code ctrlq_handler() in ctrlq_handler.c. Provide Makefile for building the executable for mysh_v3.c, name it mysh.exe, in v3/. Outside of entering character 'q' at the prompt, what other ways (besides turning off the computer) are there to terminate the running shell? Include your answer in lab1ans.pdf.
Create a subdirectory v4/ under lab1. Using mysh_v3.c from Problem 3 as starting point, modify the concurrent server so that command requests are transmitted from a client process through IPC using FIFO (i.e., named pipe). Call the server code mysh_v4.c and the client code mycl.c. We will use two FIFOs, the first named myfifo4a which is created by the server using system call mkfifo(). myfifo4a will be used by the client to send requests to the server. A second FIFO, myfifo4b, created by the client upon startup, is used by the server to communicate the results of performing a request to the client. Hence the two FIFOs are unidirectional (e.g., for myfifo4a the client is the writer and the server the reader), referred to as half-duplex in network terminology. We will replace the two FIFOs with a single socket() in lab2 where a socket is bidirectional, referred to as full-duplex. Both server and client run on the same host. In lab2 we will run client and server on different lab machines, and use the socket() interface to enable communication between client and server processes.
The client mycl.c creates FIFO myfifo4b and opens the FIFO, myfifo4a, whose names are hardcoded using CPP command #define. mycl.c acts both as a client and a server. With respect to mysh_v4.c the code in mycl.c acts as a client that transmits command requests to the server via myfifo4a. mycl.c acts as a server when viewed from the perspective of a human user who enters a command to be executed on stdin that is read by mycl.c. This is enabled by mycl.c by printing a prompt "% " upon startup to stdout and waiting for user input. Thus mycl.c behaves similar to a shell that accepts user requests via stdin but instead of carrying out the command itself or its child processes, mycl.c forwards the request to mysh_v4.c which then performs the requested task. After reading a command from stdin mycl.c transmits it to the process running mysh_v4.c. Let's call the process running mysh_v4.c mysh4 and the process running mycl.c mycl. After forwarding the command request to mysh4, mycl blocks on myfifo4b by calling read() until a response (the result of executing the command at the server) is received. Upon receiving a response, mycl outputs the response to stdout. Then it outputs the prompt "% " on a separate line on stdout and waits on user input from stdin. When mycl transmits a request to mysh4 by writing to myfifo4a, it appends the newline character '\n' to the end of the request to indicate the end of a request.
How the output of the child process of the server that executes a requested command is communicated via myfifo4b to the client process mycl: we will make use of system call dup2() that changes stdout (i.e., file descriptor 1) of the child process so that it points to myfifo4b. Note that when a process is created both file descriptors 1 (stdout) and 2 (stderr) are initialized to point to the terminal display. We will use dup2() to override the default setting. A legacy app such as /bin/ps is coded so that its output is sent to stdout (i.e., file descriptor 1). Since dup2() re-configures file descriptor 1 to point to myfifo4b, the output of legacy app ps will be sent to myfifo4b without having to modify the code of /bin/ps to do so. Re-using an existing app as is, whenever possible, is an important consideration in real-world environments.
Implement your client/server app in a modular fashion. Provide a Makefile in v4/ that generates mysh4 and mycl. Makefile should support "make clean". Create a text file README under v4/ that specifies the files and functions of your code, and a brief description of their roles. We will follow a default convention in CS 422 convention where a function abc() is placed in its own file abc.c. To test your client/server app, open two windows wherein mysh4 and mycl are executed, respectively. Test your client/server app under various conditions to assess correctness. Note that testing is the most time consuming part of system development, not coding and basic debugging. Writing some code that performs a task reasonably on the surface is easy, building software that does it well is not. Because we are not implementing code to be released for public consumption -- all lab assignments are, at the end of the day, exercises aimed at imparting knowledge and skills -- scope of testing is drastically reduced compared to production systems. However, you need to consider the different run-time scenarios that are implied or implicit in the scope by the problem description. Determining what they are is an important part of solving the problem.
A companion to Problem 4, discuss the following issues in lab1ans.pdf. (1) What happens if mycl is executed before mysh4, and why? (2) What happens if mysh4 is terminated while mycl is still running blocking on user input? (3) Suppose two or more client processes executing mycl are writing to the shared FIFO myfifo4a to transmit their requests. Since our lab machines support concurrent execution of client processes -- either through multiple cores or scheduling on a single core where processes may be preempted and context-switched out -- what prevents the payload (i.e., bytes) of write() system calls to myfifo4a by two or more processes from being interleaved (thus producing junk when mysh4 reads from myfifo4a)? (4) Describe how you coded the client/server app so that stack overflow/smashing that corrupt return addresses is prevented.
Linux supports system call clone() that allows more refined multithreading implementation than fork(). Without coding and testing, describe in lab1ans.pdf how you would modify mysh_v1.c of Problem 1 to use clone() in place of fork(). Discuss the main differences between the two implementations.
The Bonus Problem is completely optional. It serves to provide additional exercises to understand material. Bonus problems help more readily reach the 40% contributed by lab component to the course grade.
Electronic turn-in instructions:
i) For problems that require answering/explaining questions, submit a write-up as a pdf file called lab1ans.pdf. Place lab1ans.pdf in your directory lab1/. You can use your favorite editor subject to that it is able to export pdf files which several freeware editors do. Files submitted in any other format will not be graded.
ii) We will use turnin to manage lab assignment submissions. Please check that the relevant source code including Makefile are included in the v1/ and v2/ subdirectories of lab1. In the parent directory of lab1, run the command
turnin -c cs422 -p lab1 lab1
You can check/list the submitted files using
turnin -c cs422 -p lab1 -v