0% found this document useful (0 votes)
16 views27 pages

Os Module-3 Notes

Concurrency refers to the simultaneous execution of multiple instruction sequences, which can lead to resource sharing issues such as deadlocks and starvation. It enables better resource utilization and performance but requires careful management to avoid complications like race conditions and blocking. Inter-process communication is essential for cooperating processes, utilizing models like shared memory and message passing to facilitate data exchange.

Uploaded by

dikshadeware
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
16 views27 pages

Os Module-3 Notes

Concurrency refers to the simultaneous execution of multiple instruction sequences, which can lead to resource sharing issues such as deadlocks and starvation. It enables better resource utilization and performance but requires careful management to avoid complications like race conditions and blocking. Inter-process communication is essential for cooperating processes, utilizing models like shared memory and message passing to facilitate data exchange.

Uploaded by

dikshadeware
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 27

Module 3

Concurrency:
➔ Concurrency is the execution of multiple instruction sequences at the same time.
➔ It happens in the operating system when there are several process threads
running in parallel.
➔ The running process threads always communicate with each other through shared
memory or message passing.
➔ Concurrency results in sharing of resources resulting in problems like deadlocks
and resource starvation.
➔ It helps in techniques like coordinating execution of processes, memory allocation
and execution scheduling for maximizing throughput.
Principles of Concurrency:
➔ Interleaved Processes are concurrent processes.
➔ Overlapped Processes are concurrent processes.

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 prevent its use by other processes.

Issues of Concurrency:
➔ Non-atomic - Operations that are non-atomic but interruptible by several
processes may cause issues. A non-atomic operation depends on other
processes, and an atomic operation runs independently of other processes.
➔ Deadlock - In concurrent computing, it occurs when one group member waits for
another member, including itself, to send a message and release a lock. Software
and hardware locks are commonly used to arbitrate shared resources and
implement process synchronization in parallel computing, distributed systems, and
multiprocessing.
➔ Blocking - A blocked process is waiting for some event, like the availability of a
resource or completing an I/O operation. Processes may block waiting for
resources, and a process may be blocked for a long time waiting for terminal
input. If the process is needed to update some data periodically, it will be very
undesirable.
➔ Race Conditions - A race problem occurs when the output of a software
application is determined by the timing or sequencing of other uncontrollable
events. Race situations can also happen in multithreaded software, runs in a
distributed environment, or are interdependent on shared resources.
➔ Starvation - A problem in concurrent computing is where a process is continuously
denied the resources it needs to complete its work. It could be caused by errors
in scheduling or mutual exclusion algorithms, but resource leaks may also cause it.

Advantages of Concurrency :
➔ Running multiple applications – It enables you 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 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.
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.
Inter-Process Communication:
➔ Processes executing concurrently in the operating system may be either
independent processes or cooperating processes.
➔ A process is independent if it cannot affect or be affected by the other
processes executing in the system.
➔ Any process that does not share data with any other process is independent.
➔ A process is cooperating if it can affect or be affected by the other processes
executing in the system.
➔ Any process that shares data with other processes is a cooperating process.
➔ There are several reasons for providing an environment that allows process
cooperation
➔ information sharing - Since several users may be interested in the same piece
of information (for instance, a shared file), we must provide an environment to
allow concurrent access to such information.
➔ Computation speedup - If we want a particular task to run faster, we must
break it into subtasks, each of which will be executing in parallel with the others.
Notice that such a speedup can be achieved only if the computer has multiple
processing cores.
➔ Modularity - We may want to construct the system in a modular fashion,
dividing the system functions into separate processes or threads.
➔ Convenience - Even an individual user may work on many tasks at the same
time. For instance, a user may be editing, listening to music, and compiling in
parallel.
➔ Cooperating processes require an interprocess communication (IPC) mechanism
that will allow them to exchange data and information.
➔ There are two fundamental models of interprocess communication: shared
memory and message passing.
➔ In the shared-memory model, a region of memory that is shared by cooperating
processes is established.
➔ Processes can then exchange information by reading and writing data to the
shared region.
➔ In the message-passing model, communication takes place by means of messages
exchanged between the cooperating processes.
● Shared-Memory Systems
➔ Interprocess communication using shared memory requires communicating
processes to establish a region of shared memory.
➔ A shared-memory region resides in the address space of the process creating
the shared-memory segment.
➔ Other processes that wish to communicate using this shared-memory segment
must attach it to their address space.
➔ The operating system tries to prevent one process from accessing another
process’s memory.
➔ Shared memory requires that two or more processes agree to remove this
restriction.
➔ They can then exchange information by reading and writing data in the shared
areas.
➔ The form of the data and the location are determined by these processes and
are not under the operating system’s control.
➔ The processes are also responsible for ensuring that they are not writing to the
same location simultaneously.

