CS162 Lecture Wednesday 02/05/2007 Jacob Porter I. Class logistics The next lectures will be the Nachos Tutorial, and Professor Smith will be out of town. The original time for the Nachos Tutorial has been cancelled. There will be two readers available at Copy Central on Hearst: 1. Nachos reader with source code and tutorial slides 2. A reader of technical papers to be available in a few days that contains the problem sets, lecture schedule, sample exams, an application to the ACM (Association for Computing Machinery) for students, and articles on teamwork, computer use and misuse, history, programming with threads, and linkers/loaders. Professor Smith was a member of ACM and IEEE when he was an undergraduate, and he highly recommends it. II. Last time the implementation of semaphores was discussed. Semaphores are a way to lock the critical section. III. Alternatives to semaphores (Section 6.4 in Operating System Concepts) A. use the swap() instruction to access the critical section. The swap instruction swaps the values of its two input arguments 1. implementation: void Swap(boolean *a, boolean *b) { boolean temp = *a; *a = *b; *b = temp; } 2. The statement swap(local(i), lock) allows us to access the critical section. local(i) and lock are boolean variables. local is an array of booleans so that each process has its own boolean value in local(i) a. the critical section is locked if lock is set to true. b. Lock is global and shared by all processes c. local(i) is a local variable for the ith process d. when the lock is free (false), swap sets local(i) to false and the critical section can be accessed, otherwise wait e. init: lock = false local(i) = true repeat swap(local(i), lock) until local(i) == false lock = false; B. use the TestAndSet() instruction. This is an atomic instruction that was implemented in some IBM computers. (http://en.wikipedia.org/wiki/Test-and-set) 1. TestAndSet() sets its second argument to true but returns the old value. The first argument gets the old value of the first argument. a. implementation of TestAndSet(): boolean TestAndSet(boolean *target, boolean *source) { *target = *source; *source = TRUE; return *target; } b. use a busy waiting loop to implement a lock init lock = false repeat TestAndSet(local(i), lock) until local(i) == FALSE; // critical section lock = false; c. read - modify - writes may be directly implemented in hardware. Has to be done in some central place so it's not possible for someone to sneak around the back. Hence, the command is atomic. C. Using TestAndSet() for mutual exclusion 1. here is how to implement P(S) with TestAndSet() disable interrupts local(i) = T Repeat (TestAndSet(local(i), S.Lock) until local(i) == false // spin If s>0, then { s = s - 1; S.Lock = false; enable interrupts; return;} Add process to S.Q S.Lock = false Enable interrupts Call dispatcher 2. Here is V(S) with TestAndSet(): Disable interrupts local(i) = TRUE Repeat (TestAndSet(local(i), S.Lock) until local(i) = false if (S.Q is empty) { s = s+1; } else { remove process from S.Q; wake it up} S.Lock = false Enable interrupts 3. Interrupts don't have to be disabled, but disabling them improves performance. An example will show why. Consider process A using P and V. There is an interrupt for process B. B runs. B spin locks indefinitely until process A is rescheduled and allowed to finish and reset the lock. Not allowing B to interrupt saves time. 4. The busy loop is not terrible since it is short (about 10 instructions for "spin-lock") 5. The trick with managing the critical section is to implement some mechanism once very carefully and use it every time. D. semaphore implementation is done IV. Deadlock (Chapter 7 in Operating System Concepts) A. first some discussion about resources 1. Two (2) kinds of resources: a. preemptive: can be taken away and given back later. examples: CPU, I/O, main memory b. non-preemptive: bad things happen if you take it away or preemption is very difficult examples: file space, terminal, printer, semaphores 2. two kinds of decisions about resources: a. allocation: who gets what and keeps what for non-preemptive resources. What order is efficient? b. scheduling: How long can a process keep a preemptive resource? B. deadlock: a situation where each of a set of processes is waiting for something from other processes in the set of processes. Since all are waiting, none can provide any of the things being waited for. (Chapter 7 Operating System Concepts, http://en.wikipedia.org/wiki/Deadlock) Normally a process goes through the following loop: a. request resource b. use resource c.release resource If all processes cannot gain access to any resources, then a deadlock occurs. 1. Five examples of deadlocks: a. traffic jam: each lane is waiting for another lane to clear = = = __ = = = = \/ = = = = __ = ===== ================ \/ ==== ||> ||> ||> ||> ||> __ ===== ================ \/ ==== = /\ = = __ = = "" = = \/ = = /\ = = __ = = "" = = \/ = ==== /\ ================ ==== "" <|| <|| <|| <|| <|| ==== /\ ================ ==== = "" = = = b. semaphores: there are two processes. One does P(X) followed by P(Y). The other does the reverse. The resources are semaphores X & Y This figure can explain it: P1 P2 P(X) P(Y) P(Y) P(X) c. there are 4 tape drives. each process needs 3, holds 2 and waits d. there are 2 or more processes, and each one has what the other has e. processes wait for a bit more of a resource (like memory) but the other processes hold the rest of the resource. Each process has to wait. 2. There are four conditions that all must exist for deadlock to occur. If any condition is not present, deadlock will not occur. These should be memorized. (Section 7.2.1 in Operating System Concepts) a. mutual exclusion: resources cannot be shared. If a process requests a resource that is being used, the requesting process must be delayed. b. hold and wait: A process must be waiting for new resources while holding resources c. no preemption: once allocated, can't be taken away. The resource can only be given up voluntarily by the process that has it d. a circularity in the graph of who holds what. 1. a graph that shows this relationship is called a hold-and-wait graph or a resource request graph 2. an example: process X waits on resource A, a resource that process Y is holding. Y waits on resource B, and Z holds resource B. Z waits on resource C while X holds resource C. Note the circularity of the graph. |X| ----> A ----> |Y| ----> B ---->|Z| ----> C-----| /\ | |-------------------------------------------------| An example of a deadlock can be described in the following figure. Deadlock Occurance Figure: Process 1 (P1) proceeds along the x-axis, and process 2 (P2) proceeds along the y-axis. Both processes need resources x1 and x2. In region A, P1 acquires x1 and holds it, and P2 acquires x2 and holds it. Each of the processes needs the resource that the other process holds, so there is a deadlock in region B. Processes cannot get to C and complete since there is a deadlock. P2 /\ ___________ | () | | C A and E: can go here. | () E | |________ B: deadlock forbidden x1 | | () | | C: can't go here. | | () | | | | () |______ B | | () | | x2| () A | | | () |____________| () ()===============================> P1 ----------- x1 ------------- x2 3. There are two approaches to dealing with deadlocks: prevent them (almost impossible), or cure them (check if deadlock, kill processes, try again). Cures are often not practical. There is a lot of theory behind deadlocks, but solutions are expensive. 4. Deadlock prevention. Prevent the existence of the four conditions above. Here are ways of doing it. (Section 7.4 in Operating System Concepts) 1. create enough resources for everyone (hard or impossible) 2. make all resources shareable (some resources not shareable like printer and tape drive) 3. don't permit mutual exclusion (not feasible since some resources can't be shared) 4. virtualize non-shared resources For example, the system can use spooling of the printer and card reader. 5. use only uniprogramming (only applicable in limited cases) 6. don't allow waiting and holding. The process crashes if resources are not available, and it requests all resources at once. This usually involves requesting too much of a resource just to be safe and requesting both input and output devices even if they are not used. This can lead to starvation. Sometimes requesting all of the resources is impossible. For example, if there are only two tape drives, and a process needs two input drives and two output drives the process would have to request four tape drives. An example of a process that doesn't allow waiting and holding is a phone company that doesn't allow a call to go through if it is dropped. 7. a process releases all current resources before requesting any new ones. However, some resources are not releasable / reacquireable without restarting the process. An example is modifying a database. 8. allow preemption, but some resources are not preemptive 9. make ordered or hierarchical requests. For example, ask for all of resource one (R1), then all of resource two (R2), and so on. All processes must follow the same ordering scheme, so that there won't be any circularity in the use of resources. Advanced knowledge of what is needed is necessary. There can't be a circular wait. There needs to be an arrow R1 -> R2. a. An system that did this is called witness, and it is implemented on BSD versions of UNIX. It signals a warning to the console if a mutex is aquired by a thread out of order. 5. deadlock avoidance with the banker's algorithm. First some definitions. (Section 7.5 in Operating System Concepts) a. A system is in a ***safe state*** if there exists a sequence of tasks such that all tasks complete. In other words, the system can allocate resources to each process and avoid deadlock. b. A ***safe allocation*** is an allocation of resources that leads to a safe state c. A ***safe sequence*** is a sequence of tasks that leads to a safe state. If a system is in a safe state, and a task completes along the safe sequence, then the system is still in a safe state. d. If the state is not safe, it is unsafe. It will lead to deadlocks. However, if a system is in an unsafe state, a task may release some resources temporarily and allow other tasks to complete. In this way, an unsafe state can be "rolled back" to search for a safe state. This is essentially what the Banker's Algorithm does. e. The ***Banker's Algorithm*** is a theoretical algorithm that computes a safe sequence. It was discovered by Edsger Dijkstra and was found in the process of developing the THE operating system in the mid 60's. THE is an acronym for Technische Hogeschool Eindhoven, which is Dutch for the Eindhoven University of Technology. The algorithm was so named because it solves a problem for making loan requests. Processes are borrowers, and money is the resource. Customers come in with partial loan requests, and the algorithm can be used to compute whether the bank can make the necessary loans so that customers can repay them. (Section 7.5.3 in Operating System Concepts, Dijkstra himself -- http://www.cs.utexas.edu/users/EWD/ewd06xx/EWD623.PDF ) 1. Every process j needs to know how much of a resource k it will need. Let this be Max(j, k). Let Alloc(j, k) be the amount of the resource k that is allocated to the process j. Let Need(j,k) be the amount of resource k that process j still needs. Finally, let Avail(k) be the amount of resource k still available. 2. The Banker's Algorithm is: K is a resource For each thread j (that uses k) { Max(j, k) Alloc(j,k) Need(j,k) Avail(k) If all other threads can finish with no additional resources, that is Need(j, k) <= Avail(k), then the state is safe. Otherwise, find a thread that satisfies the full need with what is currently available and repeat the above block of code from the beginning. If this can't be done, then the state is unsafe. } This algorithm runs in at most n^2 time where n is the number of processes. For each process, it checks if the remaining processes can be completed. If ever there is a sequence that can be completed, the algorithm returns that there is a safe state. The algorithm takes less than n^2 time when it can quickly find a safe state. If there are m resources, then the algorithm can add a for loop that checks each resource. This means that this more general algorithm takes time m * n^2 time in the worst case. The algorithm is correct because it checks through all possible allocations before returning unsafe. If a sequence is safe, then the algorithm returns that the state is safe. See Section 7.5.3.3 for an illustrative example.