Inter-Process Communication/Synchronization via Shared Memory

In homework #1, you learned how one process can create (a.k.a. "spawn" or "fork") another process using the system service call fork( ). In homework #2, you learned how a thread can create another thread.  As you saw in both homework #2 and again in homework #3, it's easy to arrange for multiple threads within the same process to communicate via shared memory:  All threads in a process share the same physical address space so any global variables are certainly accessible (common) to all the threads.  Processes, however, normally share no memory with other processes so even a parent/child process pair whose address spaces are initially almost identical in content can't communicate via common variables, since there aren't any: The contents of the parent's address space, obviously including all the variables in the stack, heap, and data sections, are initially copied into the child's address space during the spawn (Unix does that; but Windows doesn't), but even if so, after that initialization of the child's space, those two physical address spaces are totally independent of one another; changes the child makes to its variables (including global variables in its data section) are not reflected in the parent's address space nor vice versa. If we want processes to share access to some common memory as a means of interprocess communication, we have to make special arrangements, which is what you'll do for this assignment:  Use the system service calls shmget and shmat to obtain some new memory that will be accessible to multiple processes (and use shmdt and shmctl to clean up when you're done).  Here's what I want your code to do:

  1. The parent process (only process at the start, right?) should get some new memory which will be shared with a child process that it (the parent) will create later.  No magic here:  In CS125 or some other C programming course, you've used the system service call malloc to get new memory before.  Now you just need to learn to use a more complicated set of system service calls to get new memory that can be shared between processes (malloc'd memory is on the heap, remember, so it can't be shared between processes).  All we need here is enough memory for an integer (later we'll try something more complicated ;-)
      
    1. First use shmget to tell the OS to assign you a block of memory (to be shared among multiple processes) big enough to hold an integer; have your program print out the shared memory identifier it receives from shmget. Shared memory identifier? Just an arbitrary number that identifies some chunk of shared memory, much the way a process identifier identifies a process. A shared memory identifier is not a segment number or address. Why not? Well, for one thing, shared memory can be shared among processes running very different programs --- in this assignment, parent and child run the same code, but that need not be true in general, as you demonstrated in your program #1, when child #2 used an execv to load a new program. There's no way to insure that separate processes could all use the same segment number for a shared memory segment--- different processes may have a different number of segments in their logical address space; and if they're not parent and child, as they will be for this assignment, they probably will have different numbers of segments in their respective segment tables when shmat comes along and attaches a new one. Although a shared segment is indeed a segment, the shared memoryID, which must somehow be made common among all processes wanting to use this segment, is not a segment number in the MMU/address binding sense of the term; but different processes can use different segment numbers to bind/attach to the same physical address just by putting the same physical address in their (different) entries in their respective segment tables. The point of the shared memory identifier is to allow multiple processes to have a common name to use for shmat to refer to a region of memory that may be (generally will be) at different logical addresses in the different processes --- hey guys, how about we all communicate via shared memory ID #37; shmat will tell each of us individually what address to use to get to this shared block of memory.
        
    2. Next, use shmat to get the address for your process to use to refer to the shared memory (this step is known as "attaching" the shared memory to your process). shmat adds an entry to the calling process's segment table, often/usually assigning a different segment number in each process, but makes the base physical address of the new segment the same in the segment tables of all processes attached to this shared memory. But the protection bits in each process's segment table could be different if some processes were only to be allowed read access while others could write as well as read.
        
  2. The parent process should set the new (shareable) integer to 0 and then spawn a child process (and make sure to do this in the correct order --- first set the new, shared memory to 0, then spawn a child).
      
  3. In the parent (after the spawn):
      
    1. Ask the user for a (non-zero) value to store in the new integer.
        
    2. Once that has been accomplished, the parent should spin on that integer until it becomes zero again.
        
    3. After exiting from the spin, the parent should print a message saying that the shared integer is now 0 again (thus confirming successful two way communications withthe child via shared memory).
        
    4. Detach the shared memory from the process's address space using shmdt
        
    5. Return the shared memory segment to the memory manager using shmctl (roughly analogous to the "free" function used to return malloc'd memory).
        
    6. Print out an "all done" message.
        
  4. In the child (after the spawn):
      
    1. Print some sort of "I'm alive" message
        
    2. Spin on the new integer until it becomes non-zero. Note: You caught a small break here; when a child process is spawned, it inherits any shared memory attachments of its parent; otherwise the child too would have to call shmat (as the parent did) before it could "see" the shared memory. How would the child know what shared memory segment identifier to attach to? In Unix/Linux, that variable would be "passed" to the child when the parent's address space was copied over during the spawn; exactly the same way your child processes figured out which child they were in homework #1 (in Windows it would be more difficult). Anyway, because a child inherits its parent's attachments (otherwise how could it's logical address space be a copy of its parent's?), it doesn't need to do a shmat itself, provided of course, that the parent attached before spawning the child.
        
    3. After exiting from the spin (because the parent eventually puts a user-supplied non-zero value in the shared integer), print out the value just received from its parent and then re-set the shared integer back to 0 (so the parent can eventually stop spinning and finish up).
        
    4. That's it for the child. You don't need to detach the child from the shared memory (the OS does that when a process terminates, which the child will do when it runs out of code to execute after step 4c, above) and you certainly don't want the child to destroy the shared memory segment since the parent may still need it to finish its printout. When the parent is done printing out the value the child re-zeroed, it (the parent) removes/destroys the (no longer shared) shared memory segment in step 3e, above — it better; shared memory segments are persistent, they are allowed to continue to exist after their creating process terminates (as a means, perhaps, of allowing interprocess communication from beyond the grave). The first few times I myself tried this sort of programming I eventually discovered that I had left some previously shared segments around from several years earlier. Now I clean up each year over Xmas break, just in case you folks don't do your own cleanup properly (see Note D, below). Here (this homework) there's no need to leave that shared memory segment lying around "orphaned" after the parent terminates; so we'll have the parent return it (the previously shared segment) to the OS before it (the parent) terminates.

Here's what a sample output of mine looks like (user input in orange);

Parent: Successfully created shared memory segment with shared memory ID # (not segment #) of 12714047 (This shared memory doesn't get a true segment number until this process adds it to its segment table by attaching to it.)

Parent: My pid is 29355; now spawning a child after setting the shared integer to 0

Child: My pid is 29356, my parent's pid is 29355; the shared integer value is currently 0; I'll spin until it's not 0

Parent: My pid is 29355, spawned a child with pid of 29356; please enter an integer to be stored in shared memory: 48

Child: The value in the shared integer is now 48; I'll set it back to 0

Parent: the child has re-zeroed our shared integer

Child process terminating

Parent: Child terminated; parent successfully removed segment whose ID # was 12714047

 

Notes:

  1. This is a simple program to write; this writeup is a lot longer than the actual code itself will be. The challenge here of course is learning to use the new (and to be fair, modestly complex) system service calls. But the overall code is short and straightforward. You are welcome (encouraged, in fact) to scour the web for examples of using shmget, shmat, shmdt, and shmctl to help you figure out how to use them. I think our textbook may have an example somewhere and I'm sure there are some on the web somewhere
      
    1. Make sure any web example is for Linux; different OS's can and do differ, even for things that are supposed to be standard. Note: shmget, shmat, shmdt, and shmctl are documented under "System Calls" in the Linux documentation
        
    2. Often, web examples are are intended to show how all the parameters to these system service calls work. That usually makes them a fair bit more complicated than we need them to be for this simple problem. I'll deduct a point or two here if you don't sufficiently simplify your code. But better, as far as grading is concerned, that you copy (and get working!) an overly complicated example that you don't fully understand than you don't get anything to work at all. Note that that's a leniency we can afford in an academic setting; it won't carry over to industry. "Oh yeah, the 747 crashed 'cause I didn't really understand the system service call I used – it worked when I tested it on my desktop, though."
        
  2. When perusing any examples you find, remember that identifiers in ALL_CAPS are just human-readable symbolic names (#defined in the .h files you include as per the Linux documentation) for constants used to control the behavior of the system service calls. Which is easier to understand: shmget(IPC_PRIVATE, ... ) or shmget( (key_t) 0, ... ) ? Note that sometimes these constants represent individual bits (e.g. 0x00004000), so in the examples you find you might see something like ... IPC_CREAT | IPC_EXCL ... (which does a bitwise "or" of two constants), although I don't think this particular assignment requires anything that complicated. If you want or need to know what these things mean and it isn't clear from the name (is the meaning of "IPC_EXCL" intuitively clear to you? Me neither) either browse the web some more or be a real pro and learn to use the Unix utility grep and search the header files in /usr/include/sys    Here's how I figured out what IPC_EXCL means:
  3. % grep IPC_EXCL /usr/include/sys/*
    /usr/include/sys/ipc.h:#define IPC_EXCL 0002000 /* fail if key exists */

  4. You're using 4 new system service calls here, each of them more complicated than the first one we ever tried in this class (fork). Unless you're a lot better at this stuff than I was, you'll have some failures as you try to understand the Linux system service call documentation and put it into practice. Remember to use perror or strerror to help you troubleshoot your problems with system service calls. Don't assume that just because your code compiles and executes without blowing up that the system service calls are actually doing what you want them to. There are usually several ways they can fail without necessarily causing your program to blow up. You need to check for errors after each one; and then use perror or strerror to help you figure out what went wrong.

  5. While learning to use shmctl, you may wind up creating an "orphan" segment, meaning that you created it successfully (with shmget) but couldn't get shmctl to delete it for you before your program terminated. Such orphans will, in fact, survive after your program/process terminates (or dies at the hands of an irritated OS). Use ipcs -m as a command to the shell to get a list of all shared memory segments you have access to. Any of those that list you as an owner you can delete using ipcrm -m followed by the shared memory identifier you discovered using ipcs -m

  6. Although technically the two processes for this homework assignment have a critical section over the same resource (the shared integer), race conditions aren't a concern for us here because we are in fact, synchronizing with our spins. (That probably isn't true in general, of course, but it is for this assignment). Note that that's an argument, and not actually a particularly good one, but hardly a proof. In any event, don't worry about race conditions for this assignment. The next and last program of the semester (which may be optional) will combine this current program with homework #3: You'll create a structure in shared memory that includes two integers in it and have to synchronize two processes to prevent race conditions as they update the structure concurrently. Don't worry about that just yet, however; let's go one step at a time. Do this problem first.