Fig. Communications models. (a) Message passing. (b) Shared memory.


➔ To illustrate the concept of cooperating processes, let’s consider the
producer–consumer problem.
➔ A producer process produces information that is consumed by a consumer
process.
➔ For example, a compiler may produce assembly code that is consumed by an
assembler.
➔ The assembler, in turn, may produce object modules that are consumed by the
loader.
➔ One solution to the producer–consumer problem uses shared memory.
➔ To allow producer and consumer processes to run concurrently, we must have
available a buffer of items that can be filled by the producer and emptied by the
consumer.
➔ This buffer will reside in a region of memory that is shared by the producer and
consumer processes.
➔ A producer can produce one item while the consumer is consuming another item.
➔ The producer and consumer must be synchronized, so that the consumer does
not try to consume an item that has not yet been produced.
➔ Two types of buffers can be used.
➔ The unbounded buffer places no practical limit on the size of the buffer.
➔ The consumer may have to wait for new items, but the producer can always
produce new items.
➔ The bounded buffer assumes a fixed buffer size. In this case, the consumer
must wait if the buffer is empty, and the producer must wait if the buffer is full.
➔ The following variables reside in a region of memory shared by the producer and
consumer processes:

#define BUFFER SIZE 10


typedef struct {
...
}item;
item buffer[BUFFER SIZE];
int in = 0;
int out = 0;

➔ The shared buffer is implemented as a circular array with two logical pointers: in
and out. The variable in points to the next free position in the buffer; out points
to the first full position in the buffer. The buffer is empty when in == out; the
buffer is full when ((in + 1) % BUFFER SIZE) == out.
➔ The producer process has a local variable next produced in which the new item to
be produced is stored. The consumer process has a local variable next consumed
in which the item to be consumed is stored. This scheme allows at most BUFFER
SIZE - 1 items in the buffer at the same time. We leave it as an exercise for you
to provide a solution in which BUFFER SIZE items can be in the buffer at the
same time.

The producer process using shared memory.

while (true) {
/* produce an item in next produced */
while (((in + 1) % BUFFER SIZE) == out)
; /* do nothing */
buffer[in] = next produced;
in = (in + 1) % BUFFER SIZE;
}
The consumer process using shared memory.

while (true) {
while (in == out)
; /* do nothing */
next consumed = buffer[out];
out = (out + 1) % BUFFER SIZE;
/* consume the item in next consumed */
}

Message-Passing Systems
➔ Message passing provides a mechanism to allow processes to communicate and
to synchronize their actions without sharing the same address space.
➔ It is particularly useful in a distributed environment, where the communicating
processes may reside on different computers connected by a network.
➔ For example, an Internet chat program could be designed so that chat participants
communicate with one another by exchanging messages.
➔ A message-passing facility provides at least two operations:
send(message)
receive(message)
➔ Messages sent by a process can be either fixed or variable in size.
➔ If processes P and Q want to communicate, they must send messages to and
receive messages from each other: a communication link must exist between
them. This link can be implemented in a variety of ways.
➔ Here are several methods for logically implementing a link and the send()/receive()
operations:
➔ Direct or indirect communication
➔ Synchronous or asynchronous communication
➔ Automatic or explicit buffering

Naming
➔ Processes that want to communicate must have a way to refer to each other.
➔ They can use either direct or indirect communication.
➔ Under direct communication, each process that wants to communicate must
explicitly name the recipient or sender of the communication.
➔ In this scheme, the send() and receive() primitives are defined as:
➔ send(P, message)—Send a message to process P.
➔ receive(Q, message)—Receive a message from process Q
➔ A communication link in this scheme has the following properties:
➔ A link is established automatically between every pair of processes that want
to communicate. The processes need to know only each other’s identity to
communicate.
➔ A link is associated with exactly two processes.
➔ Between each pair of processes, there exists exactly one link.
➔ This scheme exhibits symmetry in addressing; that is, both the sender process
and the receiver process must name the other to communicate. A variant of this
scheme employs asymmetry in addressing. Here, only the sender names the
recipient; the recipient is not required to name the sender. In this scheme, the
send() and receive() primitives are defined as follows:
➔ send(P, message)—Send a message to process P.
➔ receive(id, message)—Receive a message from any process. The variable id is
set to the name of the process with which communication has taken place.

