Os Unit 3 Part 1
Os Unit 3 Part 1
Race Condition
Definition: A situation where several processes access and manipulate the same data
concurrently and the outcome of the execution depends on the particular order in which the
access takes place, is called a race condition.
To guard against the race condition we need to ensure that only one process at a time
can be manipulating the variable counter. To make such a guarantee, we require that the
processes be synchronized in some way.
Synchronization Hardware
Software-based solutions such as Peterson’s does not guaranteed to work on modern
computer architectures. The following are several more solutions to the critical-section
10
problem using techniques ranging from hardware to software-based on the premise of
locking —that is, protecting critical regions through the use of locks.
Disable Interrupts
Critical section
Enable Interrupts
Remainder section
2. Compare _and_Swap
The compare and swap () instruction, in contrast to the test and set () instruction,
operates on three operands.
The operand value is set to new value only if the expression (*value == expected) is
true. Regardless, compare and swap () always returns the original value of the
variable value.
Definition of compare _and_swap
int compare _and_swap(int *value, int expected, int new_value)
{
int temp = *value;
if (*value == expected)
*value = new_value;
return temp;
}
Mutual-exclusion implementation with compare _and_swap
do
{
while (compare_and_swap(&lock, 0, 1) != 0)
; /* do nothing */
/* critical section */
lock = 0;
/* remainder section */
} while (true);
12
/* remainder section */
} while (true);
Semaphores
Hardware based solutions to critical section problems are complicated, so we use a
more robust software tool called Semaphore.
Definition: A semaphore S is an integer variable that, apart from initialization, is accessed
only through two standard atomic operations: wait () and signal ().
Definition of the wait () operation
Wait(S)
{
while (S <= 0)
; // busy wait
S--;
}
Definition of the signal () operation
Signal(S)
{
S++;
}
1. Semaphore Usage
Operating systems often distinguish between counting and binary semaphores.
a. Counting Semaphore
The value of a counting semaphore can range over an unrestricted domain.
Counting semaphores can be used to control access to a given resource consisting
of a finite number of instances. The semaphore is initialized to the number of
resources available.
Each process that wishes to use a resource performs a wait () operation on the
semaphore (thereby decrementing the count).
When a process releases a resource, it performs a signal () operation
(incrementing the count). When the count for the semaphore goes to 0, all
resources are being used. After that, processes that wish to use a resource will
block until the count becomes greater than 0.
b. Binary Semaphore
The value of a binary semaphore can range only between 0 and 1.
Example: Consider two concurrently running processes: P1 with a statement S1
and P2 with a statement S2. Suppose we require that S2 be executed only after S1
has completed. We can implement this scheme readily by letting P1 and P2 share
a common semaphore synch, initialized to 0.
In process P1, we insert the statements
S1;
signal (synch);
In process P2, we insert the statements
13
wait (synch);
S2;
Because synch is initialized to 0, P2 will execute S2 only after P1 has invoked signal
(synch), which is after statement S1 has been executed.
2. Semaphore Implementation
The previous definition of wait () and signal () has a problem of busy waiting which
wastes CPU cycles. To overcome this, we will modify the definition of wait () and signal ().
Wait ()
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.
Then control is transferred to the CPU scheduler, which selects another process to
execute.
Signal ()
A process that is blocked, waiting on a semaphore S, should be 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.
The process is then placed in the ready queue.
Defining a semaphore
typedef struct
{
int value;
struct process *list;
} semaphore;
Suppose that P0 executes wait(S) and then P1 executes wait (Q).When P0 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.
Another problem related to deadlocks is indefinite blocking or starvation, a situation
in which processes wait indefinitely within the semaphore. Indefinite blocking may occur if
we remove processes from the list associated with a semaphore in LIFO (last-in, first-out)
order.
16
The second readers –writer’s problem requires that, once a writer is ready, that writer
perform its write as soon as possible. In other words, if a writer is waiting to access the
object, no new readers may start reading. In this readers may starve.
Data structures:
semaphore rw mutex = 1;
semaphore mutex = 1;
int read count = 0;
The semaphores mutex and rw mutex are initialized to 1; read count is initialized to 0.
The semaphore rw mutex is common to both reader and writer processes. The mutex
semaphore is used to ensure mutual exclusion when the variable read count is updated. The
read count variable keeps track of how many processes are currently reading the object. The
semaphore rw mutex functions as a mutual exclusion semaphore for the writers. It is also
used by the first or last reader that enters or exits the critical section. It is not used by readers
who enter or exit while other readers are in their critical sections.
The structure of a writer process
do
{
wait (rw_mutex);
...
/* writing is performed */
...
signal(rw_mutex);
} while (true);
17
Reader–Writer Locks
The readers–writers problem and its solutions have been generalized to provide
reader–writer locks on some systems. Acquiring a reader–writer lock requires specifying the
mode of the lock: either read or write access. When a process wishes only to read shared
data, it requests the reader–writer lock in read mode. A process wishing to modify the shared
data must request the lock in write mode. Multiple processes are permitted to concurrently
acquire a reader–writer lock in read mode, but only one process may acquire the lock for
writing, as exclusive access is required for writers.
Solution
One simple solution is to represent each chopstick with a semaphore. A philosopher
tries to grab a chopstick by executing a wait () operation on that semaphore. She releases her
chopsticks by executing the signal () operation on the appropriate semaphores. Thus, the
shared data are
semaphore chopstick [5];
where all the elements of chopstick are initialized to 1.
The structure of Philosopher i:
do
{
wait (chopstick[i]);
wait (chopStick [(i + 1) % 5]);
18
// eat for a while
signal (chopstick[i] );
signal (chopstick[ (i + 1) % 5] );
Although this solution guarantees that no two neighbours are eating simultaneously, it
nevertheless must be rejected because it could create a deadlock. Suppose that all five
philosophers become hungry at the same time and each grabs her left chopstick. All the
elements of chopstick will now be equal to 0. When each philosopher tries to grab her right
chopstick, she will be delayed forever.
Several possible remedies to the deadlock problem are replaced by:
Allow at most four philosophers to be sitting simultaneously at the table.
Allow a philosopher to pick up her chopsticks only if both chopsticks are available (to
do this, she must pick them up in a critical section).
Use an asymmetric solution—that is, an odd-numbered philosopher picks up first her
left chopstick and then her right chopstick, whereas an even numbered philosopher
picks up her right chopstick and then her left chopstick.
Monitors
Although semaphores provide a convenient and effective mechanism for process
synchronization, using them incorrectly can result in errors such as following,
Suppose that a process interchanges the order in which the wait() and signal() operations
on the semaphore mutex are executed, resulting in the following execution:
signal (mutex);
...
critical section
...
wait (mutex);
In this situation, several processes may be executing in their critical sections
simultaneously, violating the mutual-exclusion requirement. This error may be discovered
only if several processes are simultaneously active in their critical sections.
Suppose that a process replaces signal (mutex) with wait (mutex). That is, it executes
wait (mutex);
...
critical section
...
wait (mutex);
In this case, a deadlock will occur.
Suppose that a process omits the wait (mutex), or the signal (mutex), or both. In this
case, either mutual exclusion is violated or a deadlock will occur.
19
To deal with such errors, researchers have developed one fundamental high-level
synchronization construct—the monitor type.
1. Monitor Usage
A monitor type is an ADT that includes a set of programmer defined operations that
are provided with mutual exclusion within the monitor. The monitor type also declares the
variables whose values define the state of an instance of that type, along with the bodies of
functions that operate on those variables.
Monitor monitorname
{ /* shared variable declarations */
function P1 ( . . . ) { . . .
}
function P2 ( . . . ) { . . .
}
.
.
.
function Pn ( . . . ) { . . .
}
initialization code ( . . . ) { . . .
}
}
The monitor construct ensures that only one process at a time is active within the
monitor. Consequently, the programmer does not need to code this synchronization constraint
explicitly.
Condition Variables: The monitor construct is not sufficiently powerful for modelling some
synchronization schemes. For this purpose, we need to define additional synchronization
mechanisms. These mechanisms are provided by the condition construct,
condition x, y;
The only operations that can be invoked on a condition variable are wait () and signal
(). The operation
x.wait ();
means that the process invoking this operation is suspended until another process invokes
x.signal ();
20
The x.signal () operation resumes exactly one suspended process. If no process is suspended,
then the signal () operation has no effect.
21
}
Procedure
Each procedure F will be replaced by,
wait (mutex);
...
body of F
...
if (next _count > 0)
signal (next);
else
signal (mutex);
Mutual exclusion within a monitor is ensured.
Condition Variables
For each condition x, we introduce a semaphore x _sem and an integer variable x _count,
both initialized to 0.
The operation x.wait () can now be implemented as,
x_count++;
if (next_count > 0)
signal (next);
else
signal (mutex);
wait (x_sem);
x_count--;
if (x_count > 0)
{
22
next_count++;
signal (x_sem);
wait (next);
next_count--;
}
23
void release()
{
busy = false;
x.signal();
}
initialization code()
{
busy = false;
}
}
Problems
The following problems can occur:
A process might access a resource without first gaining access permission to the
resource.
A process might never release a resource once it has been granted access to the
resource.
A process might attempt to release a resource that it never requested.
A process might request the same resource twice (without first releasing the resource).
One possible solution is to include the resource access operations within the
ResourceAllocator monitor.
24