Operating System Concepts chapter 6 CS 355 Operating Systems Dr. Matthew Wright Background • Concurrent access to shared data may result in data inconsistency • Maintaining data consistency requires mechanisms to ensure the orderly execution of cooperating processes • Suppose that we wanted to provide a solution to the consumer-producer problem that fills all the buffers. We can do so by having an integer count that keeps track of the number of full buffers. Initially, count is set to 0. It is incremented by the producer after it produces a new buffer and is decremented by the consumer after it consumes a buffer. /* producer code */ while (count = BUFFER_SIZE) ; // do nothing /* consumer code */ while (count = 0) ; // do nothing //add an item to the buffer buffer[in] = item; in = (in + 1) % BUFFER_SIZE; count++; //add an item to the buffer item = buffer[out]; out = (out + 1) % BUFFER_SIZE; count--; Implementation • count++ could be implemented as: register1 = count register1 = register1 + 1 count = register 1 • count-- could be implemented as: register2 = count register2 = register2 – 1 count = register 2 • Consider this executing interleaving with count = 5 initially: 1. producer executes register1 = count 2. producer executes register1 = register1 + 1 3. consumer executes register2 = count 4. consumer executes register2 = register2 – 1 5. producer executes count = register1 6. consumer executes count = register2 • This creates a race condition. (register1 = 5) (register1 = 6) (register2 = 5) (register2 = 4) (count = 6) (count = 4) Critical Section • Suppose we have n processes: P0, P1, …, Pn • Each process has a segment of code, called a critical section, in which the process changes shared memory • If one process is executing in its critical section, no other process ought to execute in its critical section • Critical-Section Problem: design a protocol to facilitate process communication and protect shared resources /* typical process • Standard design: 1. Entry section: a process must request structure */ while (true) { permission to enter critical section entry section 2. Process executes critical section 3. Exit section: process notifies others critical section that it has exited its critical section exit section 4. Remainder section: any code remainder section following the critical section } Requirements A solution to the critical-section problem must satisfy: 1. Mutual Exclusion: If process Pi is executing in its critical section, then no other processes can be executing in their critical sections. 2. Progress: If no process is executing in its critical section and there exist some processes that wish to enter their critical section, then the selection of the processes that will enter the critical section next cannot be postponed indefinitely. 3. Bounded Waiting: A bound must exist on the number of times that other processes are allowed to enter their critical sections after a process has made a request to enter its critical section and before that request is granted. • Assume that each process executes at a nonzero speed • No assumption concerning relative speed of the processes Peterson’s Solution • Works for two processes, denoted P0 and P1 • Requires that machine-language load and store operations are atomic—that is, they cannot be interrupted • The two processes share two data items: int turn; boolean flag[2]; • Variable turn indicates whose turn it is to enter the critical section. • Array flag indicates whether each process is ready to enter its critical section. If flag[i] = true, then Pi is ready. Peterson’s Solution while (true) { flag[i] = true; turn = j; while (flag[j] && turn == j) /* wait */ ; critical section flag[i] = false; remainder section } Examine the code to see that this satisfies the three requirements: 1. Mutual exclusion 2. Progress 3. Bounded Waiting Locks • We can prevent race conditions by protecting critical section with locks. • A process must acquire a lock before entering its critical section. • A process releases a lock when it exits its critical section. • Locks may be implemented in software or hardware. /* solution using locks */ while (true) { acquire lock critical section release lock remainder section } Hardware Solutions • Many systems provide hardware support for critical section code. • On a single-processor system, we could disable interrupts when a process executes its critical section. – Currently running code would execute without preemption – Generally too inefficient on multiprocessor systems – Operating systems using this not broadly scalable • Modern machines provide special atomic (non-interruptible) hardware instructions. For example: – Test memory word and set value – Swap contents of two memory words • Java HardwareData class presents an abstract view of atomic hardware instructions Java HardwareData /** /** * Sample data structure for hardware solution /** * Using HardwareData getAndSet() to implement a lock * * Using HardwareData * Gagne, Galvin, Silberschatzswap() to implement a lock * @author * Gagne, Galvin, Silberschatz * @author * Operating System Concepts with Galvin, Java, 8th Ed., Fig. 6.4 * @author Gagne, Silberschatz * Operating System Concepts with Java, 8th Ed., Fig. 6.5 */ * Operating System Concepts with Java, 8th Ed., Fig. 6.6 */ */ public class HardwareData //lock is shared by all threads { //locklock is shared by all threads HardwareData = new HardwareData(false); private boolean value = false; HardwareData lock = new HardwareData(false); (true) { publicwhile HardwareData(boolean value) { //each thread has a local copy of key while (lock.getAndSet(true)) this.value =HardwareData value; Thread.yield(); key = new HardwareData(true); } while (true) { */ /* critical section public boolean get() { key.set (true); return value; lock.set(false); } do { lock.swap(key); /* remainder section */ public void set(boolean newValue) { } } this.value = newValue; while (key.get() == true); } Each of these methods must be implemented atomically. /* critical section */ public boolean getAndSet(boolean newValue) { boolean oldValue = this.get(); lock.set(false); Semaphores • Simpler synchronization tool than the previous • A semaphore consists of: – an integer variable – atomic operations acquire() and release() • The value of a counting semaphore can be any integer. • The value of a binary semaphore (also called a mutex lock) is only 0 or 1. • A binary semaphore can control access to a critical section. /* definitions of acquire() and release() */ acquire() { while value <= 0 ; // wait value--; } release() { value++; /* }using a binary semaphore to control access to a critical section */ Semaphore sem = new Semaphore(1); sem.acquire(); /* critical section */ sem.release(); /* remainder section */ Multiple Java Threads using Semaphores /** * Worker thread to demonstrate use of a semaphore * /** * @author Gagne,* Galvin, create aSilberschatz semaphore and five threads * Operating System * Concepts with Java, 8th Ed., Fig. 6.8 */ * @author Gagne, Galvin, Silberschatz * Operating System Concepts with Java, 8th Ed., Fig. 6.8 public class Worker */ implements Runnable { private Semaphore public sem; class SemaphoreFactory { public Worker(Semaphore public static sem) { void main(String args[]) { this.sem = sem; Semaphore sem = new Semaphore(1); } Thread[] bees = new Thread[5]; public void run() { for (int i = 0; i < 5; i++) while (true) { bees[i] = new Thread(new Worker(sem)); sem.acquire(); criticalSection(); for (int i = 0; i < 5; i++) sem.release(); bees[i].start(); remainderSection(); } } } } } Semaphore Implementation • Disadvantage of previous implementation: busy waiting • Better idea: – When a process fails to acquire a semaphore, it places itself in a waiting queue associated with the semaphore. – When a process releases a semaphore, it restarts a process waiting for the semaphore. • This requires a semaphore to be an integer value and a list of processes. • Now value might be negative; if so, its magnitude is the number of waiting processes. /* definitions of acquire() and release() */ acquire() { value--; if (value <= 0) { add this process to list block(); } } release() { value++; if (value < 0) { remove a process P from list wakeup(P); } } Semaphore Implementation • Operating provides block() and wakeup(P) as basic system calls • The list may be implemented as any type of queue. • Important: acquire() and release() must be implemented atomically—this creates a critical section problem! • We have transferred the critical section problem from the application program to the Semaphore class, but it’s progress, since the new critical sections are short. • Possible solutions: – Disable interrupts during acquire() and release() – Use a hardware solution /* definitions of acquire() and release() */ acquire() { value--; if (value <= 0) { add this process to list block(); } } release() { value++; if (value < 0) { remove a process P from list wakeup(P); } } Possible Problem: Deadlock • A semaphore with a waiting queue can result in a situation where two processes are waiting for an event that can only be caused by the other waiting process. • This is called a deadlock. • Example: P0 P1 S.acquire(); Q.acquire(); Q.acquire(); S.acquire(); ⋮ ⋮ S.release(); Q.release(); Q.release(); S.release(); • Deadlocks are the subject of Chapter 7. • A related problem is indefinite blocking or starvation, in which a process waits indefinitely to acquire the semaphore. Possible Problem: Priority Inversion • A high-priority process might have to wait for a low-priority process to release a shared resource. • Example: – Three processes, L, M, and H, with priorities L < M < H – Process H is waiting for resource R, which is currently locked by process L – Process M becomes runnable and preempts process L – Now process M has caused process H to wait longer • This problem is called priority inversion. • Possible solutions: – Have only two priorities – Priority-inheritance protocol: all processes accessing resources needed by higher-priority processes inherit the higher priority until they release the resources Bounded Buffer Problem • Recall: BoundedBuffer helps solve the Producer-Consumer /* producers call this method */ Problem /* consumers call this method */ public insert(E item) { • We can usevoid semaphores to protect the shared memory items in empty.acquire(); public void remove(E the BoundedBuffer object item) { mutex.acquire(); E item; /** // add an item to the buffer * boundedbuffer[in] buffer full.acquire(); with semaphores = item; mutex.acquire(); * in = (in + 1) % BUFFER_SIZE; * @author Gagne, Galvin, Silberschatz // Concepts remove anwith itemJava, to the * Operating System 8thbuffer Ed., Fig. 6.9 mutex.release(); item = buffer[out]; */ full.release(); out = (out + 1) % BUFFER_SIZE; } import java.util.*; mutex.release(); full.release(); public class BoundedBuffer<E> implements Buffer<E> { return item; private private private private private static} final int BUFFER_SIZE = 5; E[] buffer; int in, out; Semaphore mutex; Semaphore empty; Readers and Writers Problem • Suppose several concurrent processes share access to a database. • Some processes only read the database (call them readers), others read and write (call them writers). • Readers-writers problem: We must ensure that writers have exclusive access to the shared database. • Two variations: – First readers-writers problem: no reader should be kept waiting unless a writer has already obtained permission to use the database (writers might starve) – Second readers-writers problem: once a writer is ready, it must be able to write as soon as possible (readers might starve) • The will examine a Java solution using semaphores. Readers and Writers Problem /** * Database class, with methods to coordinate reader and writer * access to the database * * @author Gagne, Galvin, Silberschatz * Operating System Concepts with Java, 8th Ed., Fig. 6.18 */ public class Database implements ReadWriteLock { private int readerCount; // number of active readers Semaphore mutex; // controls access to readerCount Semaphore db; // controls access to the database public Database() { readerCount = 0; mutex = new Semaphore(1); db = new Semaphore(1); } public void acquireReadLock(int readerNum) { mutex.acquire(); /* first reader indicates that the database is being read */ ++readerCount; Dining Philosophers Problem • Five philosophers sit around a circular table, with a single chopstick between each two philosophers. • Philosophers alternate between thinking and eating. • When a philosopher wants to eat, he first must pick up the two neighboring chopsticks, one after another. • When a philosopher finishes eating, he returns the two chopsticks to the table. • Problem: How can we prevent deadlock and starvation? Dining Philosophers Problem • Simple solution: represent each chopstick by a semaphore. • This does not solve the possibility of deadlock. /* shared data (chopsticks) */ Semaphore chopStick[] = new Semaphore[5]; for(int i=0; i < 5; i++) chopStick[i] = new Semaphore(1); • Possible solutions: – Allow at most four philosophers at the table – Allow a philosopher to pick up chopsticks only if both are available (must pick them up in a critical section) – Require odd philosophers to pick up left chopstick first, and even philosophers to pick up right chopstick first. /* the structure of philosopher i */ while (true) { // get left chopstick chopStick[i].acquire(); // get right chopstick chopStick[(i+1)%5].acquire(); eating(); // return left chopstick chopStick[i].release(); // return right chopstick chopStick[(i+1)%5].release(); thinking(); } Problems with Semaphores If a single process is not well-behaved, then synchronization may fail. For example: semaphore.acquire(); critical section semaphore.release(); Correct. semaphore.release(); critical section semaphore.acquire(); violates mutual-exclusion requirement (error might not always occur) semaphore.acquire(); critical section semaphore.acquire(); produces deadlock //semaphore.acquire(); critical section semaphore.release(); violates mutual-exclusion requirement Monitors • A monitor is an abstract type that controls access to a shared resource. • The shared resource is only available via methods provided by the monitor. • The monitor ensures that only one process at a time can access the shared resource. monitor monitor name { //shared variable declarations ... //methods initialization code (...) { ... } procedure P1 (...) { ... } procedure P2 (...) { ... } ... } Java Synchronization • Java allows methods to be declared synchronized. • Each Java object has an associated lock, which we have ignored until now, and which may be owned by a single thread. • A thread acquires the lock for an object by calling a synchronized method of that object. – If no other thread owns the lock, then the calling thread gains ownership of the lock and runs the synchronized method. – If some thread owns the lock, then the calling thread waits in the entry set for the lock. • When a thread exists a synchronized method, it releases the lock. Synchronized BoundedBuffer /** * Implementing a bounded buffer with Java synchronization * * @author Gagne, Galvin, Silberschatz * Operating System Concepts with Java, 8th Ed., Fig. 6.27 */ public class BoundedBuffer<E> { private static final int BUFFER_SIZE = 5; private int count; // number of items in the buffer private int in; // points to the next free position private int out; // points to the next full position private E[] buffer; Problem: Deadlock // constructor public BoundedBuffer() { ... } // Producers call this method public synchronized void insert(E item) { while (count == BUFFER_SIZE) Thread.yield(); If the Producer tries to insert an item into a full buffer, it will sleep in the insert method and the Consumer will never be able to gain the lock to consume an item. Java: Wait and Notify • Each Java object also has a wait set. • When a thread enters a synchronized method, it gains the lock for the object. • If the thread determines that it has to wait for something, it can call the wait() method. When this happens: – The thread releases the lock for the object. – The state of the thread is set to blocked. – The thread is placed in the wait set for the object. • The notify() method does the following: – Picks a thread T from the wait set – Moves T from the wait set to the entry set – Changes the state of T from blocked to runnable Synchronized BoundedBuffer /** * Implementing a bounded buffer with Java synchronization * * @author Gagne, Galvin, Silberschatz * Operating System Concepts with Java, 8th Ed., Fig. 6.27 */ public class BoundedBuffer<E> { private static final int BUFFER_SIZE = 5; private int count, in, out; private E[] buffer; // constructor public BoundedBuffer() { ... } // Producers call this method public synchronized void insert(E item) { while (count == BUFFER_SIZE) { try { wait(); } catch (InterruptedException e) {} } Multiple Notification • The notify() method selects an arbitrary thread from the wait set. • It is possible that the selected thread is not, in fact, waiting on the condition for which it is notified. • The notifyAll() method moves all threads from the wait set to the entry set. • notifyAll() is a more expensive operation than notify(), but it is necessary if you must wake one particular thread out of many that might be sleeping Java Synchronization Example Solving the Readers-Writers problem using Java synchronization… /** * Database class, with methods to coordinate reader and writer * access to the database using Java synchronization * * @author Gagne, Galvin, Silberschatz * Operating System Concepts with Java, 8th Ed., Fig. 6.33-6.35 */ public class Database implements ReadWriteLock { private int readerCount; // number of active readers private boolean dbWriting; // indicates whether databse is in use public Database() { readerCount = 0; dbWriting = false; } public synchronized void acquireReadLock() { while (dbWriting == true) { try { wait(); Java Block Synchronization • The amount of time between when a lock is acquired and when it released is called the scope of the lock. • A synchronized method could be too large of a scope. • Java allows blocks of code to be declared synchronized. • Example: Object lock = new Object(); ... public void someMethod() { nonCriticalSection(); synchronized(lock) { criticalSection(); } remainderSection(); } Java Synchronization Notes • A thread can nest synchronized method invocations for different objects in order to own multiple locks simulaneously. • wait(), notify(), and notifyAll() may only be called from synchronized methods or blocks. • wait() throws an InterrupedException if the interruption status of its thread is set—that is, if someone has called interrupt() for that thread Synchronization in Java 5 • ReentrantLock – like the synchronized statement, but with more features – Allows granting the lock to the longest-waiting thread • Counting semaphores – controls access to a resource that may be simultaneously accessed by some fixed number of threads – provides acquire() and release() methods • Condition variables – provide similar functionality to wait() and notify() – offer more flexibility for threads that must wait for specific conditions