➔ With indirect communication, the messages are sent to and received from
mailboxes, or ports.
➔ A mailbox can be viewed abstractly as an object into which messages can be
placed by processes and from which messages can be removed.
➔ Each mailbox has a unique identification. For example, POSIX message queues use
an integer value to identify a mailbox. A process can communicate with another
process via a number of different mailboxes, but two processes can
communicate only if they have a shared mailbox. The send() and receive()
primitives are defined as follows:
➔ send(A, message)—Send a message to mailbox A.
➔ receive(A, message)—Receive a message from mailbox A.
➔ In this scheme, a communication link has the following properties:
➔ A link is established between a pair of processes only if both members of
➔ the pair have a shared mailbox.
➔ A link may be associated with more than two processes.
➔ Between each pair of communicating processes, a number of different links
➔ may exist, with each link corresponding to one mailbox.
➔ Now suppose that processes P1, P2, and P3 all share mailbox A. Process P1
sends a message to A, while both P2 and P3 execute a receive() from A. Which
process will receive the message sent by P1? The answer depends on which of
the following methods we choose:
➔ Allow a link to be associated with two processes at most.
➔ Allow at most one process at a time to execute a receive() operation.
➔ Allow the system to select arbitrarily which process will receive the message
(that is, either P2 or P3, but not both, will receive the message). The system
may define an algorithm for selecting which process will receive the message
(for example, round robin, where processes take turns receiving messages).
The system may identify the receiver to the sender.
➔ A mailbox may be owned either by a process or by the operating system.
➔ If the mailbox is owned by a process (that is, the mailbox is part of the address
space of the process), then we distinguish between the owner (which can only
receive messages through this mailbox) and the user (which can only send
messages to the mailbox). Since each mailbox has a unique owner, there can be
no confusion about which process should receive a message sent to this mailbox.
When a process that owns a mailbox terminates, the mailbox disappears. Any
process that subsequently sends a message to this mailbox must be notified that
the mailbox no longer exists.
➔ In contrast, a mailbox that is owned by the operating system has an existence of
its own. It is independent and is not attached to any particular process. The
operating system then must provide a mechanism that allows a process to do the
following:
➔ Create a new mailbox.
➔ Send and receive messages through the mailbox.
➔ Delete a mailbox.
➔ The process that creates a new mailbox is that mailbox’s owner by default.
Initially, the owner is the only process that can receive messages through this
mailbox. However, the ownership and receiving privilege may be passed to other
processes through appropriate system calls.

Synchronization
➔ Communication between processes takes place through calls to send() and
receive() primitives. There are different design options for implementing each
primitive. Message passing may be either blocking or nonblocking— also known as
synchronous and asynchronous.
➔ Blocking send. The sending process is blocked until the message is received by
the receiving process or by the mailbox.
➔ Nonblocking send. The sending process sends the message and resumes
operation.
➔ Blocking receive. The receiver blocks until a message is available.
➔ Nonblocking receive. The receiver retrieves either a valid message or a null.
Buffering
➔ Whether communication is direct or indirect, messages exchanged by
communicating processes reside in a temporary queue. Basically, such queues
can be implemented in three ways:
➔ Zero capacity. The queue has a maximum length of zero; thus, the link cannot
have any messages waiting in it. In this case, the sender must block until the
recipient receives the message.
➔ Bounded capacity. The queue has finite length n; thus, at most n messages
can reside in it. If the queue is not full when a new message is sent, the
message is placed in the queue (either the message is copied or a pointer to
the message is kept), and the sender can continue execution without waiting.
The link’s capacity is finite, however. If the link is full, the sender must block
until space is available in the queue.
➔ Unbounded capacity. The queue’s length is potentially infinite; thus, any number
of messages can wait in it. The sender never blocks.

COMPETITION AMONG PROCESSES FOR RESOURCES


