Silberschatz Ch06 Process Synchronization
Silberschatz Ch06 Process Synchronization
Process Synchronization
6.2
6.1 Background
Background
Concurrent access of two or more processes 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 can potentially fill all of the locations in a buffer
An integer variable count is used to keep track of the number of filled locations. Initially, count is set to 0. It is incremented by the producer when it produces a new item and places it in a buffer location and is decremented by the consumer when it consumes an item from a buffer location The integer variables in and out are used as indexes into the buffer
Buffer
count
in
6.4
out
in = (in + 1) % BUFFER_SIZE;
count++; } // End while
item buffer[BUFFER_SIZE];
int in = 0; int out = 0; int count = 0;
nextConsumed =
count--; /*
buffer[out];
. . . } // End while
Operating System Concepts 7th Edition, Feb 8, 2005 6.5 Silberschatz, Galvin and Gagne 2005
Race Condition
Although both the producer and consumer code segments are correct when each runs sequentially, they may not function correctly when executed concurrently count++ could be implemented as register1 = count register1 = register1 + 1 count = register1 count-- could be implemented as register2 = count register2 = register2 - 1 count = register2
One execution may interleave the above statements in an arbitrary order, where count initially is 5: T0: T1: T2: T3: T4: T5: producer producer consumer consumer producer consumer executes executes executes executes executes executes register1 = count register1 = register1 + 1 register2 = count register2 = register2 - 1 count = register1 count = register2 {register1 = {register1 = {register2 = {register2 = {count = 6 } {count = 4} 5} 6} 5} 4}
This results in the incorrect state of counter == 4, indicating that 4 locations are full in the buffer, when, in fact, 5 locations are full
6.6
Several processes are able to access and manipulate the same data concurrently The outcome of the execution depends on the particular order in which the data access and manipulation takes place
need to guarantee that only one process at a time can manipulate the counter variable
6.7
Consider a system consisting of n processes in which each process has a segment of code called a critical section
In the critical section, the process may change a common variable, update a table, write to a file, etc. When one process is updating in its critical section, no other process can be allowed to execute in its critical section (i.e., no two processes may execute in their critical sections at the same time)
The critical section problem is to design a protocol that the processes can use to cooperate in manipulating common data
Entry section: Contains the code where each process requests permission to enter its critical section; this code can be run concurrently Critical section: Contains the code for data manipulation; this code is only allowed to run exclusively for one process at a time Remainder section: Contains any remaining code that can execute concurrently
6.9
critical section
remainder section
} // End while
6.10
Bounded Waiting - There exists a bound, or limit, 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
We assume that each process executes at a nonzero speed We make no assumption concerning relative speed of the N processes
6.11
Various kernel code processes that perform the following activities must be safeguarded from possible race conditions
Maintaining a common list of open files Updating structures for maintaining memory allocation Updating structures for maintaining process lists Updating structures used in interrupt handling
Two general approaches are used to handle critical sections in operating systems
6.12
Preemptive kernel
Allows a process to be preempted while it is running in kernel mode Must be carefully designed to ensure that shared kernel data are free from race conditions Is more suitable for real-time programming May be more responsive because it wont be waiting for a kernel mode process to relinquish the CPU Is implemented in Linux and Solaris Does not allow a process to be preempted while it is running in kernel mode Allows a process to run until the process does one of the following: exits kernel mode, blocks, or voluntarily gives up the CPU (i.e., terminates)
Non-preemptive kernel
6.13
Petersons Solution
Petersons solution is a classic software-based solution to the critical
section problem
It provides a good algorithm description of solving the critical section problem It illustrates some of the complexities involved in designing software that addresses the three solution requirements (mutual exclusion, progress, and bounded waiting) Unfortunately, the assembly language implementations in modern computer architectures will not guarantee that Petersons solution will work (i.e., LOAD and STORE are not atomic)
6.15
The solution is restricted to two processes that alternate execution between their critical sections and remainder sections It assumes that the LOAD and STORE instructions are atomic (they cannot be interrupted)
boolean flag[2];
It turn == i, then process Pi is allowed to execute in its critical section If flag[i] == true, then Pi is ready to enter its critical section
The variable turn indicates whose turn it is to enter the critical section
The flag array is used to indicate if a process is ready to enter its critical section
6.16
while (TRUE) { flag[i] = TRUE; // I am ready to enter my critical section turn = j; // But you can go first if you are ready
6.17
protected by locks
A process must acquire a lock before entering its critical section A process releases the lock when it exits its critical section while (TRUE)
{
// Acquire lock // Critical section // Release lock // Remainder section } // End while
Operating System Concepts 7th Edition, Feb 8, 2005 6.19 Silberschatz, Galvin and Gagne 2005
Synchronization Hardware
Many systems provide hardware support for critical section code Single processor could disable interrupts
Currently running code would execute without preemption Generally too inefficient on multiprocessor systems
Operating
= non-interruptible
Either test memory word and set value boolean TestAndSet (boolean *target); Or swap contents of two memory words void Swap (boolean *a, boolean *b);
6.20
TestAndSet Instruction
Definition:
boolean TestAndSet (boolean *target) { boolean originalValue = *target; *target = TRUE; return originalValue; } // End TestAndSet
The function always sets the lock parameter to TRUE, but
6.21
while (TRUE) { while ( TestAndSet (&lock )) ; // Critical section . . . lock = FALSE; // Remainder section . . . } // End while // do nothing
6.22
Swap Instruction
Definition:
6.23
Shared Boolean variable lock initialized to FALSE; Each process has a local Boolean variable key.
Solution: while (TRUE) { key = TRUE; while ( key == TRUE) Swap (&lock, &key ); // critical section
. . .
lock = FALSE; // . . . remainder section
} // End while
6.24
6.5 Semaphores
Semaphore
Another possible synchronization tool that is less complicated than TestAndSet() or Swap() is a semaphore
A semaphore S is an integer variable that, apart from initialization, is accessed only through two standard atomic operations: wait() and signal()
Originally these operations were called P() and V() wait(S) { while S <= 0 ; // do nothing
S--;
} // End wait
6.26
semaphores
Binary semaphores can be used to deal with the critical section problem for
multiple processes
while (TRUE) { wait(mutex); // Critical section signal(mutex); // Remainder section } // End while
Operating System Concepts 7th Edition, Feb 8, 2005 6.27 Silberschatz, Galvin and Gagne 2005
Processes wanting to use a resource will block until the semaphore value becomes greater than 0
6.28
Semaphore Implementation
The main disadvantage of the semaphore definition given here so far is that is requires busy waiting
While a process is in its critical section, any other process that tries to enter its critical section must loop continuously in the entry section code
This semaphore implementation is called a spinlock because the process spins while waiting for the lock
To overcome the need for busy waiting, we can modify the definition of the wait() and signal() semaphore operations
When a process executes the wait() operation and finds that the semaphore value is not positive, it must wait
However, rather than engaging in busy waiting, the process can block itself The block() operation places a process into a waiting queue associated with the semaphore, and the state of the process is switched to the waiting state
A process that is blocked, waiting on a semaphore, is restarted when some other process executes a signal() operation
The process is restarted by a wakeup() operation, which changes the process from the waiting state to the ready state, and places it in the ready queue
6.29 Silberschatz, Galvin and Gagne 2005
a semaphore as a structure in C typedef struct { int value; struct process *list; } semaphore;
Each semaphore has an integer value and a list of processes
operation adds it to the list of processes A signal() operation removes one process from the list of waiting processes and awakens that process
6.30
Implementation of wait() function: void wait (semaphore *S) { S->value--; if (S->value < 0) { add this process to waiting queue block(); } } // End wait
Implementation of signal() function: void signal (semaphore *S) { S->value++; if (S->value <= 0) { remove a process P from the waiting queue wakeup(P); } } // End signal
6.31
The block() operation suspends the process that invokes it The wakeup() operation resumes the execution of a blocked process P These two operations would be provided by the operating system as basic system calls
We must guarantee that no two processes can execute the wait() and signal() operations on the same semaphore at the same time In a single processor environment, we can solve this by simply inhibiting interrupts during the time the wait() and signal() operations are executing In a multi-processor environment, interrupts must be disabled on every processor
This can be a difficult task and can seriously diminish performance Consequently, alternative locking such as spinlocks are used instead
6.32
Process Deadlock
The implementation of a semaphore with a waiting queue may result in a situation where two or more processes are waiting indefinitely for an event that can be caused only by one of the waiting processes The event in question is the execution of a signal() operation This situation is called a deadlock To illustrate, let S and Q be two semaphores initialized to 1
P0
wait (S); wait (Q); . . signal (S); signal (Q);
P1
wait (Q); wait (S); . . signal (Q); signal (S);
P0 executes wait(S) and then P1 executes wait(Q) When P0 then executes wait(Q), it must wait until P1 executes signal(Q) Similarly, when P1 executes wait(S), it must wait until P0 executes
signal(S)
Since these signal() operations cannot be executed, P0 and P1 are deadlocked
6.33 Silberschatz, Galvin and Gagne 2005
Process Starvation
Another problem related to deadlocks is indefinite blocking, or
starvation
This is a situation in which processes wait indefinitely within the
semaphore
Starvation may occur if we add and remove processes from the list
6.34
6.36
Bounded-Buffer Problem
N buffers, each can hold one item Semaphore mutex initialized to the value 1 Semaphore full initialized to the value 0
6.37
The structure of the producer process while (true) { // produce an item wait (empty); wait (mutex);
The structure of the consumer process while (true) { wait (full); wait (mutex); // remove an item from buffer signal (mutex);
6.38
Readers-Writers Problem
A data set is shared among a number of concurrent processes
Readers only read the data set; they do not perform any updates Writers can both read and write.
one single writer can access the shared data at the same time.
Shared Data
Data set
Semaphore mutex initialized to 1. Semaphore wrt initialized to 1. Integer readcount initialized to 0.
6.39
The structure of a writer process while (true) { wait (wrt) ; // writing is performed
The structure of a reader process while (true) { wait (mutex) ; readcount ++ ; if (readercount == 1) wait (wrt) ; signal (mutex);
signal (wrt) ; } // reading is performed wait (mutex) ; readcount - - ; if (readcount == 0) signal (wrt) ; signal (mutex) ; }
6.40
Dining-Philosophers Problem
Shared data
6.41
signal (chopstick[ (i + 1) % 5] );
// think
}
Operating System Concepts 7th Edition, Feb 8, 2005 Silberschatz, Galvin and Gagne 2005
6.42
6.7 Monitors
Incorrect use; order of signal and wait are reversed; several processes are in the critical section at the same time; this violates the mutual exclusion requirement
Incorrect use; signal call replaced with wait call; deadlock will occur wait (mutex) // Critical section wait (mutex) // This should have correctly been signal(mutex) Incorrect use; omitting of wait (mutex) or signal (mutex) (or both); mutual exclusion is violated or deadlock will occur Normal section // Critical section Normal section
6.44
Monitors
A high-level abstraction that provides a convenient and effective mechanism for process synchronization Only one process may be active within the monitor at a time monitor monitor-name { // shared variable declarations
procedure P1 () { . }
procedure Pn () {} initialization code ( .) { } }
Operating System Concepts 7th Edition, Feb 8, 2005 Silberschatz, Galvin and Gagne 2005
6.45
6.46
Condition Variables
condition x, y;
x.wait () a process that invokes the operation is suspended. x.signal () resumes one of the processes (if any) that invoked x.wait ()
6.47
6.48
6.50
and putdown() in the following sequence: dp.pickup (i) EAT dp.putdown (i)
6.51
Variables semaphore mutex; // (initially = 1) semaphore next; // (initially = 0) int next-count = 0; Each procedure F will be replaced by wait(mutex); body of F; if (next-count > 0) signal(next) else signal(mutex);
6.52
Monitor Implementation
For each condition variable x, we have: semaphore x-sem; // (initially = 0) int x-count = 0;
The operation x.wait can be implemented as: x-count++; if (next-count > 0) signal(next); else signal(mutex); wait(x-sem); x-count--;
6.53
Monitor Implementation
The operation x.signal can be implemented as:
6.54
Synchronization Examples
Solaris Windows XP Linux Pthreads
6.56
Solaris Synchronization
Implements a variety of locks to support multitasking,
6.57
Windows XP Synchronization
Uses interrupt masks to protect access to global resources on
uniprocessor systems
Uses spinlocks on multiprocessor systems
and semaphores
Dispatcher objects may also provide events
6.58
Linux Synchronization
Linux:
Linux provides:
6.59
. . .
} }
6.60
Pthreads Synchronization
Pthreads API is OS-independent It provides:
6.61
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, NULL); int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex);
6.62
End of Chapter 6