Presentation is loading. Please wait.

Presentation is loading. Please wait.

COSC 3407: Operating Systems Lecture 7: Implementing Mutual Exclusion.

Similar presentations


Presentation on theme: "COSC 3407: Operating Systems Lecture 7: Implementing Mutual Exclusion."— Presentation transcript:

1 COSC 3407: Operating Systems Lecture 7: Implementing Mutual Exclusion

2 This lecture… u Hardware support for synchronization u Building higher-level synchronization programming abstractions on top of the hardware support. – Semaphores

3 The Big Picture u The abstraction of threads is good, but concurrent threads sharing state is still too complicated u Implementing a concurrent program directly with loads and stores would be tricky and error-prone. u So we’d like to provide a synchronization abstraction that hides/manages most of the complexity and puts the burden of coordinating multiple activities on the OS instead of the programmer – Give the programmer higher level operations, such as locks.

4 Ways of implementing locks u All require some level of hardware support. u Directly implement locks and context switches in hardware – Makes hardware slow! One has to be careful not to slow down the common case in order to speed up a special case. Concurrent Programs High level atomic operations (API) Low level atomic operations (hardware) Locks semaphores monitors send&receive Load/store interrupt disable test&set comp&swap

5 Disable interrupts (uniprocessor only) u Two ways for dispatcher to get control: – internal events – thread does something to relinquish the CPU – external events – interrupts cause dispatcher to take CPU away u On a uniprocessor, an operation will be atomic as long as a context switch does not occur in the middle of the operation. u Need to prevent both internal and external events. u Preventing internal events is easy (although virtual memory makes it a bit tricky). u Prevent external events by disabling interrupts, in effect, telling the hardware to delay handling of external events until after we’re done with the atomic operation.

6 A flawed, but very simple solution u Why not do the following: 1. Need to support synchronization operations in user-level code. Kernel can’t allow user code to get control with interrupts disabled (might never give CPU back!). 2. Real-time systems need to guarantee how long it takes to respond to interrupts, but critical sections can be arbitrarily long. Thus, one should leave interrupts off for shortest time possible. 3. Simple solution might work for locks, but wouldn’t work for more complex primitives, such as semaphores or condition variables. Lock::Acquire() { disable interrupts;} Lock::Release() { enable interrupts;}