➔ Concurrent processes come into conflict with each other when they are
competing for the use of the same resource. In its pure form, we can
describe the situation as follows. Two or more processes need to access a
resource during the course of their execution. Each process is unaware of the
existence of other processes, and each is to be unaffected by the execution
of the other processes. It follows from this that each process should leave the
state of any resource that it uses unaffected. Examples of resources include
I/O devices, memory, processor time, and the clock.
➔ There is no exchange of information between the competing processes.
However, the execution of one process may affect the behavior of competing
pro- cesses. In particular, if two processes both wish access to a single
resource, then one process will be allocated that resource by the OS, and the
other will have to wait. Therefore, the process that is denied access will be
slowed down. In an extreme case, the blocked process may never get access
to the resource and hence will never terminate successfully.
➔ In the case of competing processes three control problems must be faced.
First is the need for mutual exclusion. Suppose two or more processes
require access to a single nonsharable resource, such as a printer. During the
course of execution, each process will be sending commands to the I/O device,
receiving status information, sending data, and/or receiving data. We will refer to
such a resource as a critical resource, and the portion of the program that
uses it as a critical section of the program. It is important that only one
program at a time be allowed in its critical section. We cannot simply rely on
the OS to understand and enforce this restriction because the detailed
requirements may not be obvious. In the case of the printer, for example, we
want any individual process to have con- trol of the printer while it prints an
entire file. Otherwise, lines from competing processes will be interleaved.
➔ The enforcement of mutual exclusion creates two additional control prob- lems.
One is that of deadlock. For example, consider two processes, P1 and P2, and
two resources, R1 and R2. Suppose that each process needs access to both
resources to perform part of its function. Then it is possible to have the
following situation: the OS assigns R1 to P2, and R2 to P1. Each process is
waiting for one of the two resources. Neither will release the resource that it
already owns until it has acquired the other resource and performed the
function requiring both resources. The two processes are deadlocked.
➔ A final control problem is starvation. Suppose that three processes (P1, P2,
P3) each require periodic access to resource R. Consider the situation in which
P1 is in possession of the resource, and both P2 and P3 are delayed, waiting
for that resource. When P1 exits its critical section, either P2 or P3 should be
allowed access to R. Assume that the OS grants access to P3 and that P1
again requires access before P3 completes its critical section. If the OS grants
access to P1 after P3 has finished, and subsequently alternately grants
access to P1 and P3, then P2 may indefinitely be denied access to the
resource, even though there is no dead- lock situation.
COOPERATION AMONG PROCESSES BY SHARING
➔ COOPERATION AMONG PROCESSES BY SHARING The case of cooperation by
sharing covers processes that interact with other processes without being
explicitly aware of them. For example, multiple processes may have access to
shared variables or to shared files or databases. Processes may use and
update the shared data without reference to other processes but know that
other processes may have access to the same data. Thus the processes
must cooperate to ensure that the data they share are properly managed.
The control mechanisms must ensure the integrity of the shared data.
COOPERATION AMONG PROCESSES BY COMMUNICATION
➔ In the first two cases that we have discussed, each process has its own
isolated environment that does not include the other processes. The
interactions among processes are indirect. In both cases, there is a sharing. In
the case of competition, they are sharing resources without being aware of the
other processes. In the second case, they are sharing values, and although
each process is not explicitly aware of the other processes, it is aware of the
need to maintain data integrity. When processes cooperate by communication,
however, the various processes participate in a common effort that links all of
the processes. The communication provides a way to synchronize, or
coordinate, the various activities.
➔ Typically, communication can be characterized as consisting of messages of
some sort. Primitives for sending and receiving messages may be provided as
part of the programming language or provided by the OS kernel.
➔ Because nothing is shared between processes in the act of passing
messages, mutual exclusion is not a control requirement for this sort of
cooperation. However, the problems of deadlock and starvation are still
present. As an example of deadlock, two processes may be blocked, each
waiting for a communication from the other. As an example of starvation,
consider three processes, P1, P2, and P3, that exhibit the following behavior. P1
is repeatedly attempting to communicate with either P2 or P3, and P2 and P3
are both attempting to communicate with P1. A sequence could arise in which
P1 and P2 exchange information repeatedly, while P3 is blocked waiting for a
communication from P1. There is no deadlock, because P1 remains active, but
P3 is starved.
Requirements for Mutual Exclusion
➔ Mutual Exclusion Must be forced :Only one process at a time is allowed into its
critical section, among all processes that have critical sections for the same
resource or shared object.
➔ A process that halts in its noncritical section must do so without interfering
with other processes.
➔ It must not be possible for a process requiring access to a critical section to
be delayed indefinitely: no deadlock or starvation.
➔ When no process is in a critical section, any process that requests entry to its
critical section must be permitted to enter without delay.
➔ No assumptions are made about relative process speedsor number of
processors.
➔ A process remains inside its critical section for a finite time only.

Mutual Exclusion Hardware Support

