Interprocess Communication Mechanisms: UNIT-3 Cooperating Processes
Interprocess Communication Mechanisms: UNIT-3 Cooperating Processes
Interprocess Communication Mechanisms: UNIT-3 Cooperating Processes
Pipes
Definition: A pipe acts as a conduit or channel allowing two processes to communicate.
Pipes are the oldest form of IPC
Common types of pipes
1
Two common types of pipes used on both UNIX and Windows systems:
Ordinary pipes or Pipes
Named pipes or FIFOs
What happens after the fork depends on which direction of data flow we want.
a) For a pipe from the parent to the child, the parent closes the read end of the pipe (fd[0]),
and the child closes the write end (fd[1]).
2
b) For a pipe from the child to the parent, the parent closes fd[1], and the child closes
fd[0].
3
a) Duplication of Output stream
b) Client server communication
Using FIFOs to Duplicate Output Streams
FIFOs can be used to duplicate an output stream and can be used for nonlinear
connections.
With a FIFO and the UNIX program tee(1), we can accomplish this procedure without
using a temporary file. (The tee program copies its standard input to both its standard output
and the file named on its command line.)
mkfifo fifo1
prog3 < fifo1 &
prog1 < infile | tee fifo1 | prog2
We create the FIFO and then start prog3 in the background, reading from the FIFO. We
then start prog1 and use tee to send its input to both the FIFO and prog2.
The problem in using FIFOs for this type of client–server communication is how to
send replies back from the server to each client. A single FIFO can’t be used, as the clients
would never know when to read their response versus responses for other clients. One
4
solution is for each client to send its process ID with the request. The server then creates a
unique FIFO for each client, using a pathname based on the client’s process ID.
Message Queues
A message queue is a linked list of messages stored within the kernel and identified by a
message queue identifier.
Each queue has the following msqid_ds structure associated with it:
struct msqid_ds
{
struct ipc_perm msg_perm; /* defines permissions */
msgqnum_t msg_qnum; /* # of messages on queue */
msglen_t msg_qbytes; /* max # of bytes on queue */
pid_t msg_lspid; /* pid of last msgsnd() */
pid_t msg_lrpid; /* pid of last msgrcv() */
time_t msg_stime; /* last-msgsnd() time */
time_t msg_rtime; /* last-msgrcv() time */
time_t msg_ctime; /* last-change time */
.
.
.
5
};
This structure defines the current status of the queue.
msgget
The first function normally called is msgget to either open an existing queue or create
a new queue.
General Form: int msgget(key_t key, int flag);
Key is converted into identifier of the process and used to decide whether a new
queue is created or an existing queue is referenced.
When a new queue is created, the following members of the msqid_ds structure are
initialized.
The mode member of this structure is set to the corresponding permission bits of flag.
msg_qnum, msg_lspid, msg_lrpid, msg_stime, and msg_rtime are all set to 0.
msg_ctime is set to the current time.
msg_qbytes is set to the system limit.
On success, msgget returns the non-negative queue ID. This value is then used with
the other three message queue functions.
msgsnd
Data is placed onto a message queue by calling msgsnd.
General Form: int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
Each message is composed of a positive long integer type field, a non-negative length
(nbytes), and the actual data bytes (corresponding to the length). Messages are always placed
at the end of the queue.
The ptr argument points to a long integer that contains the positive integer message
type, and it is immediately followed by the message data.
Message structure:
struct mymesg
{
long mtype; /* positive message type */
char mtext[512]; /* message data, of length nbytes */
};
The ptr argument is then a pointer to a mymesg structure. The message type can be
used by the receiver to fetch messages in an order other than first in, first out.
A flag value of IPC_NOWAIT can be specified, this causes msgsnd to return an error
message when the queue is full.
When msgsnd returns successfully, the msqid_ds structure associated with the
message queue is updated to indicate the process ID that made the call (msg_lspid), the time
that the call was made (msg_stime), and that one more message is on the queue (msg_qnum).
msgrcv
Messages are retrieved from a queue by msgrcv.
General Form: ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
6
the ptr argument points to a long integer (where the message type of the returned
message is stored) followed by a data buffer for the actual message data. nbytes specifies the
size of the data buffer.
If the returned message is larger than nbytes and the MSG_NOERROR bit in flag is
set, the message is truncated.
The type argument lets us specify which message we want.
type == 0 The first message on the queue is returned.
type >0 The first message on the queue whose message type equals type is
returned.
type <0 The first message on the queue whose message type is the lowest value
less than or equal to the absolute value of type is returned.
type= nonzero Used to read the messages in an order other than first in, first out such
as using priority
When msgrcv succeeds, the kernel updates the msqid_ds structure associated with the
message queue to indicate the caller’s process ID (msg_lrpid), the time of the call
(msg_rtime), and that one less message is on the queue (msg_qnum).
msgctl
The msgctl function performs various operations on a queue.
General Form: int msgctl(int msqid, int cmd, struct msqid_ds *buf );
The cmd argument specifies the command to be performed on the queue specified by msqid.
IPC_STAT Fetch the msqid_ds structure for this queue, storing it in the structure
pointed to by buf.
IPC_SET Copy the following fields from the structure pointed to by buf to the
msqid_ds structure associated with this queue: msg_perm.uid,
msg_perm.gid, msg_perm.mode, and msg_qbytes.
IPC_RMID Remove the message queue from the system and any data still on the
queue. This removal is immediate.
Shared memory
Shared memory allows two or more processes to share a given region of memory.
This is the fastest form of IPC, because the data does not need to be copied between the
client and the server.
The only trick in using shared memory is synchronizing access to a given region among
multiple processes. If the server is placing data into a shared memory region, the client
shouldn’t try to access the data until the server is done. Often, semaphores are used to
synchronize shared memory access.
7
The kernel maintains a structure with at least the following members for each shared
memory segment:
struct shmid_ds
{
struct ipc_perm shm_perm; /* defines permissions */
size_t shm_segsz; /* size of segment in bytes */
pid_t shm_lpid; /* pid of last shmop() */
pid_t shm_cpid; /* pid of creator */
shmatt_t shm_nattch; /* number of current attaches */
time_t shm_atime; /* last-attach time */
time_t shm_dtime; /* last-detach time */
time_t shm_ctime; /* last-change time */
.
.
.
};
shmget
The first function called is usually shmget, to obtain a shared memory identifier
General Form: int shmget(key_t key, size_t size, int flag);
Key is converted into an identifier and whether a new segment is created or an
existing segment is referenced.
When a new segment is created, the following members of the shmid_ds structure are
initialized.
The ipc_perm structure is initialized . The mode member of this structure is set to the
corresponding permission bits of flag.
shm_lpid, shm_nattch, shm_atime, and shm_dtime are all set to 0.
shm_ctime is set to the current time.
shm_segsz is set to the size requested.
The size parameter is the size of the shared memory segment in bytes.
shmctl
The shmctl function is the catchall for various shared memory operations.
General Form: int shmctl(int shmid, int cmd, struct shmid_ds *buf );
The cmd argument specifies one of the following five commands to be performed, on
the segment specified by shmid.
IPC_STAT Fetch the shmid_ds structure for this segment, storing it in the structure
pointed to by buf.
8
IPC_SET Set the following three fields from the structure pointed to by buf in
the shmid_ds structure associated with this shared memory segment:
shm_perm.uid, shm_perm.gid, and shm_perm.mode.
IPC_RMID Remove the shared memory segment set from the system.
Two additional commands are provided by Linux and Solaris, but are not part of the
Single UNIX Specification.
SHM_LOCK Lock the shared memory segment in memory. This command can be
executed only by the superuser.
SHM_UNLOCK Unlock the shared memory segment. This command can be executed
only by the superuser
shmat
Once a shared memory segment has been created, a process attaches it to its address
space by calling shmat.
General Form: void *shmat(int shmid, const void *addr, int flag);
The address in the calling process at which the segment is attached depends on the
addr argument and whether the SHM_RND bit is specified in flag.
If addr is 0, the segment is attached at the first available address selected by the
kernel. This is the recommended technique.
If addr is nonzero and SHM_RND is not specified, the segment is attached at the
address given by addr.
If addr is nonzero and SHM_RND is specified, the segment is attached at the address
given by (addr − (addr modulus SHMLBA)). The SHM_RND command stands for
‘‘round.’’ SHMLBA stands for ‘‘low boundary address multiple’’ and is always a
power of 2.
The value returned by shmat is the address at which the segment is attached, or −1 if
an error occurred.
If shmat succeeds, the kernel will increment the shm_nattch counter in the shmid_ds
structure associated with the shared memory segment.
shmdt
When we’re done with a shared memory segment, we call shmdt to detach it. This
does not remove the identifier and its associated data structure from the system. The identifier
remains in existence until some process (often a server) specifically removes it by calling
shmctl with a command of IPC_RMID.
General Form: int shmdt(const void *addr);
The addr argument is the value that was returned by a previous call to shmat. If
successful, shmdt will decrement the shm_nattch counter in the associated shmid_ds
structure.
9
PROCESS MANAGEMENT AND SYNCHRONIZATION
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