Programming Assignment 3 [Last Revision 8/2/12, 11:36am] Due 8:00pm Sunday November 4th This programming assignment is on the topic of synchronization, and consists of 4 parts, A, B, C, and D. Each part consists of a set of exercises which you should do. You will hand in TWO FILES: mykernel3.c and pa3d.c 1. your modifications to the operating system in mykernel3.c 2. your solution to Exercise D contained in pa3d.c Each of the programs below can be found in separate files, pa3a.c, pa3b.c, pa3c.c, and pa3d.c. To install this assignment: 1. Log in to cs341.cs.oberlin.edu using your class account. 2. Enter in the command, "getprogram3". This will create a directory called "pa3" and will copy the relevant files into that directory. 3. To compile, enter the command "make" (from within the pa3 directory). This will compile all of the programs. You can compile a particular program by specifying it as a parameter to make, e.g., "make pa3a". 4. To turn in the assignment, make sure that BOTH mykernel3.c and pa3d.c files that you need to turn in are in the current directory and enter the command "turninprogram3". Notes on grading 1. All you will be graded on is whether your code works, and how well it works (is it efficient in time and/or space). You will not be graded specifically on commenting/documentation. This doesn't mean you should not add comments as, if the grader can't figure out what your code does, the comments will often help. But, it's up to you. 2. Unless indicated otherwise, you should NOT use any library routines or any code that you did not write yourself other than the routines given to you. For example, if you need a data structure like a linked list or queue, you should create it yourself rather than relying on a C library. 3. You should NOT use dynamic memory allocation in your kernel. For example, you should not use malloc (both because it is a dynamic memory allocator, and it is a C library routine which, as indicated above, you should not use). Since any dynamic memory allocator may fail (if all the memory is used), the kernel cannot depend on it (otherwise it might fail, which would be catastrophic). /* Programming Assignment 3: Exercise A * * In this assignment, you will implement semaphores, and use them in a * program that simulates cars sharing a single-lane road and that require * synchronization. * * In this first exercise, we begin by studying a simulation program * that creates multiple processes, where each process simulates a car * entering, proceeding over, and exiting, a single-lane road. The road * is 10 miles long, and consists of 10 positions (each of length 1 mile). * * Cars entering ------------------------------------------ Cars entering * from the WEST -> | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <- from the EAST * ------------------------------------------ * * Cars may enter the road from the West, at position 1, or they may enter * from the East, at position 10. Cars enter and proceed at a certain speed * (expressed in MPH, miles per hour) and maintain that speed until they exit. * * The primary rule of the road is that each position can be occupied by * at most 1 car at any time. If a car tries to proceed to a position * that is already occupied, a CRASH occurs. When a crash occurs, both * cars get automatically removed from the road (allowing other cars to * proceed). * * * In the program below, two cars drive over the road in the same direction, * both entering from the West (pos 1). Car 1 drives at 40 MPH, and Car 2 * at 60 MPH. The procedure DriveRoad simulates this activity: it takes * the car's ID (process ID), entrance point (EAST or WEST), and speed * (in MPH). To make sure that both cars do not collide by entering the * road at the same time, Car 2 waits some time before entering. This is * implemented by the procedure Delay (t) which takes a time (in seconds) * as a parameter. In the program, Car 2 delays by 900 seconds. Since * Car 1 drives at 40 MPH, it will travel the 10-mile road in 15 minutes, * or 900 seconds. Hence, Car 2 will enter at just about the time that * Car 1 exits. * * Exercises * * 1. Run the program and note the output. A line is printed each time a * car makes a move (either to enter, proceed, or exit, the road) or if a * collision occurs. * * 2. Since Car 2 is delayed by 900 seconds, it will have waited until Car 1 * exits (note that times are NOT exact in the simulation, so Car 1 may or * may not exit right at the time Car 2 enters). Modify the program so that * Car 2 delays by only 450 seconds. What happens, and why? * * 3. Now modify the program so Car 2 delays by only 300 seconds. What * happens, and why? Run the program a few times to see if the same events * occur in the same way each time. * * 4. Modify the program so Car 2 delays by 0 seconds, just to see where * the crash occurs. * * 5. Now change the direction of Car 2 so that it enters from the East * (thus, starting at position 10). Leave the delay at 0. Where does the * crash occur, and why? * * 6. Still having both cars go in opposite directions, modify the delay so * that a crash does not occur. How large does the delay have to be? * * 7. Study the implementation of DriveRoad. In addition to the Delay * procedure, it makes use of two other procedures, EnterRoad, and ProceedRoad. * EnterRoad causes a car to enter the road, and takes a parameter indicating * the car's point of entrance. ProceedRoad simply causes the car to * move one position (one mile), in the direction it entered. Notice how * movement is implemented by having the cars delay by 3600/mph, the number * of seconds in an hour divided by the speed in miles per hour. */ #include #include "aux.h" #include "umix.h" #define CAR1 1 #define CAR2 2 int driveRoad (int c, int from, int mph); void Main () { if (Fork () == 0) { /* Car 2 */ Delay (900); /* wait 900 simulated secs */ driveRoad (CAR2, WEST, 60); /* enter from West at 60 mph */ Exit (); } driveRoad (CAR1, WEST, 40); /* Car 1 */ Exit (); } #define IPOS(FROM) (((FROM) == WEST) ? 1 : NUMPOS) int driveRoad (c, from, mph) int c, from, mph; { int p, np, i; /* positions */ EnterRoad (from); PrintRoad (); Printf ("Car %d enters at %d at %d mph\n", c, IPOS(from), mph); for (i = 1; i < NUMPOS; i++) { if (from == WEST) { p = i; np = i + 1; } else { p = NUMPOS + 1 - i; np = p - 1; } Delay (3600/mph); ProceedRoad (); PrintRoad (); Printf ("Car %d moves from %d to %d\n", c, p, np); } Delay (3600/mph); ProceedRoad (); PrintRoad (); Printf ("Car %d exits road\n", c); } /* Programming Assignment 3: Exercise B * * One way to solve the "race condition" causing the cars to crash is to add * synchronization directives that cause cars to wait for others. In this * assignment, we will use semaphores. The kernel supports a large number * of semaphores (defined by MAXSEMS in umix.h, currently set to 100), and * each semaphore is identified by an integer 0 - 99 (MAXSEMS-1). Semaphore * operations are: * * int s = Seminit (int v) * * Allocates a semaphore and initializes its value to v. * Returns a unique identifier s of the semaphore, which is * then used to refer to the semaphore in Wait and Signal * operations. Returns -1 if unsuccessful (e.g., if there * are no available semaphores). * * Wait (int s) * * Semaphore wait operation. * * Signal (int s) * * Semaphore signal operation. * * The above are system calls that can be called by user programs. These * procedures cause a trap into the kernel, and each calls a corresponding * procedure located in mykernel3.c: * * int s = MySeminit (int p, int v) * * MyWait (int p, int s) * * MySignal (int p, int s) * * Notice that these routines take an additional parameter p, which is the * process ID of the calling process. Given these interfaces, you are to * implement these routines. * * Exercises * * 1. Study the program below. Process 1 (Car 1) allocates a semaphore, * storing its ID in sem, and initializes its value to 0. It then creates * process 2 (Car 2) which immediately executes Wait (sem). What should * happen to process 2 given that sem is initialized to 0? After driving * over the road, process 1 executes Signal (sem). What should happen to * process 2? * * 2. Study the file mykernel3.c. It contains a skeletal data structure and * code for the semaphore operations. Notice how MySeminit finds a free * entry in the semaphore table, allocates it, initializes it, and uses * the index as the semaphore ID that is returned. The other routines, * MyWait and MySignal have minimal bodies that decrement and increment * the semaphore value, but have no effect on synchronization. To * implement synchronization, you need two utility kernel functions * that are provided to you: * * Block (int p) causes process p to block. It basically removes p * from being eligible for scheduling, and context switches to another * process that is eligible. * * Unblock (int p) causes process p to be eligible for scheduling. * This does not mean it will execute immediately, but only that * when a scheduling decision is made, p may be selected. * * Given these utility routines, implement the semaphore routines. * * NOTE: The kernel already enforces atomicity of MySignal and MyWait, * so you do NOT need to implement any additional mechansims for atomicity. * * 3. Run the program below. It should now cause Car 2 to wait for Car 1 * before driving over the road, thus avoiding a crash. * */ #include #include "aux.h" #include "umix.h" #define CAR1 1 #define CAR2 2 void driveRoad (int c, int from, int mph); void Main () { int sem; sem = Seminit (0); if (Fork () == 0) { /* Car 2 */ Wait (sem); Delay (0); driveRoad (CAR2, EAST, 60); Exit (); } driveRoad (CAR1, WEST, 40); /* Car 1 */ Signal (sem); Exit (); } #define IPOS(FROM) (((FROM) == WEST) ? 1 : NUMPOS) void driveRoad (c, from, mph) int c, from, mph; { int p, np, i; /* positions */ EnterRoad (from); PrintRoad (); Printf ("Car %d enters at %d at %d mph\n", c, IPOS(from), mph); for (i = 1; i < NUMPOS; i++) { if (from == WEST) { p = i; np = i + 1; } else { p = NUMPOS + 1 - i; np = p - 1; } Delay (3600/mph); ProceedRoad (); PrintRoad (); Printf ("Car %d moves from %d to %d\n", c, p, np); } Delay (3600/mph); ProceedRoad (); PrintRoad (); Printf ("Car %d exits road\n", c); } /* Programming Assignment 3: Exercise C * * How to Use Shared Memory * * Processes in Umix are able to share memory. An explicit mechanism is * needed because the default for processes is that, as in Unix, their * memories are independent of each other, with no portion being shared. * When a Fork occurs, a new process is created with its own memory that * is initialized with the state of the memory of the creating process. * But from that point on, when one of the processes modifies its private * memory, the private memory of the other process is unaffected. Hence, * the need for a shared memory area. * * Two processes can share memory by identifying and registering variables * to be shared. The variables in each process should be the same size * and type. To share a set of variables, one can simply define a struct * containing the variables as members and then share a variable declared * as the struct. The following example will clarify this. * * For two processes to share, say, three integers, a struct should be * defined in both programs as follows: * * struct { * int x, y, z; * } shm; * * This structure defines three integers x, y, and z, as its members, and * the variable shm is declared of this structure's type. * * Within the program, shm must be registered with the operating system * as a shared variable using the Regshm system call: * * Regshm ((char *) &shm, sizeof (shm)) * * The first parameter is the address of the shared (struct) variable, * and the second is its size. The latter is needed bacuse there is a * maximum size (MAXSHM) that the operating system supports for shared * memory. * * From then on, you can reference any of the shared variables via the * expression shm.x, where shm is the name of the addressing variable * and x is a member of the shared variable structure. * * If a process has a registered shared memory area, then any processes * it creates will inherit the shared memory, i.e., the child processes * do not have to call Regshm. Otherwise, processes need to each call * Regshm to share memory. * * If Regshm is called more than once, the latest registration supercedes * the previous ones (i.e., only the last one matters, the others are * ignored). Typically, all the variables that should be shared are * placed in a struct as shown above, and then registered once, with * no more registrations. * * A sample program is given below. Note that the output of the program * is actually indeterminate because without any synchronization (e.g., * semaphores), Process 2 may print before Process 1 sets the variables. */ #include #include "aux.h" #include "umix.h" struct { /* structure of variables to be shared */ int x; /* example of an integer variable */ char y[10]; /* example of an array of character variables */ } shm; void Main () { Regshm ((char *) &shm, sizeof (shm)); /* register as shared */ if (Fork () == 0) { /* Process 2 */ /* Proc 2 inherits the shared memory registered by Proc 1 */ Printf ("P2: x = %d, y[3] = %c\n", shm.x, shm.y[3]); Exit (0); } shm.x = 1062; /* Process 1 */ shm.y[3] = 'a'; Printf ("P1: x = %d, y[3] = %c\n", shm.x, shm.y[3]); } /* Programming Assignment 3: Exercise D * * Now that you have a working implementation of semaphores, you can * implement a more sophisticated synchronization scheme for the car * simulation. * * Study the program below. Car 1 begins driving over the road, entering * from the East at 40 mph. After 900 seconds, both Car 3 and Car 4 try to * enter the road. Car 1 may or may not have exited by this time (it should * exit after 900 seconds, but recall that the times in the simulation are * approximate). If Car 1 has not exited and Car 4 enters, they will crash * (why?). If Car 1 has exited, Car 3 and Car 4 will be able to enter the * road, but since they enter from opposite directions, they will eventually * crash. Finally, after 1200 seconds, Car 2 enters the road from the West, * and traveling twice as fast as Car 4. If Car 3 was not coming from the * opposite direction, Car 2 would eventually reach Car 4 from the back and * crash. (You may wish to experiment with reducing the initial delay of * Car 2, or increase the initial delay of Car 3, to cause Car 2 to crash * with Car 4 before Car 3 crashes with Car 4.) * * Exercises * * 1. Modify the procedure DriveRoad such that the following rules are obeyed: * * A. Avoid all collisions. * * B. Multiple cars should be allowed on the road, as long as they are * traveling in the same direction. * * C. If a car arrives and there are already other cars traveling in the * SAME DIRECTION, the arriving car should be allowed enter as soon as it * can. Two situations might prevent this car from entering immediately: * (1) there is a car immediately in front of it (going in the same * direction), and if it enters it will crash (which would break rule A); * (2) one or more cars have arrived at the other end to travel in the * opposite direction and are waiting for the current cars on the road * to exit, which is covered by the next rule. * * D. If a car arrives and there are already other cars traveling in the * OPPOSITE DIRECTION, the arriving car must wait until all these other * cars complete their course over the road and exit. It should only wait * for the cars already on the road to exit; no new cars traveling in the * same direction as the existing ones should be allowed to enter. * * E. This last rule implies that if there are multiple cars at each end * waiting to enter the road, each side will take turns in allowing one * car to enter and exit. (However, if there are no cars waiting at one * end, then as cars arrive at the other end, they should be allowed to * enter the road immediately.) * * F. If the road is free (no cars), then any car attempting to enter * should not be prevented from doing so. * * G. All starvation must be avoided. For example, any car that is * waiting must eventually be allowed to proceed. * * This must be achieved ONLY by adding synchronization and making use of * shared memory (as described in Exercise C). You should NOT modify the * delays or speeds to solve the problem. In other words, the delays and * speeds are givens, and your goal is to enforce the above rules by making * use of only semaphores and shared memory. * * 2. Devise different tests (using different numbers of cars, speeds, * directions) to see whether your improved implementation of DriveRoad * obeys the rules above. * * IMPLEMENTATION GUIDELINES * * 1. Avoid busy waiting. In class one of the reasons given for using * semaphores was to avoid busy waiting in user code and limit it to * minimal use in certain parts of the kernel. This is because busy * waiting uses up CPU time, but a blocked process does not. You have * semaphores available to implement the driveRoad function, so you * should not use busy waiting anywhere. * * 2. Prevent race conditions. One reason for using semaphores is to * enforce mutual exclusion on critical sections to avoid race conditions. * You will be using shared memory in your driveRoad implementation. * Identify the places in your code where there may be race conditions * (the result of a computation on shared memory depends on the order * that processes execute). Prevent these race conditions from occurring * by using semaphores. * * 3. Implement semaphores fully and robustly. It is possible for your * driveRoad function to work with an incorrect implementation of * semaphores, because controlling cars does not exercise every use of * semaphores. You will be penalized if your semaphores are not correctly * implemented, even if your driveRoad works. * * 4. Avoid starvation. This is especially relevant when implementing the * Signal function. If there are multiple processes that blocked on the * same semaphore, then a good policy is to unblock them in FIFO order. * * 5. Control cars with semaphores: Semaphores should be the basic * mechanism for enforcing the rules on driving cars. You should not * force cars to delay in other ways inside driveRoad such as by calling * the Delay function or changing the speed of a car. (You can leave in * the delay that is already there that represents the car's speed, just * don't add any additional delaying). Also, you should not be making * decisions on what cars do using calculations based on car speed (since * there are a number of unpredictable factors that can affect the * actual cars' progress). * * GRADING INFORMATION * * 1. Semaphores: We will run a number of programs that test your * semaphores directly (without using cars at all). For example: * enforcing mututal exclusion, testing robustness of your list of * waiting processes, calling signal and wait many times to make sure * the semaphore state is consistent, etc. * * 2. Cars: We will run some tests of cars arriving in different ways, * to make sure that you correctly enforce all the rules for cars given * in the assignment. We will use a correct semaphore implementation for * these tests so that even if your semaphores are not correct you could * still get full credit on the driving part of the grade. Think about * how your driveRoad might handle different situations and write your * own tests with cars arriving in different ways to make sure you handle * all cases correctly. * * * WHAT TO TURN IN * * You must turn in two files: mykernel3.c and p3d.c. mykernel3.c should * contain you implementation of semaphores, and p3d.c should contain * your modified version of InitRoad and DriveRoad (Main will be ignored). * Note that you may set up your static shared memory struct and other * functions as you wish. They should be accessed via InitRoad and DriveRoad, * as those are the functions that we will call to test your code. * * Your programs will be tested with various Main programs that will exercise * your semaphore implementation, AND different numbers of cars, directions, * and speeds, to exercise your DriveRoad function. Our Main programs will * first call InitRoad before calling DriveRoad. Make sure you do as much * rigorous testing yourself to be sure your implementations are robust. */ #include #include "aux.h" #include "umix.h" #define CAR1 1 #define CAR2 2 #define CAR3 3 #define CAR4 4 void InitRoad (); void driveRoad (int c, int from, int mph); void Main () { InitRoad (); /* The following code is specific to this particular simulation, * e.g., number of cars, directions, and speeds. You should * experiment with different numbers of cars, directions, and * speeds to test your modification of DriveRoad. When your * solution is tested, we will use different Main procedures, * which will first call InitRoad before any calls to DriveRoad. * So, you should do any initializations in InitRoad. */ if (Fork () == 0) { /* Car 2 */ Delay (1162); driveRoad (CAR2, WEST, 60); Exit (); } if (Fork () == 0) { /* Car 3 */ Delay (900); driveRoad (CAR3, EAST, 50); Exit (); } if (Fork () == 0) { /* Car 4 */ Delay (900); driveRoad (CAR4, WEST, 30); Exit (); } driveRoad (CAR1, EAST, 40); /* Car 1 */ Exit (); } /* Our tests will call your versions of InitRoad and DriveRoad, so your * solution to the car simulation should be limited to modifying the code * below. This is in addition to your implementation of semaphores * contained in mykernel3.c. */ void InitRoad () { /* do any initializations here */ } #define IPOS(FROM) (((FROM) == WEST) ? 1 : NUMPOS) void driveRoad (c, from, mph) int c, from, mph; { int p, np, i; /* positions */ EnterRoad (from); PrintRoad (); Printf ("Car %d enters at %d at %d mph\n", c, IPOS(from), mph); for (i = 1; i < NUMPOS; i++) { if (from == WEST) { p = i; np = i + 1; } else { p = NUMPOS + 1 - i; np = p - 1; } Delay (3600/mph); ProceedRoad (); PrintRoad (); Printf ("Car %d moves from %d to %d\n", c, p, np); } Delay (3600/mph); ProceedRoad (); PrintRoad (); Printf ("Car %d exits road\n", c); }