Interrupt Disabling
➔ In a uniprocessor system, concurrent processes cannot have overlapped
execution; they can only be interleaved. Furthermore, a process will continue to
run until it invokes an OS service or until it is interrupted. Therefore, to
guarantee mutual exclusion, it is sufficient to prevent a process from being
interrupted. This capability can be provided in the form of primitives defined
by the OS kernel for disabling and enabling interrupts. Because the critical
section cannot be interrupted, mutual exclusion is guaranteed.
Special Machine Instructions
➔ In a multiprocessor configuration, several processors share access to a
common main memory. In this case, there is not a master/slave relationship;
rather the processors behave independently in a peer relationship. There is no
interrupt mechanism between processors on which mutual exclusion can be
based.
➔ At the hardware level, as was mentioned, access to a memory location ex-
cludes any other access to that same location. With this as a foundation, pro-
cessor designers have proposed several machine instructions that carry out
two actions atomically,2 such as reading and writing or reading and testing, of a
single memory location with one instruction fetch cycle. During execution of the
instruction, access to the memory location is blocked for any other instruction
referencing that location.
The Critical-Section Problem
➔ Consider a system consisting of n processes {P0, P1,..., Pn−1}. Each process
has a segment of code, called a critical section, in which the process may be
changing common variables, updating a table, writing a file, and so on. The
important feature of the system is that, when one process is executing in its
critical section, no other process is allowed to execute in its critical section.
That is, no two processes are executing in their critical sections at the same
time. The critical-section problem is to design a protocol that the processes
can use to cooperate. Each process must request permission to enter its
critical section. The section of code implementing this request is the entry
section. The critical section may be followed by an exit section. The remaining
code is the remainder section. The general structure of a typical process Pi is
shown in Figure 5.1. The entry section and exit section are enclosed in boxes
to highlight these important segments of code.
➔ A solution to the critical-section problem must satisfy the following three
requirements:
Mutual exclusion. If process Pi is executing in its critical section, then no other
processes can be executing in their critical sections.
Progress. If no process is executing in its critical section and some processes
wish to enter their critical sections, then only those processes that are not
executing in their remainder sections can participate in deciding which will enter
its critical section next, and this selection cannot be postponed indefinitely.
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.
➔ Two general approaches are used to handle critical sections in operating
systems: preemptive kernels and nonpreemptive kernels. A preemptive kernel
allows a process to be preempted while it is running in kernel mode. A
nonpreemptive kernel does not allow a process running in kernel mode to be
preempted; a kernel-mode process will run until it exits kernel mode, blocks, or
voluntarily yields control of the CPU.
➔ The critical-section problem could be solved simply in a single-processor
environment if we could prevent interrupts from occurring while a shared
variable was being modified. In this way, we could be sure that the current
sequence of instructions would be allowed to execute in order without pre-
emption. No other instructions would be run, so no unexpected modifications
could be made to the shared variable. This is often the approach taken by
nonpreemptive kernels.
➔ Unfortunately, this solution is not as feasible in a multiprocessor environ- ment.
Disabling interrupts on a multiprocessor can be time consuming, since the
message is passed to all the processors. This message passing delays entry
into each critical section, and system efficiency decreases. Also consider the
effect on a system’s clock if the clock is kept updated by interrupts.
➔ Many modern computer systems therefore provide special hardware
instructions that allow us either to test and modify the content of a word or
to swap the contents of two words atomically—that is, as one uninterruptible
unit. We can use these special instructions to solve the critical-section problem
in a relatively simple manner. Rather than discussing one specific instruction
for one specific machine, we abstract the main concepts behind these types
of instructions by describing the test and set() and compare and swap()
instructions.
➔ The test and set() instruction can be defined as shown in Figure 5.3. The
important characteristic of this instruction is that it is executed atomically.
Thus, if two test and set() instructions are executed simultaneously (each on
a different CPU), they will be executed sequentially in some arbitrary order. If
the machine supports the test and set() instruction, then we can implement
mutual exclusion by declaring a boolean variable lock, initialized to false. The
structure of process Pi is shown in Figure 5.4.
➔ The compare and swap() instruction, in contrast to the test and set()
instruction, operates on three operands; it is defined in Figure 5.5. The
operand value is set to new value only if the expression (*value == exected) is
true. Regardless, compare and swap() always returns the original value of the
variable value. Like the test and set() instruction, compare and swap() is
executed atomically. Mutual exclusion can be provided as follows: a global
variable (lock) is declared and is initialized to 0. The first process that invokes
compare and swap() will set lock to 1. It will then enter its critical section,
because the original value of lock was equal to the expected value of 0.
Subsequent calls to compare and swap() will not succeed, because lock now is
not equal to the expected value of 0. When a process exits its critical section,
it sets lock back to 0, which allows another process to enter its critical section.
The structure of process Pi is shown in Figure 5.6.

➔ Although these algorithms satisfy the mutual-exclusion requirement, they do not


