Professional Documents
Culture Documents
COA Chapter 5
COA Chapter 5
1. Tutorial Points
2. Geek for Geeks
3. Wikipedia
4. Lecture notes of experts as posted on Internet
Inter-Process Communication
The term IPC stands for inter-process communication, but it refers not only to communication
but synchronization, as well. Some examples of processes needing to communicate include:
Basic information passing, as via signals and messages, does not ordinarily lead to problems, but
operating on shared resources and data certainly does.
• Independent process.
• Co-operating process.
An independent process is not affected by the execution of other processes while a co-operating
process can be affected by other executing processes. Though one can think that those processes,
which are running independently, will execute very efficiently but in practical, there are many
situations when co-operative nature can be utilised for increasing computational speed,
convenience and modularity. Inter process communication (IPC) is a mechanism which allows
processes to communicate each other and synchronize their actions. The communication between
these processes can be seen as a method of co-operation between them. Processes can
communicate with each other using these two ways:
1. Shared Memory
2. Message passing
The Figure 1 below shows a basic structure of communication between processes via shared
memory method and via message passing.
An operating system can implement both method of communication. First, we will discuss the
shared memory method of communication and then message passing. Communication between
processes using shared memory requires processes to share some variable and it completely
depends on how programmer will implement it. One way of communication using shared
memory can be imagined like this: Suppose process1 and process2 are executing simultaneously
and they share some resources or use some information from other process, process1 generate
information about certain computations or resources being used and keeps it as a record in shared
memory. When process2 need to use the shared information, it will check in the record stored in
shared memory and take note of the information generated by process1 and act accordingly.
Processes can use shared memory for extracting information as a record from other process as
well as for delivering any specific information to other process.
Let’s discuss an example of communication between processes using shared memory method.
Now, We will start our discussion for the communication between processes via message
passing. In this method, processes communicate with each other without using any kind of
shared memory. If two processes p1 and p2 want to communicate with each other, they proceed
as follow:
• Establish a communication link (if a link already exists, no need to establish it again.)
• Start exchanging messages using basic primitives.
We need at least two primitives:
– send(message, destinaion) or send(message)
– receive(message, host) or receive(message)
The message size can be of fixed size or of variable size. if it is of fixed size, it is easy for OS
designer but complicated for programmer and if it is of variable size then it is easy for
programmer but complicated for the OS designer. A standard message can have two parts:
header and body.
The header part is used for storing Message type, destination id, source id, message length and
control information. The control information contains information like what to do if runs out of
buffer space, sequence number, priority. Generally, message is sent using FIFO style.
Terms
We begin the discussion with a few important definitions:
Race Conditions
When a race condition exists, program correctness is likely to be violated. Consider two example
sections of code, called T1 and T2, which might be threads that operate on the shared variables a
and b.
// T1
a = a + 1;
b = b + 1;
// T2
b = b * 2;
a = a * 2;
If either T1 or T2 runs in isolation, the property a=b is preserved, as in the following examples:
// T1 then T2 (a=b=1 initially)
a = a + 1;
b = b + 1;
b = b * 2;
a = a * 2; // a=b=4
// T2 then T1 (a=b=1 initially)
b = b * 2;
a = a * 2;
a = a + 1;
b = b + 1; // a=b=4
However, suppose the code execution is somehow interleaved between T1 and T2. If initially
a=1 and b=1, the following interleaved execution could occur:
a = a + 1; // T1 (a=b=1)
b = b * 2; // T2
b = b + 1; // T1
a = a * 2; // T2 (a=4, b=3)
At the end of the interleaved execution, a=4 and b=3, so a=b is no longer true. We observe that
the value of the output depends on the order of execution, not just on the value of the input data.
It might be very rare that T1 and T2 ever execute in such a way, but when they do, the program
no longer executes correctly. This example involves integer variables, but can be applied in
principle to any shared data or object.
Critical Sections
In the previous example, the problem could have ben prevented if we could guarantee that, when
either T1 or T2 runs, it maintains exclusive access to the shared objects (a and b). This is the idea
behind the concept of a critical section. When a process (or thread) is executing in its own
critical section, no other process (or thread) may be execute in its own same critical section.
critical sections are a mechanism by which mutual exclusion can be enforced.
Mutual Exclusion
There are a number of requirements for mutual exclusion to be successful:
• No two processes may be simultaneously inside their critical sections, and a process
remains inside its critical section for a finite time
• A process that halts must do so without interfering with other processes
• A process must not be denied access to a critical resource when there is no other process
using it
• No assumptions are made about relative process speeds or the number of processes or
CPUs
This is possibly the simplest approach. Disable all interrupts before a process/thread enters its
critical section, then re-enable them after the process/thread exits its critical section. On the good
side, this approach does actually enforce mutual exclusion. The running process can't be
interrupted in its critical section, even by the OS!
while (true)
{
/* disable interrupts */
/* critical section */
/* enable interrupts */
/* remainder */
}
However, there are two important limitations:
Besides disabling interrupts, there are other hardware-based mutual exclusion strategies. In each
case, there are one or more hardware instructions that, when properly used, can implement
mutual exclusion correctly.
The Exchange method requires a hardware instruction that can perform a swap operation
atomically (indivisibly). A typical assembly representation of the machine instruction follows.
The special instruction is XCHG.
enter_critical:
MOV REGISTER, 1
XCHG REGISTER, LOCK ; swap contents of REGISTER and LOCK
CMP REGISTER, 0 ; was LOCK 0, before the swap?
JNE enter_critical ; no: busy loop (LOCK was 1)
RET ; enter critical_section
leave_critical:
MOV LOCK, 0 ; open the lock
RET ; leave critical section
int key = 1;
while (true) {
do exchange(&key, &lock) while (key != 0);
/* critical section */
lock = 0;
/* remainder */
}
As long as the lock is locked (i.e., the lock variable holds a '1'), then the action of swapping has
no effect -- it continually swaps a '1' with another '1'. Only when the lock variable is '0' does the
swap have an effect -- the lock is quickly reset to '1', and the calling function gets returned a '0'
value, indicating it has successfully gained access. The calling process knows it is the only one
gaining access, because the exchange is atomic. This is generally done by briefly locking the
memory bus in hardware. The Intel x86/x64 architecture implements a version of Exchange. It is
described in the architectural manual as follows:
There are other similar instructions. One mentioned in the text, TSL (Test and Set Lock),
functions almost exactly the same as the Exchange example.
Hardware-based solutions can implement mutual exclusion correctly, but they rely on busy
waiting. They also fail if processes do not cooperate, and both deadlock and starvation are still
possible.
Lock Variables
In this approach, there is a shared 'lock' variable. If its value is 0, no process is in its critical
region. If the value is 1, a process is in its critical region and all others must wait. A process
needing to enter its critical section repeatedly checks the value, while the value is 1. As soon as
the lock value is 0 again, the process enters its critical section and sets the lock value to 1.
while (true) {
while (lock == 1)
/* busy wait: do nothing until lock==0 */ ;
The problem with this approach is that it relies on a race condition. If two processes read the lock
value as 0 and both proceed into their critical sections before the lock value can be set to 1, then
mutual exclusion is not enforced successfully.
Strict Alternation
The integer variable turn keeps track of whose turn it is to enter the critical section. Each
process will busy wait until its own turn.
while (true) {
while (turn != 0)
/* busy wait */ ;
/* critical section */
turn = 1;
/* remainder */
}
while (true) {
while (turn != 1)
/* busy wait */ ;
/* critical section */
turn = 0;
/* remainder */
}
The problem with this approach is that, if one process is significantly faster, the slower process is
repeatedly blocked for long periods of time. Also, it becomes complex to scale beyond two
processes. The approach provides mutual exclusion, but is very inefficient.
Peterson's Solution
In 1981, G.L. Peterson devised a software-based solution to mutual exclusion that does not
require strict alternation. There are a few variations of how to code Peterson's Solution; the one
shown is from the Tanenbaum text.
There are two function calls, one each for entering and leaving a process' criticial region. Each
process must call enter_region with its own process number as an argument. This will cuase it
to wait, if necessary, until it is safe to enter. After accessing the shared variables, it calls
leave_region to exit the critical region.
#define FALSE 0
#define TRUE 1
#define N 2
If both processes call enter_region simultaneously, they will each try to modify the variable
turn. The value of turn is overwritten by the second process, but by doing so it causes the
second process to hang on the while loop and busy wait, until the earlier processes finishes and
leaves its criticial region.
Peterson's solution works correctly if implemented properly, allowing mutual exclusion by two
cooperating processes. It can also be scaled to work with more than two processes. However,
Peterson's solution employs busy waiting, an inefficient use of the CPU.
Mutex vs Semaphore
What are the differences between Mutex vs Semaphore? When to use mutex and when to use
semaphore?
As per operating system terminology, mutex and semaphore are kernel resources that provide
synchronization services (also called as synchronization primitives). Why do we need such
synchronization primitives? Won’t be only one sufficient? To answer these questions, we need to
understand few keywords. Please read the posts on atomicity and critical section. We will
illustrate with examples to understand these concepts well, rather than following usual OS
textual description.
Note that the content is generalized explanation. Practical details vary with implementation.
Consider the standard producer-consumer problem. Assume, we have a buffer of 4096 byte
length. A producer thread collects the data and writes it to the buffer. A consumer thread
processes the collected data from the buffer. Objective is, both the threads should not run at the
same time.
Using Mutex:
A mutex provides mutual exclusion, either producer or consumer can have the key (mutex) and
proceed with their work. As long as the buffer is filled by producer, the consumer needs to wait,
and vice versa.
At any point of time, only one thread can work with the entire buffer. The concept can be
generalized using semaphore.
Using Semaphore:
A semaphore is a generalized mutex. In lieu of single buffer, we can split the 4 KB buffer into
four 1 KB buffers (identical resources). A semaphore can be associated with these four buffers.
The consumer and producer can work on different buffers at the same time.
Misconception:
There is an ambiguity between binary semaphore and mutex. We might have come across that a
mutex is binary semaphore. But they are not! The purpose of mutex and semaphore are different.
May be, due to similarity in their implementation a mutex would be referred as binary
semaphore.
Strictly speaking, a mutex is locking mechanism used to synchronize access to a resource. Only
one task (can be a thread or process based on OS abstraction) can acquire the mutex. It means
there is ownership associated with mutex, and only the owner can release the lock (mutex).
Semaphore is signaling mechanism (“I am done, you can carry on” kind of signal). For
example, if you are listening songs (assume it as one task) on your mobile and at the same time
your friend calls you, an interrupt is triggered upon which an interrupt service routine (ISR)
signals the call processing task to wakeup.
These problems are used for testing nearly every newly proposed synchronization scheme. The
following problems of synchronization are considered as classical problems:
These are summarized, for detailed explanation, you can view the linked articles for each.
1. Bounded-buffer (or Producer-Consumer) Problem:
Bounded Buffer problem is also called producer consumer problem. This problem is generalized
in terms of the Producer-Consumer problem. Solution to this problem is, creating two counting
se aphores full a d e pty to keep tra k of the urre t u er of full and empty buffers
respectively. Producers produce a product and consumers consume the product, but both use of
one of the containers each time.
2. Dining-Philosphers Problem:
The Dining Philosopher Problem states that K philosophers seated around a circular table with
one chopstick between each pair of philosophers. There is one chopstick between each
philosopher. A philosopher may eat if he can pickup the two chopsticks adjacent to him. One
chopstick may be picked up by any one of its adjacent followers but not both. This problem
involves the allocation of limited resources to a group of processes in a deadlock-free and
starvation-free manner.
Monitor vs Semaphore
Both semaphores and monitors are used to solve the critical section problem (as they allow
processes to access the shared resources in mutual exclusion) and to achieve process
synchronization in the multiprocessing environment.
Monitor:
A Monitor type high-level synchronization construct. It is an abstract data type. The Monitor
type contains shared variables and the set of procedures that operate on the shared variable.
When any process wishes to access the shared variables in the monitor, it needs to access it
through the procedures. These processes line up in a queue and are only provided access when
the previous process release the shared variables. Only one process can be active in a monitor at
a time. Monitor has condition variables.
Syntax:
monitor {
Semaphore:
A Semaphore is a lower-level object. A semaphore is a non-negative integer variable. The value
of Semaphore indicates the number of shared resources available in the system. The value of
semaphore can be modified only by two functions, namely wait() and signal() operations (apart
from the initialization).
When any process accesses the shared resources, it performs the wait() operation on the
semaphore and when the process releases the shared resources, it performs the signal() operation
on the semaphore. Semaphore does not have condition variables. When a process is modifying
the value of the semaphore, no other process can simultaneously modify the value of the
semaphore.
1. Binary semaphore
2. Counting semaphore
Syntax:
// Wait Operation
wait(Semaphore S) {
while (S<=0);
S--;
}
// Signal Operation
signal(Semaphore S) {
S++;
}
Advantages of Monitors:
Advantages of Semaphores:
• Semaphores are machine independent (because they are implemented in the kernel services).
• Semaphores permit more than one thread to access the critical section, unlike monitors.
• In semaphores there is no spinning, hence no waste of resources due to no busy waiting.
What is Semaphore?
Semaphore is simply a variable that is non-negative and shared between threads. A semaphore
is a signaling mechanism, and a thread that is waiting on a semaphore can be signaled by another
thread. It uses two atomic operations, 1)wait, and 2) signal for the process synchronization.
A semaphore either allows or disallows access to the resource, which depends on how it is set
up.
Characteristic of Semaphore
Here, are characteristic of a semaphore:
Types of Semaphores
The two common kinds of semaphores are
• Counting semaphores
• Binary semaphores.
Counting Semaphores
This type of Semaphore uses a count that helps task to be acquired or released numerous times. If
the initial count = 0, the counting semaphore should be created in the unavailable state.
However, If the count is > 0, the semaphore is created in the available state, and the number of
tokens it has equals to its count.
Binary Semaphores
The binary semaphores are quite similar to counting semaphores, but their value is restricted to 0
and 1. In this type of semaphore, the wait operation works only if semaphore = 1, and the signal
operation succeeds when semaphore= 0. It is easy to implement than counting semaphores.
Example of Semaphore
The below-given program is a step by step implementation, which involves usage and
declaration of semaphore.
After the semaphore value is decreased, which becomes negative, the command is held up until
the required conditions are satisfied.
Copy CodeP(S)
{
while (S<=0);
S--;
}
Signal operation
This type of Semaphore operation is used to control the exit of a task from a critical section. It
helps to increase the value of the argument by 1, which is denoted as V(S).
Copy CodeP(S)
{
while (S>=0);
S++;
}
Advantages of Semaphores
Here, are pros/benefits of using Semaphore:
Disadvantage of semaphores
Here, are cons/drawback of semaphore