You are on page 1of 11

EECS 678 Introduction to Operating Systems

Homework 2 Answer Key (Total of 200 Points)

Question 1 (Chap 6; 20 Points) Consider the following set of processes P1 and P2:
P1: { shared int x; x = 7; while (1) { x--; x++; if (x != 7) { printf(``x is %d'',x) } } } } P2:{ shared int x; x = 7; while (1) { x--; x++; if (x != 7) { printf(``x is %d'',x) } }

Note that the scheduler in a uniprocessor system would implement pseudo-parallel execution of these two concurrent processes by interleaving their instructions, without restriction on the order of the interleaving. 1. Show an execution (i.e., trace the sequence of inter-leavings of statements) such that the statement x is 7 is printed. 2. Show how the statement x is 9 is printed. You should remember that the increments/decrements at the source language level are not done atomically, i.e., the assembly language code:
3. 4. 5. 6. 7. 8. 9. LD INCR STO R0,X R0 R0,X /* load R0 from memory location x */ /* increment R0 */ /* store the incremented value back in X */

implements the single C increment instruction (x++).


For an output of x is 7, the interleaving producing the required behavior is fairly easy to find since it requires only an interleaving at the source language statement level. The essential fact here is that the test for the value of x is interleaved with the increment of x by the other process. Thus, x was not equal to 7 when the test was performed, but was equal to 7 by the time the value of xwas read from memory for printing.
P1: P1: P2: P1: P2: P1: x--; x++; x--; if (x != 7) x++; printf("x is %d", x); M(x) 6 7 6 6 7 7

"x is 7" is printed.

For x is 9 we need to be more inventive, since we need to use inter-leavings of the machine instructions to find a way for the value of x to be established as 8 so it can then be evaluated as 9 in a later cycle. Notice that the machine instructions are divided into blocks at places where a context switch between P1 and P2 happens. Note also, that these context switches happen at machine instruction boundaries, but not always as source line boundaries. In other words, if you examine them closely you should be able to see that the context switches interleave portions of source language statements in each process's execution.
Instruction P1: LD R0, x P2: LD R0, x P2: DECR R0 P2: STO R0, x P1: P1: P1: P1: P1: P1: P2: P2: P2: P2: P2: P1: P1: P1: P1: DECR STO LD INCR STO if(x M(x) 7 7 7 6 P1-R0 7 7 7 7 6 6 6 7 7 7 7 7 P2-R0 -7 6 6 6 6 6 6 6 7 8 8

R0 6 R0, x 6 R0, x 6 R0 6 R0, x 7 != 7) printf("x is %d", x);

LD R0, x 7 INCR R0 7 STO R0, x 8 if(x != 7) printf("x is %d", x); "x is 8" is printed. LD DECR STO LD R0, x R0 R0, x R0, x 8 8 7 7 7 7 6

8 7 7 7 7 7 7 8 8

8 8 8 8 7 6 7 6 6

P2: LD R0, x P2: DECR R0 P2: STO R0, x

P1: INCR R0 6 P1: STO R0, x 8 P1: if(x != 7) printf("x is %d", x);

P1: "x is 8" is printed. P2: P2: P2: P2: P2: LD R0, x 8 INCR R0 8 STO R0, x 9 if(x != 7) printf("x is %d", x); "x is 9" is printed. 8 8 8 8 9 9

Question 2 (Chap 6; 15 Points) In the previous problem, concurrent access to a shared variable was done without protecting it as a critical section. Consider what portion of the code in each process should be defined as a critical section, and show where/how to add semaphores to the program in the previous problem to control concurrency in a reasonable way. Done correctly, this will ensure that the printf() is never executed. Your solution should allow as much concurrency as possible, which means that the critical section should be as small as possible - but no smaller. Note: treat the notion as much as possible reasonably and don't obsess over optimalitytoo much. remember that it is always better to make sure it is correct, if less efficient than it might be, as opposed to striving for too much concurrency and straying onto an incorrect part of the design space.

Here the solution is simple: enclose the operations on the shared variable within semaphore operations which will ensure that only one process will operate on x at a time. The only trick here is to realize that surrounding the statements that decrementand increment x is not enough. We have to enclose the if statement as well since it tests the value of x. If we do not, erroneous printing can still happen if one process is in the middle of incrementing and decrementingx, while another is testing x at theif.
s: semaphore; P1: { shared int x; P(s); x = 7; V(s); for (;;) { P(s); x--; x++; if (x != 7) printf("x is %d", x); V(s); } } P2: { shared int x; P(s); x = 7; V(s);

for (;;) { P(s); x--; x++; if (x != 7) printf("x is %d", x); V(s); } }

Question 3 (Chap 6; 15 Points) Explain why spinlocks are not appropriate for uniprocessor systems, yet may be suitable for multiprocessor systems.

A spin lock is a busy-waiting approach to concurrency control. That means that when a process enters a spin-lock implementation of the P operation, and the desired semaphore is already held, it will sit in a loop continuously looking to see if the semaphore has become free. The waiting process is thus fully CPU bound by the useless activity of running, or spinning and thus the name, through the loop. The is anexcellent approach to the problem, if all other concurrent computations in the system using the semaphore are known to use it for a short time, and will thus shortly free it. Spin-locks are thus anexcellent approach to locking many shared data structures in multi-processor systems because they are held for a short time by processes that will also not be interrupted. Any process blocking on such a lock is thus assured to wait for only a short period, since the process holding the lock is executing on another CPU and will be releasing the lock in a short time. A classic error in multiprocessor systems is for a process to block while holding a spin-lock, which can have drastic effects on system performance. The crucial assumption for Spin-locks is thus that the thread of control holding the lock is actively executing concurrently with the thread of control blocking on the lock. Sadly, in a uniprocessor system only one thread of control can be active at a time, because there is only one CPU, and the concurrency is generally pseudo-concurrency for that reason. DMA by network, disk, or other device controllers theoretically introduces physical concurrency, but that is not relevant here. Thus, in a uniprocessor system, if a thread of control uses a Spin-lock, and begins spinning on it because the lock is already held, the lock must be held by a process that is not executing. Spinning on the lock is thus a huge waste of time because the process spins through the loop continuously looking at a semaphore variable that cannot change valueuntil the spinning process gives up the CPU and permits the process holding the lock to execute again, thus giving it a chance to finish using the lock and to release it. At the logical extreme, the system is now deadlocked because the spinning process is CPU-bound, and will never give the process holding the lock a chance to run and thus release it. In reality, the CPU-time quantum of the spinning process will eventually expire, and priority aging will reduce its priority, so the process holding the lock will eventually run, so the system will not be permanently frozen, but a huge number of CPU cycles will certainly be wasted. This is why a sleepwakeup implementation for P and V is much more appropriate in uni-processors.

Question 4 (Chap 6; 15 Points) Show that if the wait and signal operations are not executed atomically, then mutual exclusion may be violated.

Pseudo-code, or an algorithmic description, of the wait and signal operations are defined as follows (Section 6.5, 7th Edition):
int S=1; wait(S) { while (S <= 0); S--; } signal(S) { S++; }

Processes that invoke wait(S) will be allowed to enter the critical section if S is greater than zero, and it will then decrement S to note its presence in the critical section. If the operation is not atomic, if the test of S and the decrement of S are not atomic, then it is possible that one process executes the test in the while loop, but is preempted just before decrementingS. Then a second process can also succeed in passing thewhile loop test, then decrement S and enter its critical section. Later, when the first process runs again, it also enters its critical section, thus violating mutual exclusion. This is why the atomic Test-and-Set is so often implemented in hardware, because that is the best way to make it atomic. Question 5 (Chap 6; 15 Points) The 678 teaching assistant holds office hours twice a week in his office. His office can hold 2 persons only, the TA and one student. Outside his office are 4 chairs for waiting students. If there are no students waiting to see the TA, the TA plays games. If a student arrives at the office and the TA is playing games, the student loudly clears his/her throat and the TA invites the student in and begins helping. If a student arrives at the office and the TA is busy with another student, the student waits in a chair outside the office until the TA is free. If the arriving student finds all the chairs occupied, then he leaves. Using semaphores, write two processes, student and TA, that synchronize access to the TA's office during his office hours. These processes will have approximately the following structure:
process TA loop <Enter protocol to synchronize with student> <Advise a student> <Exit protocol> end loop end TA process student <Enter protocol to synchronize with TA> <Get advice or leave> <Exit protocol> end student

(Although you are writing one student process, assume multiple instances of the process are active simultaneously.) Does your solution exhibit the properties of mutual exclusion, progress, and bounded wait?

The trick is to realize that semaphores can be used for signaling conditions, as well as for mutual exclusion. Thus if one process only calls wait (P()) on a semaphore and another process only calls signal (V()) on that semaphore, they turn the semaphore into a signaling mechanism between the two processes. We will use this technique to have the student processes signal their arrival to the TA (i.e., clear their throats), and to have the TA signal readiness
#define CHAIRS 4 shared semaphore shared semaphore waiting_students shared semaphore waiting shared int TA mutex (1=free) students = 0; // Indication of TA being busy = 1; // exclusive access to = 0; // Indication that students are

waiting_students = 0; // Counter for waiting students process student { wait(mutex); if (waiting_students < waiting_students++; signal(students);// clear signal(mutex); wait(TA); // wait if TA getAdvice(); } else { // there are no chairs: signal(mutex); } }

process TA { while(1) { wait(students); // playing games CHAIRS) { wait(mutex); waiting_students--; throat signal(mutex); signal(TA); // TA takes a student busy giveAdvice(); } } leave

Each semaphore has its own job to perform. The mutex semaphore ensures atomic updates to the counter keeping track of how many students are waiting, and to the decision by a student to wait or to leave based on a test of the "waiting_students" counter. Note that this counter value does not include the student currently being helped. The TA semaphore is free (value 1, done by signal(TA)) when the TA is willing to accept a new student, and is the method by which one of the students waiting on the TA semaphore enters the office since the student gains possession of the TA semaphore. Note that only students wait on the TA semaphore, and only the TA signals it. Note also that the TA starts out "busy" (value 0) and becomes available to students only when a student arrives. The students semaphore acts as signal to the TA process that at least one student is waiting. Only the TA waits on students, and only a student process signals it. Thus, if the TA process is blocked in the "wait(students)" statement, the arrival of a student wakes it up by releasing

it with "signal(students)". The limit on the number of waiting students is imposed by using the counterwaiting_students. Increment and decrement of this counter is atomicbecause all operations on it are protected by exclusive access to the semaphore mutex. This solution does not provide bounded wait for two reasons. First, if a student process arrives and finds all the chairs occupied, it will leave. This can happen an unbounded number of times. Second, because there is no queuing of the waiting students. When the TA calls "Next!" the waiting students metaphorically rush the door, competing desperately for the chance at an educational interlude with the TA (Hey - this is my metaphor and I can fantasize, can't I?). Intellectual starvation of a student could occur if they were never able to win the rush to the TA's door, perhaps because they are carrying too many books, and if so many students desired education that the starving student was never the sole waiting student. Fixing this problem first requires buying the TA an unbounded number of chairs. "Say, buddy, I've got a shipment of an infinite number of chairs for you. Where do you want them?" Then, it would require a queuing protocol to ensure bounded waiting time for each student. Thewaiting_students counter might become a linked list implementing FCFS queuing to ensure this. The physical world would model this if the students took numbers and the lowest was always served next. Another approach would be if the chairs were all in a line and a student always joined the queue at the first available seat, if the student in the seat closest to the door always was served next, and if all students shifted over one seat when someone went to talk to the TA. Question 6 (Chap 7; 15 Points) Is it possible to have a deadlock involving only one process? Explain your answer.

Recall that there are four necessary conditions for deadlock. Number 4 is circular-wait. Deadlock with one process is not possible, because it is not possible to have circular wait with only one process, thus failing to satisfy a necessary condition. There is no second process to form a circle with the first one. For the same reason, it is not possible to have a deadlock involving only one resource. Question 7 (Chap 7; 20 Points) Describe a scenario showing how a system can enter an unsafe state, yet the processes can still complete their execution without entering a deadlock state.

A system is said to be in an unsafe state when the maximum needs (resources) of the processes cannot be met. Unsafe means that the operating system cannot guarantee that all processes will be able to acquire all of the resources they need without blocking, or engaging in hold and wait. An unsafe state does not imply that all, or even nearly all, of the resources are in use. An unsafe state also does not guarantee that a deadlock will happen, only that there is a possible sequence of resource allocations that will produce deadlock. Here is an example of an unsafe state in a system with 5 resources.

In use Maximum request Process #1 1 5 Process #2 1 5 This system is in an unsafe state, although only 2 of the 5 resources are in use. The danger is not in resources allocated, but in the number which might be requested in the future. Also, it is not necessarily any more dangerous to have all resources in use. If you can't use them, why have them? To demonstrate that unsafe does not imply deadlock, we must show that from an unsafe state, there is at least one scenario not leading to deadlock. Three possible scenarios are:
y y y

One or more processes complete without requesting more resources. The processes share contested resources. Release on refusal: if a process requests resources, but the operating system refuses to grant the request, then the process must give up the resources it already holds.

Question 8 (Chap 7; 20 Points) Can a system detect that some of its processes are starving? If you answer yes, explain how it can. If you answer no, explain how the system can deal with the starvation problem.

No. As stated it is really not very easy to answer yes to this question. A process starves if it does not get to run for "too long". If a given process has not run for 1 second, has it starved? How about if it has not run for 10 seconds? A minute? An hour? None of these indicate that a process is starving, since it may get to run in the next second, and however long it has waited may not be "too long" for that process. However, as the amount of time that a process has waited gets longer and longer, the probability that it is starving goes up. This argument depends on the fact that, as the question is stated, no numeric criteria have been supplied for declaring a process to have starved, or to have started starving. If, on the other hand, we declared as a basic design assumption that a process has "started to starve" if it has waited 10 seconds without running, then we might be able to answer "yes". The key point is that a definition of starvation that can be evaluated by the system in terms of process behavior has been made available. In either case, starvation can be dealt with by using aging. The longer a process waits, the higher its priority (or the closer it gets to the front of a queue). In this way, we can be sure that a process will eventually get to run. Note that this is not a perfect solution because, alas, we do not live in a perfect world. While this approach generally works, in theory it is possible for the demands on a system to so completely exceed its capacity, that even aging will not be enough to raise the priority of a given process to the point where it gets the CPU, because there really are processes with higher priority always in front of it. An important corollary to Murphy's law is:"The demands placed on the system by its environment can always exceed its design specifications". Note also, however, that this is not the fault of the software architect, since the architect can only

design a system with respect to its specified requirements. Every set of design requirements necessarily includes assumptions. It is the obligation of the designer to know what they are, and to know their implications. It is also the obligation of the software architect to make as few assumptions as possible, but it is impossible to avoid making any. So, if the behavior of the environment violates the set of design assumptions in some way, the software architect is not at fault. Question 9 (Chap 7; 30 Points) Consider the traffic deadlock depicted in the Figure 7.9 (pg. 269/309) a. Show that the four necessary conditions for deadlock indeed hold in this example. b. State a simple rule that will avoid deadlocks in this system.

a. <!--[endif]-->The four necessary conditions for deadlock hold in this example for the following reasons : <!--[if !supportLists]-->(i) <!--[endif]-->Mutual Exclusion : Each of the vehicles present in the streets hold a non-sharable resource: the part of the road they occupy, which they cannot share with the other vehicles. <!--[if !supportLists]-->(ii) <!--[endif]-->Hold and Wait : Each of the vehicles hold the space resource they occupy and are waiting the space in front of them to be freed by other waiting vehicles. <!--[if !supportLists]-->(iii) <!--[endif]-->No Preemption : There is no possibility of preemption as none of the vehicles can give up their resource. In this situation preemption would have to take the form of a vehicle pulling into a parking lot, or a crane reaching down and lifting a vehicle off the road. <!--[if !supportLists]-->(iv) <!--[endif]-->Circular Wait : Circular wait is present because each vehicle is waiting for the space in front of it, and some vehicles occupy spaces where two vehicles wait on them. It is thus possible to trace a cycle of waiting cars. This is the weakest assertion in the set, though, and is clearly untrue out at the edges somewhere, since some car can clearly move someplace in the city. If you have ever experienced grid-lock, though you know that this is small comfort, and that a rule to avoid even "local" deadlock is extremely desirable. b. The simple rule that could be used to avoid traffic deadlocks in such a system is that intersections should always remain clear as lights change. In this way, the resource of space in the intersection is freed for use at periodic intervals (light changes). This is, in fact, the law in most major cities, including New York City where I painfully remember sitting in traffic for at least an hour on a hot summer afternoon trying to get through a set of 3 lights because this rule had been broken. Question 10 (Chap 7; 35 Points)

Consider a computer system that runs 5,000 jobs per month with no deadlock prevention or deadlock avoidance scheme. Deadlocks occur about twice per month, and the operator must terminate and rerun about 10 jobs per deadlock. Each job is worth about $2 (in CPU time) , and the jobs terminated tend to be about halfdone when they are aborted. A systems programmer has estimated that a deadlockavoidance algorithm(like the Bankers algorithm ) could be installed in the system with an increase in the average execution time per job of about 10 percent. Since the machine currently has 30-percent idle time, all 5000 jobs per month could still be run, although turnaround time would increase by about 20 percent on an average. a. What are the arguments for installing the deadlock-avoidance algorithm? b. What are the arguments against installing the deadlock-avoidance algorithm?

a. The arguments in favor of installing the deadlock-avoidance algorithm are : <!--[if !supportLists]-->y <!--[endif]--> Avoids deadlocks and the costs of reruns <!--[if !supportLists]-->y <!--[endif]--> Avoids waste of resources as the reruns require duplicate use of resources and time to run the aborted processes all over again <!--[if !supportLists]-->y <!--[endif]--> Increases useful utilization of the resources as the systems idle-time is reduced b. The arguments against installing the deadlock avoidance algorithm are : <!--[if !supportLists]-->y <!--[endif]--> Increases total execution time for processes <!--[if !supportLists]-->y <!--[endif]--> Increases the total cost as the total execution time of processes increases by 10% <!--[if !supportLists]-->y <!--[endif]--> The processes take more time to complete due to an increase in the turnaround time by 20 % <!--[if !supportLists]-->y <!--[endif]--> It introduces an overhead on the execution of every process Deciding between the arguments in favor and against deadlock control requires an examination of the costs involved. In other words, it depends on the details of the situation. The problem states that the computing load represents about $10K per month in computing time, and that about 20 jobs having used $1 each in CPU time are terminated per month (this information is easily derived from the facts stated in the problem). This ignores the cost of fixing the deadlock, but the cost of the deadlock is clearly no less than $1 perjob. That means that not controlling deadlock costs about $20 per month. The problem also states that

controlling deadlock introduces overhead costing about 10% on every job, or about 10% * $2 = $1000 in CPU cost overhead. The proposal to control deadlock thus proposes to spend about $1000 (overhead) to save about $20 (deadlock loss). Do you think that is a good deal? (If so, please see meimmediately if you have have large amounts of money (unmarked cash only in small bills) to invest in my new scheme for supplying infinite cheap energy from my perpetual motion machine.... :-)) However, the situation is still fairly grey, and could go either way, since the deadlock avoidance overhead will use time that is currently idle. However, it still seems like a bad deal to me, since if business picked up, the system would not make as much money as it could at full capacity. The real answer is that it depends on exactly the set of metrics with which you choose to evaluate system performance, and the exact set of circumstances, which can also change. One can make the answer come out either way, really. An arguement in favor of paying the deadlock avoidance overhead is that situations chagne, and the cost of a deadlock might be much higher for some jobs.