satisfy the bounded-waiting requirement. In Figure 5.7, we present another
algorithm using the test and set() instruction that satisfies all the
critical-section requirements. The common data structures are
boolean waiting[n];
boolean lock;
➔ These data structures are initialized to false. To prove that the mutual-
exclusion requirement is met, we note that process Pi can enter its critical
section only if either waiting[i] == false or key == false. The value of key can
become false only if the test and set() is executed. The first process to
execute the test and set() will find key == false; all others must wait. The
variable waiting[i] can become false only if another process leaves its critical
section; only one waiting[i] is set to false, maintaining the mutual-exclusion
requirement.
➔ To prove that the progress requirement is met, we note that the arguments
presented for mutual exclusion also apply here, since a process exiting the
critical section either sets lock to false or sets waiting[j] to false. Both allow a
process that is waiting to enter its critical section to proceed.
➔ To prove that the bounded-waiting requirement is met, we note that, when a
process leaves its critical section, it scans the array waiting in the cyclic
ordering (i + 1, i + 2, ..., n − 1, 0, ..., i − 1). It designates the first process in this
ordering that is in the entry section (waiting[j] == true) as the next one to
enter the critical section. Any process waiting to enter its critical section will
thus do so within n − 1 turns.
Semaphore
➔ A semaphore S is an integer variable that, apart from initialization, is accessed
only through two standard atomic operations: wait() and signal(). The wait()
operation was originally termed P (from the Dutch proberen, “to test”); signal()
was originally called V (from verhogen, “to increment”). The definition of wait() is
as follows:

➔ The definition of signal() is as follows:

➔ Operating systems often distinguish between counting and binary semaphores.


The value of a counting semaphore can range over an unrestricted domain. The
value of a binary semaphore can range only between 0 and 1. Thus, binary
semaphores behave similarly to mutex locks. In fact, on systems that do not
provide mutex locks, binary semaphores can be used instead for providing
mutual exclusion.
➔ 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.
➔ We can also use semaphores to solve various synchronization problems. For
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
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.
➔ The definitions of the wait() and signal() semaphore operations just described
present the same problem. To overcome the need for busy waiting, we can
modify the definition of the wait() and signal() operations as follows: 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.
➔ 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. (The CPU
may or may not be switched from the running process to the newly ready
process, depending on the CPU-scheduling algorithm.)
➔ To implement semaphores under this definition, we define a semaphore as
follows:
typedef struct{
int value;
struct process *list;
} semaphore;
➔ Each semaphore has an integer value and a list of processes list. When a
process must wait on a semaphore, it is added to the list of processes. A
signal() operation removes one process from the list of waiting processes and
awakens that process.
➔ Now, the wait() semaphore operation can be defined as

