Project 3a: Using Shared Memory in LinuxYou are likely to find that it is not possible to divide this project up by "server" vs "client" (and have one person work on each part) since they need to work together closely; you are likely to have the most success if you work together. This project will not be a lot of code, but it can be tricky to use the suggested library routines correctly.
UPDATE: For consistency, all functions, files, and types should begin with the string "stats_" and not "stat_" as some may have been named before.
There are numerous objectives to this assignment:
OverviewIn this assignment, you will implement a client and server that communicate through a shared memory segment to display statistics about the client processes. All of the processes are running on the same machine. Each client process will periodically write to the shared memory segment with updates about its recent behavior (e.g., how much progress it has made and how much CPU it has been allocated); the server process collects this information (by reading from the shared memory segment) and periodically displays the information for all the client processes. As you know by now, when processes (or threads) cooperate through shared memory, they require synchronization primitives (such as locks or semaphores) to ensure that they do not have race conditions when they are each simultaneously updating the same memory locations. To minimize our need for synchronization in this Project (synchronization is for Project 4!), we will construct the clients and server so that only a single client is (usually) writing to each memory location and the server is only reading (not writing); if the server happens to occasionally read data that is not up-to-date, that is okay, since the data is just usage statistics (and not your bank account). The only time you will need to worry about mutual exclusion will be when clients are first starting and when they exit. For this project, you will be implementing three components: a server process that displays client statistics every second, a library containing two functions for updating those statistics, and a client process that sleeps and computes at rates specified by command-line arguments. With these statistics, we hope that you will be able to see more about how the Linux scheduler treats processes at different priorities and with different behavior. Server Process
Let us consider what the server process,
First, it is the responsibility of the server process to create and
intialize the shared memory segment. A shared memory segment can be
created with Each shared memory segment is identified with a unique key; this
key is used to match requests for a particular segment between the
server and the clients. You should obtain this key from the command
line; that is, when the server is started from the command line, it
expects a single flag stats_server -k 931starts the server using 931 as the key for the shared memory segment. To attach this shared memory segment to the server's address
space, you will use the If anything goes wrong with this setup (e.g., the shared memory segment cannot be exclusively created), then the After the server process creates this single page of shared memory and initializes it appropriately, it should enter an infinite loop where it continually sleeps for one second, reads the contents of shared memory, and then displays the statistics that have been written to shared memory to STDOUT. There should be one line of output for each client process (followed by an extra blank line after each second); the format of each line of output should be as follows: [server_iter] [pid] [argv[0]] [counter] [cpu_secs] [priority] For example, if there are two client processes, the 21st time through the server's loop, the output may look like the following: 21 1267 hello_world 36 1.26 10 21 1272 cpu_work 4056 0.56 18Then, the next second (the 22nd iteration), the output could look as follows: 22 1267 hello_world 52 2.06 10 22 1272 cpu_work 5391 0.86 18If in the next second, a new client starts writing to the shared memory segment, the next output could look like this: 23 1267 hello_world 88 2.83 10 23 1272 cpu_work 6183 1.87 18 23 1277 sleep_work 2 0.01 16Finally, if a client terminates, then in the next second, the output could look like this: 24 1267 hello_world 126 3.51 10 24 1277 sleep_work 5 0.02 16 For the name of the client process, regardless of the actual length of argv[0], the server should display only up to 15 characters; whether the client or the server performs this truncation is up to you. Similarly,
So that the client and the server agree on the format of the shared
memory segment, you will need to define a structure for each client's
statistics. The details of this structure are up to you. We suggest
treating the shared memory segment as an array of these structures.
We've started a definition for the type Note that the server does not compute or interpret any of these statistics; the server simply displays the information that it reads from the shared memory segment (though it might do truncating or formatting). The server should create only a single page of shared memory. Don't guess or hard-code this value; figure out how you can find the right page size at run-time! Your server must be able to handle 16 clients. If more than 16 clients at a single time try to use this shared memory segment, those clients should receive an error. When a client terminates, it must reset its entry in the shared memory page so that the entry can be used by another client later. One of your challenges when implementing the server will be to determine the number of valid clients; don't print out any garbage if there are fewer than 16 running clients! To ensure that only one client at a time is searching through and
modifying the essential structures of the shared memory segment, you
should use a semaphore to provide mutual exclusion across
both To create a semaphore that can be used across unrelated processes,
you should use if ((mutex = sem_open("mysemaphore -- key?", O_CREAT, 0644, 1)) == SEM_FAILED) { perror("sem_open"); exit(1); } The clients can then use When your server terminates, you will need to ensure that it
correctly removes the shared memory segment and semaphore so that each
resource does not remain allocated forever. Look
into You may find it useful to run the command
UPDATE: As you all know, when you use sem_open() to create a new semaphore, you specify a name for that semaphore (as the first argument). If you get unlucky with your naming choice and you pick the same name as someone else running on your machine, you may get unexpected behavior (e.g., you won't have permission to open the existing semaphore).
To ensure that your program behaves as expected, we strongly recommend that you use your cslogin as a unique prefix in your semaphore name (e.g., for me, I could use "dusseau_sem" or "dusseauP3" or "dusseauXYZ"). Note that when we run your P3a code for grading, your code could run into this name conflict problem -- so fix this even if you haven't seen this problem in your own experience.
Client Library Functions
To simplify the task of constructing client processes you will
encapsulate the functionality of interacting with the server into two
library routines:
To define the One of your challenges for the client library code will be how to determine which structures in the shared memory segment correspond to currently active clients so that a new client can be assigned to the next (free) structure (and the corresponding address returned). To ensure that only one client at a time is searching through and
modifying the essential structures of the shared memory segment, you
should use a semaphore to provide mutual exclusion across
both Within your You must provide these two routines in a shared library named "libstats.so". Placing the routines in a shared library instead of a simple object file makes it easier for other programmers to link with your code. There are further advantages to shared (dynamic) libraries over static libraries. When you link with a static library, the code for the entire library is merged with your object code to create your executable; if you link to many static libraries, your executable will be enormous. However, when you link to a shared library, the library's code is not merged with your program's object code; instead, a small amount of stub code is inserted into your object code and the stub code finds and invokes the library code when you execute the program. Therefore, shared libraries have two advantages: they lead to smaller executables and they enable users to use the most recent version of the library at run-time. To create a shared library named libstats.so, use the following commands (assuming your library code is in a single file "stats.c"): gcc -c -fpic stats.c -Wall -Werror gcc -shared -o libstats.so stats.o To link with this library, you simply specify the base name of the library with "-lstats" and the path so that the linker can find the library "-L.". You'll need something like this: gcc -o myprogram mymain.c -Wall -Werror -lstats -L. Of course, these commands should be placed in a Makefile. Before you run "myprogram", you will need to set the environment variable, LD_LIBRARY_PATH, so that the system can find your library at run-time. Assuming you always run myprogram from this same directory, you can use the command: export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH Note that export is what you use in bash; if you are not using bash, you'll have to figure out the alternative command to set the environment. Client ProcessTo use your library functions, you will need to create a client process. Multiple clients may connect simultaneously to the same server and same shared memory segment.After performing some initialization (and calling stats_init), your client process will forever iterate between sleeping and computing; at the end of each iteration, it will increment a counter and ensure that it has updated all the statistics that are being tracked about it.
This client process, stats_client -k key -p priority -s sleeptime_ns -c cputime_nsFor example: stats_client -k 841 -p 10 -s 1000 -c 1000000If other flags are specified, stats_client should print
an error message and exit with return code 1. If a flag is not specified, choose a reasonable default value.
These fields should have the following effects:
To summarize, the
Finally, when should your client program
call You are likely to need to link with a number of libraries to create your server and client. To ensure that we compile your server, your library, and your client process correctly for the demo, you will need to create a simple makefile. The makefile must make all three targets. If you don't know how to write a makefile, you might want to look at the man pages for make. Make sure the file stats.h defines your completed main structure, again, with no changes to the fields we defined.
For testing your stats library, we may also create our own client
processes with different behavior. We will include
your
We will again verify that your code passes lint and valgrind tests, as in Project P1a and P2a. |