22XX403 – OPERATING SYSTEM
UNIT II & PROCESS MANAGEMENT
PROCESS SYNCHRONIZATION AND CRITICAL SECTION
PROBLEM
2.1 INTRODUCTION OF PROCESS SYNCHRONIZATION
Process Synchronization is the coordination of execution of multiple processes in a
multi-process system to ensure that they access shared resources in a controlled and
predictable manner. It aims to resolve the problem of race conditions and other
synchronization issues in a concurrent system.
The main objective of process synchronization is to ensure that multiple processes
access shared resources without interfering with each other and to prevent the possibility of
inconsistent data due to concurrent access. To achieve this, various synchronization
techniques such as semaphores, monitors, and critical sections are used.
In a multi-process system, synchronization is necessary to ensure data consistency
and integrity, and to avoid the risk of deadlocks and other synchronization problems. Process
synchronization is an important aspect of modern operating systems, and it plays a crucial
role in ensuring the correct and efficient functioning of multi-process systems.
2.1.1. What is Process?
A process is a program that is currently running or a program under execution is called
a process. It includes the program’s code and all the activity it needs to perform its tasks,
such as using the CPU, memory, and other resources. Think of a process as a task that the
computer is working on, like opening a web browser or playing a video.
2.1.2. Types of Process
On the basis of synchronization, processes are categorized as one of the following
two types:
Independent Process: The execution of one process does not affect the execution of other
processes.
Cooperative Process: A process that can affect or be affected by other processes executing in
the system.
Process synchronization problem arises in the case of Cooperative processes also
because resources are shared in Cooperative processes. Consider an example, we used the
producer-consumer cooperating processes:
Producer Code
item nextProduced;
while( true ) {
/* Produce an item and store it in nextProduced */
nextProduced = makeNewItem( . . . );
/* Wait for space to become available */
while( ( ( in + 1 ) % BUFFER_SIZE ) == out );
/* Do nothing */
/* And then store the item and repeat the loop. */
buffer[ in ] = nextProduced;
in = ( in + 1 ) % BUFFER_SIZE;
}
Consumer Code
item nextConsumed;
while( true ) {
/* Wait for an item to become available */
while( in == out );
/* Do nothing */
/* Get the next available item */
nextConsumed = buffer[ out ];
out = ( out + 1 ) % BUFFER_SIZE;
/* Consume the item in nextConsumed
( Do something with it ) */
}
The only problem with the above code is that the maximum number of items which can
be placed into the buffer is BUFFER_SIZE - 1. One slot is unavailable because there always
has to be a gap between the producer and the consumer.
We could try to overcome this deficiency by introducing a counter variable, as shown
in the following code segments:
Unfortunately, we have now introduced a new problem, because both the producer and
the consumer are adjusting the value of the variable counter, which can lead to a condition
known as a race condition. In this condition a piece of code may or may not work correctly,
depending on which of two simultaneous processes executes first, and more importantly if one
of the processes gets interrupted such that the other process runs between important steps of
the first process. (Bank balance example discussed in class).
The particular problem above comes from the producer executing "counter++" at the
same time the consumer is executing "counter--". If one process gets part way through making
the update and then the other process butts in, the value of counter can get left in an incorrect
state.
But, you might say, "Each of those are single instructions - How can they get
interrupted halfway through?" The answer is that although they are single instructions in C++,
they are actually three steps each at the hardware level: (1) Fetch counter from memory into
a register, (2) increment or decrement the register, and (3) Store the new value of counter back
to memory. If the instructions from the two processes get interleaved, there could be serious
problems, such as illustrated by the following:
2.2 THE CRITICAL-SECTION PROBLEM
The producer-consumer problem described above is a specific example of a more
general situation known as the critical section problem. The general idea is that in a number of
cooperating processes, each has a critical section of code, with the following conditions and
terminologies:
Only one process in the group can be allowed to execute in their critical section at any
one time. If one process is already executing their critical section and another process
wishes to do so, then the second process must be made to wait until the first process
has completed their critical section work.
The code preceding the critical section, and which controls access to the critical section,
is termed the entry section. It acts like a carefully controlled locking door.
The code following the critical section is termed the exit section. It generally releases
the lock on someone else's door, or at least lets the world know that they are no longer
in their critical section.
The rest of the code not included in either the critical section or the entry or exit sections
is termed the remainder section.
General structure of a typical process Pi
A solution to the critical section problem must satisfy the following three conditions:
Mutual Exclusion
Only one process at a time can be executing in their critical section.
Progress
If no process is currently executing in their critical section, and one or more processes
want to execute their critical section, then only the processes not in their remainder sections
can participate in the decision, and the decision cannot be postponed indefinitely. (i.e. processes
cannot be blocked forever waiting to get into their critical sections).
Bounded Waiting
There exists a limit as to how many other processes can get into their critical sections
after a process requests entry into their critical section and before that request is granted. (i.e.
a process requesting entry into their critical section will get a turn eventually, and there is a
limit as to how many other processes get to go first).
We assume that all processes proceed at a non-zero speed, but no assumptions can be
made regarding the relative speed of one process versus another.
Kernel processes can also be subject to race conditions, which can be especially
problematic when updating commonly shared kernel data structures such as open file tables or
virtual memory management. Accordingly, kernels can take on one of two forms:
Non-preemptive kernels do not allow processes to be interrupted while in kernel mode.
This eliminates the possibility of kernel-mode race conditions, but requires kernel mode
operations to complete very quickly, and can be problematic for real-time systems, because
timing cannot be guaranteed.
Preemptive kernels allow for real-time operations, but must be carefully written to
avoid race conditions. This can be especially tricky on SMP systems, in which multiple kernel
processes may be running simultaneously on different processors.
Non-preemptive kernels include Windows XP, 2000, traditional UNIX, and Linux prior
to 2.6; Preemptive kernels include Linux 2.6 and later, and some commercial UNIXes such as
Solaris and IRIX.
2.2.1. Peterson's Solution
Peterson's Solution is a classic software-based solution to the critical section problem.
It is unfortunately not guaranteed to work on modern hardware, due to vagaries of load and
store operations, but it illustrates a number of important concepts.
Peterson's solution is based on two processes, P0 and P1, which alternate between their
critical sections and remainder sections. For convenience of discussion, "this" process is Pi,
and the "other" process is Pj. (i.e. j = 1 - i)
Peterson's solution requires two shared data items:
int turn - Indicates whose turn it is to enter into the critical section. If turn = = i, then
process i is allowed into their critical section.
boolean flag[ 2 ] - Indicates when a process wants to enter into their critical section.
When process i wants to enter their critical section, it sets flag[ i ] to true. In the
following diagram, the entry and exit sections are enclosed in boxes.
In the entry section, process i first raises a flag indicating a desire to enter the critical
section. Then turn is set to j to allow the other process to enter their critical section if process j
so desires.
The while loop is a busy loop (notice the semicolon at the end), which makes process i
wait as long as process j has the turn and wants to enter the critical section. Process i lowers
the flag[ i ] in the exit section, allowing process j to continue if it has been waiting.
The structure of process Pi in Peterson's solution
To prove that the solution is correct, we must examine the three conditions listed above:
Mutual exclusion
If one process is executing their critical section when the other wishes to do so, the
second process will become blocked by the flag of the first process. If both processes attempt
to enter at the same time, the last process to execute "turn = j" will be blocked.
Progress
Each process can only be blocked at the while if the other process wants to use the
critical section (flag[ j ] = = true), AND it is the other process's turn to use the critical section
( turn = = j ). If both of those conditions are true, then the other process ( j ) will be allowed to
enter the critical section, and upon exiting the critical section, will set flag[ j ] to false, releasing
process i. The shared variable turn assures that only one process at a time can be blocked, and
the flag variable allows one process to release the other when exiting their critical section.
Bounded Waiting
As each process enters their entry section, they set the turn variable to be the other
processes turn. Since no process ever sets it back to their own turn, this ensures that each
process will have to let the other process go first at most one time before it becomes their turn
again.
Note that the instruction "turn = j" is atomic, that is it is a single machine instruction
which cannot be interrupted.
2.3 SYNCHRONIZATION HARDWARE
To generalize the solution(s) expressed above, each process when entering their critical
section must set some sort of lock, to prevent other processes from entering their critical
sections simultaneously, and must release the lock when exiting their critical section, to allow
other processes to proceed. Obviously it must be possible to attain the lock only when no other
process has already set a lock. Specific implementations of this general procedure can get quite
complicated, and may include hardware solutions as outlined in this section.
One simple solution to the critical section problem is to simply prevent a process from
being interrupted while in their critical section, which is the approach taken by non preemptive
kernels. Unfortunately, this does not work well in multiprocessor environments, due to the
difficulties in disabling and the re-enabling interrupts on all processors. There is also a question
as to how this approach affects timing if the clock interrupt is disabled.
Another approach is for hardware to provide certain atomic operations. These
operations are guaranteed to operate as a single instruction, without interruption. One such
operation is the "Test and Set", which simultaneously sets a boolean lock variable and returns
its previous value, as shown in Figures
Illustration of "test_and_set( )" function
The above examples satisfy the mutual exclusion requirement, but unfortunately do not
guarantee bounded waiting. If there are multiple processes trying to get into their critical
sections, there is no guarantee of what order they will enter, and any one process could have
the bad luck to wait forever until they got their turn in the critical section. (Since there is no
guarantee as to the relative rates of the processes, a very fast process could theoretically release
the lock, whip through their remainder section, and re-lock the lock before a slower process
got a chance. As more and more processes are involved vying for the same resource, the odds
of a slow process getting locked out completely increase).
Figure illustrates a solution using test-and-set that does satisfy this requirement, using
two shared data structures, boolean lock and boolean waiting [ N ], where N is the number of
processes in contention for critical sections:
Bounded-waiting mutual exclusion with Test And Set ( ).
The key feature of the above algorithm is that a process blocks on the AND of the
critical section being locked and that this process is in the waiting state. When exiting a critical
section, the exiting process does not just unlock the critical section and let the other processes
have a free-for-all trying to get in. Rather it first looks in an orderly progression (starting with
the next process on the list) for a process that has been waiting, and if it finds one, then it
releases that particular process from its waiting state, without unlocking the critical section,
thereby allowing a specific process into the critical section while continuing to block all the
others. Only if there are no other processes currently waiting is the general lock removed,
allowing the next process to come along access to the critical section.
Unfortunately, hardware level locks are especially difficult to implement in multi-
processor architectures.
22XX403 – OPERATING SYSTEM
UNIT II & PROCESS MANAGEMENT
SEMAPHORES, CLASSICAL PROBLEMS OF
SYNCHRONIZATION, CONCURRENCY CONTROL
2.1 SEMAPHORES
A more robust alternative to simple mutexes is to use semaphores, which are integer
variables for which only two (atomic) operations are defined, the wait and signal operations,
as shown in the following figure.
Note that not only must the variable-changing steps (S-- and S++) be indivisible, it is
also necessary that for the wait operation when the test proves false that there be no
interruptions before S gets decremented. It IS okay, however, for the busy loop to be interrupted
when the test is true, which prevents the system from hanging forever.
2.1.1. Semaphore Usage
In practice, semaphores can take on one of two forms:
Binary semaphores can take on one of two values, 0 or 1. They can be used to solve
the critical section problem as described above, and can be used as mutexes on systems that
do not provide a separate mutex mechanism. The use of mutexes for this purpose is shown in
Figure (from the 8th edition) below.
Mutual-exclusion implementation with semaphores.
Counting semaphores can take on any integer value, and are usually used to count the
number remaining of some limited resource. The counter is initialized to the number of such
resources available in the system, and whenever the counting semaphore is greater than zero,
then a process can enter a critical section and use one of the resources. When the counter gets
to zero (or negative in some implementations), then the process blocks until another process
frees up a resource and increments the counting semaphore with a signal call. (The binary
semaphore can be seen as just a special case where the number of resources initially available
is just one).
Semaphores can also be used to synchronize certain operations between processes. For
example, suppose it is important that process P1 execute statement S1 before process P2
executes statement S2. First we create a semaphore named synch that is shared by the two
processes, and initialize it to zero.
Then in process P1 we insert the code:
S1;
signal(synch);
and in process P2 we insert the code:
wait(synch);
S2;
Because synch was initialized to 0, process P2 will block on the wait until after P1
executes the call to signal.
2.1.2. Semaphore Implementation
The big problem with semaphores as described above is the busy loop in the wait call,
which consumes CPU cycles without doing any useful work. This type of lock is known as a
spinlock, because the lock just sits there and spins while it waits. While this is generally a bad
thing, it does have the advantage of not invoking context switches, and so it is sometimes used
in multi-processing systems when the wait time is expected to be short - One thread spins on
one processor while another completes their critical section on another processor.
An alternative approach is to block a process when it is forced to wait for an available
semaphore, and swap it out of the CPU. In this implementation each semaphore needs to
maintain a list of processes that are blocked waiting for it, so that one of the processes can be
woken up and swapped back in when the semaphore becomes available. (Whether it gets
swapped back into the CPU immediately or whether it needs to hang out in the ready queue
for a while is a scheduling problem).
The new definition of a semaphore and the corresponding wait and signal operations
are shown as follows:
Note that in this implementation the value of the semaphore can actually become
negative, in which case its magnitude is the number of processes waiting for that semaphore.
This is a result of decrementing the counter before checking its value.
Key to the success of semaphores is that the wait and signal operations be atomic, that
is no other process can execute a wait or signal on the same semaphore at the same time.
(Other processes could be allowed to do other things, including working with other
semaphores, they just can't have access to this semaphore). On single processors this can be
implemented by disabling interrupts during the execution of wait and signal; Multiprocessor
systems have to use more complex methods, including the use of spinlocking.
2.1.3. Deadlocks and Starvation
One important problem that can arise when using semaphores to block processes
waiting for a limited resource is the problem of deadlocks, which occur when multiple
processes are blocked, each waiting for a resource that can only be freed by one of the other (
blocked) processes, as illustrated in the following example.
Another problem to consider is that of starvation, in which one or more processes gets
blocked forever, and never get a chance to take their turn in the critical section. For example,
in the semaphores above, we did not specify the algorithms for adding processes to the waiting
queue in the semaphore in the wait( ) call, or selecting one to be removed from the queue in
the signal( ) call. If the method chosen is a FIFO queue, then every process will eventually get
their turn, but if a LIFO queue is implemented instead, then the first process to start waiting
could starve.
2.2 CLASSIC PROBLEMS OF SYNCHRONIZATION
The following classic problems are used to test virtually every new proposed
synchronization algorithm.
2.2.1. The Bounded-Buffer Problem
This is a generalization of the producer-consumer problem wherein access is controlled
to a shared group of buffers of a limited size.
In this solution, the two counting semaphores "full" and "empty" keep track of the
current number of full and empty buffers respectively (and initialized to 0 and N respectively).
The binary semaphore mutex controls access to the critical section. The producer and consumer
processes are nearly identical - One can think of the producer as producing full buffers, and the
consumer producing empty buffers.
2.2.2. The Readers-Writers Problem
In the readers-writer’s problem there are some processes (termed readers) who only
read the shared data, and never change it, and there are other processes (termed writers) who
may change the data in addition to or instead of reading it. There is no limit to how many
readers can access the data simultaneously, but when a writer accesses the data, it needs
exclusive access.
There are several variations to the readers-writer’s problem, most centered around
relative priorities of readers versus writers.
The first readers-writer’s problem gives priority to readers. In this problem, if a reader
wants access to the data, and there is not already a writer accessing it, then access is granted
to the reader. A solution to this problem can lead to starvation of the writers, as there could
always be more readers coming along to access the data. (A steady stream of readers will jump
ahead of waiting writers as long as there is currently already another reader accessing the data,
because the writer is forced to wait until the data is idle, which may never happen if there are
enough readers).
The second readers-writer’s problem gives priority to the writers. In this problem,
when a writer wants access to the data it jumps to the head of the queue - All waiting readers
are blocked, and the writer gets access to the data as soon as it becomes available. In this
solution the readers may be starved by a steady stream of writers.
The following code is an example of the first readers-writer’s problem, and involves
an important counter and two binary semaphores:
readcount is used by the reader processes, to count the number of readers currently
accessing the data.
mutex is a semaphore used only by the readers for controlled access to readcount.
rw_mutex is a semaphore used to block and release the writers. The first reader to
access the data will set this lock and the last reader to exit will release it; The remaining
readers do not touch rw_mutex. (Eighth edition called this variable wrt).
Note that the first reader to come along will block on rw_mutex if there is currently a
writer accessing the data, and that all following readers will only block on mutex for their turn
to increment readcount.
Some hardware implementations provide specific reader-writer locks, which are
accessed using an argument specifying whether access is requested for reading or writing. The
use of reader-writer locks is beneficial for situation in which: (1) processes can be easily
identified as either readers or writers, and (2) there are significantly more readers than writers,
making the additional overhead of the reader-writer lock pay off in terms of increased
concurrency of the readers.
2.2.3. The Dining-Philosophers Problem
The dining philosopher’s problem is a classic synchronization problem involving the
allocation of limited resources amongst a group of processes in a deadlock-free and starvation-
free manner:
Consider five philosophers sitting around a table, in which there are five chopsticks
evenly distributed and an endless bowl of rice in the centre, as shown in the diagram below.
(There is exactly one chopstick between each pair of dining philosophers).
These philosophers spend their lives alternating between two activities: eating and
thinking.
When it is time for a philosopher to eat, it must first acquire two chopsticks - one from
their left and one from their right.
When a philosopher thinks, it puts down both chopsticks in their original locations.
The situation of the Dining Philosophers
One possible solution, as shown in the following code section, is to use a set of five
semaphores (chopsticks[ 5 ] ), and to have each hungry philosopher first wait on their left
chopstick ( chopsticks[ i ] ), and then wait on their right chopstick ( chopsticks[ ( i + 1 ) % 5
]).
But suppose that all five philosophers get hungry at the same time, and each starts by
picking up their left chopstick. They then look for their right chopstick, but because it is
unavailable, they wait for it, forever, and eventually all the philosophers starve due to the
resulting deadlock.
The structure of philosopher i
Some potential solutions to the problem include:
Only allow four philosophers to dine at the same time. (Limited simultaneous
processes.)
Allow philosophers to pick up chopsticks only when both are available, in a critical
section. (All or nothing allocation of critical resources).
Use an asymmetric solution, in which odd philosophers pick up their left chopstick first
and even philosophers pick up their right chopstick first. (Will this solution always
work? What if there are an even number of philosophers?).
Note carefully that a deadlock-free solution to the dining philosopher’s problem does
not necessarily guarantee a starvation-free one. (While some or even most of the philosophers
may be able to get on with their normal lives of eating and thinking, there may be one unlucky
soul who never seems to be able to get both chopsticks at the same time.
2.3 CONCURRENCY IN OPERATING SYSTEM
Concurrency in operating systems refers to the capability of an OS to handle more than
one task or process at the same time, thereby enhancing efficiency and responsiveness. It may
be supported by multi-threading or multi-processing whereby more than one process or
threads are executed simultaneously or in an interleaved fashion.
Thus, more than one program may run simultaneously on shared resources of the
system, such as CPU, memory, and so on. This helps optimize performance and reduce idle
times while improving the responsiveness of applications, generally in multitasking contexts.
Good concurrency handling is crucial for deadlock situations, race conditions, and usually
also for uninterrupted execution of tasks. It helps in techniques like coordinating the execution
of processes, memory allocation, and execution scheduling for maximizing throughput.
2.3.1. What is Concurrency in OS?
Concurrency in an operating system refers to the ability to execute multiple processes
or threads simultaneously, improving resource utilization and system efficiency. It allows
several tasks to be in progress at the same time, either by running on separate processors or
through context switching on a single processor. Concurrency is essential in modern OS
design to handle multitasking, increase system responsiveness, and optimize performance for
users and applications.
There are several motivations for allowing concurrent execution:
Physical resource Sharing: Multiuser environment since hardware resources are
limited.
Logical resource Sharing: Shared file (same piece of information).
Computation Speedup: Parallel execution.
Modularity: Divide system functions into separation processes.
2.3.2. Principles of Concurrency
Both interleaved and overlapped processes can be viewed as examples of concurrent
processes, they both present the same problems. The relative speed of execution cannot be
predicted. It depends on the following:
The activities of other processes
The way operating system handles interrupts
The scheduling policies of the operating system
2.3.3. Problems in Concurrency
Sharing global resources: Sharing of global resources safely is difficult. If two
processes both make use of a global variable and both perform read and write on that variable,
then the order in which various read and write are executed is critical.
Optimal allocation of resources: It is difficult for the operating system to manage the
allocation of resources optimally.
Locating programming errors: It is very difficult to locate a programming error
because reports are usually not reproducible.
Locking the channel: It may be inefficient for the operating system to simply lock the
channel and prevents its use by other processes.
2.3.4. Advantages of Concurrency
Running of multiple applications: It enable to run multiple applications at the same
time.
Better resource utilization: It enables that the resources that are unused by one
application can be used for other applications.
Better average response time: Without concurrency, each application has to be run to
completion before the next one can be run.
Better performance: It enables the better performance by the operating system. When
one application uses only the processor and another application uses only the disk drive then
the time to run both applications concurrently to completion will be shorter than the time to
run each application consecutively.
2.3.5. Drawbacks of Concurrency
It is required to protect multiple applications from one another.
It is required to coordinate multiple applications through additional mechanisms.
Additional performance overheads and complexities in operating systems are required
for switching among applications.
Sometimes running too many applications concurrently leads to severely degraded
performance.
2.3.6. Issues of Concurrency
Non-atomic: Operations that are non-atomic but interruptible by multiple processes
can cause problems.
Race conditions: A race condition occurs of the outcome depends on which of several
processes gets to a point first.
Blocking: Processes can block waiting for resources. A process could be blocked for
long period of time waiting for input from a terminal. If the process is required to periodically
update some data, this would be very undesirable.
Starvation: Starvation occurs when a process does not obtain service to progress.
Deadlock: Deadlock occurs when two processes are blocked and hence neither can
proceed to execute.