wait(semaphore *S) {

S->value--;

if (S->value < 0) {

add this process to S->list;

block();

}
➔ and the signal() semaphore operation can be defined as

signal(semaphore *S) {

S->value++;

if (S->value <= 0) {

remove a process P from S->list; wakeup(P);

➔ The block() operation suspends the process that invokes it. The wakeup(P)
operation resumes the execution of a blocked process P. These two
operations are provided by the operating system as basic system calls.
➔ Note that in this implementation, semaphore values may be negative, whereas
semaphore values are never negative under the classical definition of
semaphores with busy waiting. If a semaphore value is negative, its magnitude
is the number of processes waiting on that semaphore. This fact results from
switching the order of the decrement and the test in the implementation of the
wait() operation.
➔ The list of waiting processes can be easily implemented by a link field in each
process control block (PCB). Each semaphore contains an integer value and a
pointer to a list of PCBs. One way to add and remove processes from the list
so as to ensure bounded waiting is to use a FIFO queue, where the
semaphore contains both head and tail pointers to the queue. In general,
however, the list can use any queueing strategy. Correct usage of semaphores
does not depend on a particular queueing strategy for the semaphore lists.
➔ It is critical that semaphore operations be executed atomically. We must
guarantee that no two processes can execute wait() and signal() operations
on the same semaphore at the same time. This is a critical-section problem;
and in a single-processor environment, we can solve it by simply inhibiting
interrupts during the time the wait() and signal() operations are executing. This
scheme works in a single-processor environment because, once interrupts are
inhibited, instructions from different processes cannot be interleaved. Only the
currently running process executes until interrupts are reenabled and the
scheduler can regain control.
➔ In a multiprocessor environment, interrupts must be disabled on every pro-
cessor. Otherwise, instructions from different processes (running on different
processors) may be interleaved in some arbitrary way. Disabling interrupts on
every processor can be a difficult task and furthermore can seriously diminish
performance. Therefore, SMP systems must provide alternative locking tech-
niques—such as compare and swap() or spinlocks—to ensure that wait() and
signal() are performed atomically.
➔ It is important to admit that we have not completely eliminated busy waiting
with this definition of the wait() and signal() operations. Rather, we have moved
busy waiting from the entry section to the critical sections of application
programs. Furthermore, we have limited busy waiting time to the critical
sections of the wait() and signal() operations, and these sections are short (if
properly coded, they should be no more than about ten instructions). Thus, the
critical section is almost never occupied, and busy waiting occurs rarely, and
then for only a short time. An entirely different situation exists with application
programs whose critical sections may be long (minutes or even hours) or may
almost always be occupied. In such cases, busy waiting is extremely inefficient.
Principles of Deadlock:
➔ Deadlock can be defined as the permanent blocking of a set of processes
that either compete for system resources or communicate with each other. A
set of processes is deadlocked when each process in the set is blocked
awaiting an event (typically the freeing up of some requested resource) that
can only be triggered by another blocked process in the set. Deadlock is
permanent because none of the events is ever triggered.

➔ All deadlocks involve conflicting needs for resources by two or more


processes. A common example is the traffic deadlock. Figure 6.1a shows a
situation in which four cars have arrived at a four-way stop intersection at
approximately the same time. The four quadrants of the intersection are the
resources over which control is needed. In particular, if all four cars wish to go
straight through the intersection, the resource requirements are as follows:

​ Car 1, traveling north, needs quadrants a and b.

​ Car 2 needs quadrants b and c.

​ Car 3 needs quadrants c and d.

​ Car 4 needs quadrants d and a.

➔ If all four cars ignore the rules and proceed (cautiously) into the intersection
at the same time, then each car seizes one resource (one quadrant) but
cannot proceed because the required second resource has already been
seized by another car. This is an actual deadlock.
Reusable Resource

➔ Two general categories of resources can be distinguished: reusable and


consumable. A reusable resource is one that can be safely used by only one
process at a time and is not depleted by that use. Processes obtain
resource units that they later release for reuse by other processes. Examples
of reusable resources include processors; I/O channels; main and secondary
memory; devices; and data structures such as files, databases, and
semaphores.
➔ As an example of deadlock involving reusable resources, consider two
processes that compete for exclusive access to a disk file D and a tape
drive T. The programs engage in the operations depicted in Figure 6.4. Deadlock
occurs if each process holds one resource and requests the other. For
example, deadlock occurs if the multiprogramming system interleaves the
execution of the two processes as follows:

0 p1 q0 q1 p2 q2
p

➔ It may appear that this is a programming error rather than a problem for the
OS designer. However, we have seen that concurrent program design is
challeng- ing. Such deadlocks do occur, and the cause is often embedded in
complex program logic, making detection difficult. One strategy for dealing with
such a deadlock is to impose system design constraints concerning the order
in which resources can be requested.
➔ Another example of deadlock with a reusable resource has to do with requests
for main memory. Suppose the space available for allocation is 200 Kbytes,
and the following sequence of requests occurs:

➔ Deadlock occurs if both processes progress to their second request. If the


amount of memory to be requested is not known ahead of time, it is difficult
to deal with this type of deadlock by means of system design constraints.

Consumable Resources

➔ A consumable resource is one that can be created (produced) and destroyed


(con- sumed). Typically, there is no limit on the number of consumable
resources of a particular type. An unblocked producing process may create
any number of such resources. When a resource is acquired by a consuming
process, the resource ceases to exist. Examples of consumable resources
are interrupts, signals, messages, and information in I/O buffers.
➔ As an example of deadlock involving consumable resources, consider the fol-
lowing pair of processes, in which each process attempts to receive a
message from the other process and then send a message to the other
process:

➔ Deadlock occurs if the Receive is blocking (i.e., the receiving process is blocked
until the message is received). Once again, a design error is the cause of the
deadlock. Such errors may be quite subtle and difficult to detect. Furthermore,
it may take a rare combination of events to cause the deadlock; thus a
program could be in use for a considerable period of time, even years, before
the deadlock actually occurs.

Resource Allocation Graphs

➔ The resource allocation graph is a directed graph that depicts a state of the
system of resources and pro- cesses, with each process and each resource
represented by a node. A graph edge directed from a process to a resource
indicates a resource that has been requested by the process but not yet
granted (Figure 6.5a). Within a resource node, a dot is shown for each instance
of that resource. Examples of resource types that may have multiple
instances are I/O devices that are allocated by a resource management
module in the OS. A graph edge directed from a reusable resource node dot
to a process indicates a request that has been granted (Figure 6.5b); that is,
the process has been assigned one unit of that resource. A graph edge
directed from a consumable resource node dot to a process indicates that
the process is the producer of that resource.
➔ Figure 6.5c shows an example deadlock. There is only one unit each of re-
sources Ra and Rb. Process P1 holds Rb and requests Ra, while P2 holds Ra
but re- quests Rb. Figure 6.5d has the same topology as Figure 6.5c, but there
is no deadlock because multiple units of each resource are available.
➔ The resource allocation graph of Figure 6.6 corresponds to the deadlock situa-
tion in Figure 6.1b. Note that in this case, we do not have a simple situation in
which two processes each have one resource the other needs. Rather, in this
case, there is a circular chain of processes and resources that results in
deadlock.
Conditions for Deadlock

➔ Mutual exclusion. Only one process may use a resource at a time. No process
may access a resource unit that has been allocated to another process.
➔ Hold and wait. A process may hold allocated resources while awaiting assign-
ment of other resources.
➔ Nopreemption.No resource can be forcibly removed from a process holding it.
➔ Circular wait. A closed chain of processes exists, such that each process holds
at least one resource needed by the next process in the chain.

Deadlock Prevention

Mutual Exclusion

➔ The mutual exclusion condition must hold. That is, at least one resource must
be nonsharable. Sharable resources, in contrast, do not require mutually
exclusive access and thus cannot be involved in a deadlock. Read-only files are
a good example of a sharable resource. If several processes attempt to open
a read-only file at the same time, they can be granted simultaneous access to
the file. A process never needs to wait for a sharable resource. In general,
however, we cannot prevent deadlocks by denying the mutual-exclusion
condition, because some resources are intrinsically nonsharable. For example, a
mutex lock cannot be simultaneously shared by several processes.

Hold and Wait

➔ To ensure that the hold-and-wait condition never occurs in the system, we


must guarantee that, whenever a process requests a resource, it does not
hold any other resources. One protocol that we can use requires each process
to request and be allocated all its resources before it begins execution. We
can implement this provision by requiring that system calls requesting
resources for a process precede all other system calls.
➔ An alternative protocol allows a process to request resources only when it has
none. A process may request some resources and use them. Before it can
request any additional resources, it must release all the resources that it is
currently allocated.

No Preemption

➔ The third necessary condition for deadlocks is that there be no preemption of


resources that have already been allocated. To ensure that this condition does
not hold, we can use the following protocol. If a process is holding some
resources and requests another resource that cannot be immediately allocated
to it (that is, the process must wait), then all resources the process is
currently holding are preempted. In other words, these resources are implicitly
released. The preempted resources are added to the list of resources for
which the process is waiting. The process will be restarted only when it can
regain its old resources, as well as the new ones that it is requesting.
➔ Alternatively, if a process requests some resources, we first check whether
they are available. If they are, we allocate them. If they are not, we check
whether they are allocated to some other process that is waiting for additional
resources. If so, we preempt the desired resources from the waiting process
and allocate them to the requesting process. If the resources are neither
available nor held by a waiting process, the requesting process must wait. While
it is waiting, some of its resources may be preempted, but only if another
process requests them. A process can be restarted only when it is allocated
the new resources it is requesting and recovers any resources that were
preempted while it was waiting.
➔ This protocol is often applied to resources whose state can be easily saved
and restored later, such as CPU registers and memory space. It cannot
generally be applied to such resources as mutex locks and semaphores.

Circular Wait

➔ The circular-wait condition can be prevented by defining a linear ordering of re-


source types. If a process has been allocated resources of type R, then it
may subse- quently request only those resources of types following R in the
ordering.
➔ To see that this strategy works, let us associate an index with each resource
type. Then resource Ri precedes Rj in the ordering if i 6 j. Now suppose that
two processes, A and B, are deadlocked because A has acquired Ri and
requested Rj, and B has acquired Rj and requested Ri. This condition is
impossible because it impliesi 6 jandj 6 i.
➔ As with hold-and-wait prevention, circular-wait prevention may be inefficient,
slowing down processes and denying resource access unnecessarily.

Deadlock Avoidance

➔ In deadlock prevention, we constrain resource requests to prevent at least one


of the four conditions of deadlock. This is either done indirectly, by preventing
one of the three necessary policy conditions (mutual exclusion, hold and wait,
no preemption), or directly, by preventing circular wait. This leads to inefficient
use of resources and inefficient execution of processes. Deadlock avoidance,
on the other hand, allows the three necessary conditions but makes judicious
choices to assure that the deadlock point is never reached. As such, avoidance
allows more concurrency than prevention. With deadlock avoidance, a decision is
made dynamically whether the current resource allocation request will, if
granted, potentially lead to a deadlock. Deadlock avoidance thus requires
knowledge of future process resource requests.

You might also like