7 Implementing locks by disabling interrupts u Key idea: maintain a lock variable and impose mutual exclusion only on the operations of testing and setting that variable. class Lock { int value = FREE; } Lock::Acquire() { Disable interrupts; if (value == BUSY) { Put on queue of threads waiting for lock Go to sleep // Enable interrupts? See comments in next slides } else { value = BUSY; } Enable interrupts; } Enable Position

8 Implementing locks by disabling interrupts u Why do we need to disable interrupts at all? u Otherwise, one thread could be trying to acquire the lock, and could get interrupted between checking and setting the lock value, so two threads could think that they both have the lock. Lock::Release() { Disable interrupts; If anyone on wait queue { Take a waiting thread off wait queue Put it at the front of the ready queue } else { value = FREE; } Enable interrupts; }

9 Implementing locks by disabling interrupts u By disabling interrupts, the check and set operations occur without any other thread having the chance to execute in the middle. u When does Acquire re-enable interrupts in going to sleep? u Before putting the thread on the wait queue? – Then Release can check the queue, and not wake the thread up. u After putting the thread on the wait queue, but before going to sleep? – Then Release puts the thread on the ready queue, but the thread still thinks it needs to go to sleep! – goes to sleep, missing the wakeup from Release, and still holds lock (deadlock!) u Want to put it after sleep(). But – how?

10 Implementing locks by disabling interrupts u To fix this, in Nachos, interrupts are disabled when you call Thread::Sleep; it is the responsibility of the next thread to run to re-enable interrupts. u When the sleeping thread wakes up, it returns from Thread::Sleep back to Acquire. Interrupts are still disabled, so turn on interrupts. Thread A Thread B Disable sleep Sleep return enable Disable sleep Sleep return enable switch Time........

11 Interrupt disable and enable pattern across context switches u An important point about structuring code: – If you look at the Nachos code you will see lots of comments about the assumptions made concerning when interrupts are disabled. u This is an example of where modifications to and assumptions about program state can’t be localized within a small body of code. u When that’s the case you have a very good chance that eventually your program will “acquire” bugs: as people modify the code they may forget or ignore the assumptions being made and end up invalidating the assumptions. u Can you think of other examples where this will be a concern? – What about acquiring and releasing locks in the presence of C++ exception exits out of a procedure?

12 Atomic read-modify-write instructions mylock.acquire(); a = b / 0; mylock.release() u Problems with this solution: – Can’t give lock implementation to users u On a multiprocessor, interrupt disable doesn’t provide atomicity. u It stops context switches from occurring on that CPU, but it doesn’t stop the other CPUs from entering the critical section. u One could provide support to disable interrupts on all CPUs, but that would be expensive: stopping everyone else, regardless of what each CPU is doing.

13 Atomic read-modify-write instructions u Instead, every modern processor architecture provides some kind of atomic read-modify-write instruction. u These instructions atomically read a value from memory into a register, and write a new value. u The hardware is responsible for implementing this correctly on both uniprocessors (not too hard) and multiprocessors (requires special hooks in the multiprocessor cache coherence strategy). u Unlike disabling interrupts, this can be used on both uniprocessors and multiprocessors.

14 Examples of read-modify-write instructions u test&set (most architectures) – read value, write 1 back to memory u exchange (x86) – swaps value between register and memory u compare&swap (68000) – read value, if value matches register, do exchange u load linked and conditional store (R4000, Alpha) – designed to fit better with load/store architecture (speculative computation). – Read value in one instruction, do some operations, when store occurs, check if value has been modified in the meantime. – If not, ok. – If it has changed, abort, and jump back to start.

15 Test-and-Set Instruction u The Test-and-Set instruction is executed atomically u Busy-waiting: thread consumes CPU cycles while it is waiting. Boolean TestAndSet(Boolean &target) { Boolean rv = target; target = true; return rv; } Initially: boolean lock = false; void acquire(lock) { while TestAndSet(lock) ; // while BUSY } void release(lock) { lock = false; } Thread T i : while(true) { acquire(lock) ; critical section release(lock); remainder section }

16 Problem: Busy-Waiting for Lock u Positives for this solution – Machine can receive interrupts – User code can use this lock – Works on a multiprocessor u Negatives – This is very inefficient because the busy-waiting thread will consume cycles waiting – Waiting thread may take cycles away from thread holding lock (no one wins!) – Priority Inversion: If busy-waiting thread has higher priority than thread holding lock  no progress! u Priority Inversion problem with original Martian rover u For semaphores and monitors, waiting thread may wait for an arbitrary length of time! – Thus even if busy-waiting was OK for locks, definitely not ok for other primitives

17 Test-and-Set (minimal busy waiting) u Idea: only busy-wait to atomically check lock value; if lock is busy, give up CPU. u Use a guard on the lock itself (multiple layers of critical sections!) u Waiter gives up the processor so that Release can go forward more quickly: Acquire(lock) { while test&set(guard) ; // Short busy-wait time if (value == BUSY) { Put on queue of threads waiting for lock Go to sleep & set guard to false } else { value = BUSY; guard = false; }

18 Test-and-Set (minimal busy waiting) u Notice that sleep has to be sure to reset the guard variable. Why can’t we do it just before or just after the sleep? Release(lock) { while (test&set(guard)) ; if anyone on wait queue { take a waiting thread off put it at the front of the ready queue } else { value = FREE; } guard = false; }

19 Mutual Exclusion with Swap u Swap instruction operates on the contents of two words and is executed atomically. u Atomically swap two variables. void Swap(boolean &a, boolean &b) { Boolean temp = a; a = b; b = temp; } u Shared data: Boolean lock = false; Boolean waiting[n]; u Thread T i do { key = true; while (key == true) Swap(lock,key); critical section lock = false; remainder section } while (1);

20 Higher-level Primitives than Locks u Goal of last couple of lectures: – What is the right abstraction for synchronizing threads that share memory? – Want as high a level primitive as possible u Good primitives and practices important! – Since execution is not entirely sequential, really hard to find bugs, since they happen rarely – UNIX is pretty stable now, but up until about mid-80s (10 years after started), systems running UNIX would crash every week or so – concurrency bugs u Synchronization is a way of coordinating multiple concurrent activities that are using share state – Next lecture presents a couple of ways of structuring the sharing

21 Semaphores u Synchronization primitive – higher level than locks – invented by Dijkstra in 1968, as part of the THE os – used in the original UNIX. u A semaphore is: – a non-negative integer value S, and – support two atomic operations wait/P() and signal/V() wait (S) { // also called P() while S  0 ; // no-op Spinlock S--; } signal(S) { // also called V() S++; }

22 Busy waiting problem u Busy waiting wastes CPU cycles. u Spinlocks are useful in multiprocessor systems. – no context switch is required when a process must wait on a lock. – Spinlocks are useful when held for short times u Each semaphore has an associated queue of processes/threads – wait(S): decrement S. If S = 0, then block until greater than zero – Signal(S): increment S by one and wake 1 waiting thread (if any) – Classic semaphores have no other operations

23 Hypothetical Implementation type semaphore = record value: integer: L: list of processes; end wait(S) { S.value--; if (S.value < 0) { add this process to S.L; block(); // a system call } signal(S) { S.value++; if (S.value <= 0) { remove a process P from S.L wakeup(P); // a system call } wait()/signal() are critical sections! Hence, they must be executed atomically with respect to each other. busy waiting is limited only to the critical sections of wait and signal operations, and these are short

24 Two Types of Semaphores u Binary semaphore: like a lock (has a Boolean value) – Initialized to 1 – A thread performs a wait() until value is 1 and then sets it to 0 – Signal() sets value to 1, waking up a waiting thread, if any u Counting semaphore: – represents a resource with many units available – allows threads/process to enter as long as more units are available – counter is initialized to N » N = number of units available

25 Semaphore as General Synchronization Tool u Execute B in P j only after A executed in P i u Use semaphore flag initialized to 0 u Code: P i P j   A wait(flag) signal(flag) B

26 Deadlock and Starvation u Deadlock – two or more processes are waiting indefinitely for an event (execution of a signal operation) that can be caused by only one of the waiting processes. u Let S and Q be two semaphores initialized to 1 P 0 P 1 wait(S);wait(Q); wait(Q);wait(S);  signal(S);signal(Q); signal(Q);signal(S); u Starvation – indefinite blocking. A process may never be removed from the semaphore queue in which it is suspended. u Indefinite blocking may occur if we add and remove processes from the list associated with a semaphore in LIFO order.

27 Two uses of semaphores u Mutual exclusion (initially S = 1) – Binary semaphores can be used for mutual exclusion u Process P i : do { wait(S); CriticalSection() signal(S); remainder section } while (1);

28 Two uses of semaphores u Scheduling constraints – Locks are fine for mutual exclusion, but what if you want a thread to wait for something? – For example, suppose you had to implement Thread::Join, which must wait for a thread to terminate. – By setting the initial value to 0 instead of 1, we can implement waiting on a semaphore: Initially S = 0 Fork Thread::Join calls wait() // will wait until something makes // the semaphore positive. Thread finish calls signal() // makes the semaphore positive // and wakes up the thread // waiting in Join.

29 Summary u Important concept: Atomic Operations – An operation that runs to completion or not at all – These are the primitives on which to construct various synchronization primitives u Talked about hardware atomicity primitives: – Disabling of Interrupts, test&set, swap, comp&swap, load-linked/store conditional u Showed several constructions of Locks – Must be very careful not to waste/tie up machine resources » Shouldn’t disable interrupts for long » Shouldn’t spin wait for long – Key idea: Separate lock variable, use hardware mechanisms to protect modifications of that variable u Talked about Semaphores


Download ppt "COSC 3407: Operating Systems Lecture 7: Implementing Mutual Exclusion."

Similar presentations


Ads by Google