Notes - Unit 3rd & 4th
Notes - Unit 3rd & 4th
MEMORY MANAGEMENT
Lecture #15
Indirect Communication
Messages are directed and received from mailboxes (also referred to as ports). ✦
Each mailbox has a unique id.
✦ Processes can communicate only if they share a mailbox.
Properties of communication link
✦ Link established only if processes share a common mailbox
✦ A link may be associated with many processes.
✦ Each pair of processes may share several communication links.
✦ Link may be unidirectional or bi-directional.
Operations
✦ create a new mailbox
✦ send and receive messages through mailbox
✦ destroy a mailbox
Primitives are defined as:
send(A, message) – send a message to mailbox A receive(A,
message) – receive a message from mailbox A
Mailbox sharing
✦ P1, P2, and P3 share mailbox A.
✦ P1, sends; P2 and P3 receive. ✦ Who gets the message?
Solutions
✦ Allow a link to be associated with at most two processes.
Concurrent Processes
The concurrent processes executing 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.Clearly, any process that does not share any data (temporary or persistent) with any other process is independent.
On the other hand, a process is cooperating if it can affect or be affected by the other processes executing in the
system.Clearly, any process that shares data with other processes is a cooperating process.
We may want to provide an environment that allows process cooperation for
several reasons:
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 these types of resources.
Computation speedup: If we want a particular task to run faster, we must break it into subtasks, each of wluch will be
executing in parallel with the others. Such a speedup can be achieved only if the computer has multiple processing
elements (such as CPUS or I/O channels).
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 have many tasks on which to work at one time. For instance, a user may be
editing, printing, and compiling in parallel.
Message-Passing System
The function of a message system is to allow processes to communicate with one another without the need to
resort to shared data. We have already seen message passing used as a method of communication in microkernels. In
this scheme, services are provided as ordinary user processes. That is, the services operate outside of the kernel.
Communication among the user processes is accomplished through the passing of messages. An IPC facility provides at
least the two operations: send (message) and receive (message).
Messages sent by a process can be of either fixed or variable size. If only fixed-sized messages can be sent, the
system-level implementation is straightforward. This restriction, however, makes the task of programming more difficult.
On the other hand, variable-sized messages require a more complex system-level implementation, but the programming
task becomes simpler.
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. We are concerned
here not with the link's physical implementation, but rather with its logical implementation. Here are several methods
for logically implementing a link and the send/receive operations:
• Direct or indirect communication
• Symmetric or asymmetric communication
• Automatic or explicit buffering
• Send by copy or send by reference
• Fixed-sized or variable-sized messages
We look at each of these types of message systems next.
Synchronization
Communication between processes takes place by 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.
Different combinations of send and receive are possible. When both the send and receive are blocking, we have
a rendezvous between the sender and the receiver.
Buffering
Whether the communication is direct or indirect, messages exchanged by communicating processes reside in a temporary
queue. Basically, such a queue can be implemented in three ways:
Zero capacity: The queue has maximum length 0; 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 latter 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 has a finite capacity, however. If the link is full, the
sender must block until space is available in the queue.
Unbounded capacity: The queue has potentially infinite length; thus, any number of messages can wait in it. The sender
never blocks.The zero-capacity case is sometimes referred to as a message system with no buffering; the other cases are
referred to as automatic buffering.
Client-Server Communication
• Sockets
• Remote Procedure Calls
• Remote Method Invocation (Java)
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.
Producer-Consumer Problem
Paradigm for cooperating processes, producer process produces information that is consumed by a consumer
process.
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. 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. In this situation, the consumer must wait until an item is produced.
✦ unbounded-buffer places no practical limit on the size of the buffer. ✦
bounded-buffer assumes that there is a fixed buffer size.
#define BUFFER_SIZE 10
Typedef struct
{
...
} item;
item buffer[BUFFER_SIZE];
int in = 0;
int out =
0;
Solution is correct, but can only use BUFFER_SIZE-1 elements.
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) % BUFFERSIZE) == out.
The code for the producer and consumer processes follows. The producer process has a local variable nextproduced
in which the new item to be produced is stored:
item nextProduced;
while (1)
{
while (((in + 1) % BUFFER_SIZE) == out)
; /* do nothing */
buffer[in] =
nextProduced;
in = (in + 1) % BUFFER_SIZE;
}
item nextConsumed;
while (1)
{
while (in == out) ; /* do
nothing */ nextConsumed =
buffer[out];
out = (out + 1) % BUFFER_SIZE;
}
Consider a system consisting of n processes {Po,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 to be allowed to
execute in its critical section. Thus, the execution of critical sections by the processes is mutually exclusive in 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.
do{
Entry section
Critical section
Exit section
Remainder section
}while(1);
A solution to the critical-section problem must satisfy the following three requirements:
1. Mutual Exclusion: If process Pi is executing in its critical section, then no other processes can be executing in their
critical sections.
2. 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 section can participate in the decision on which will
enter its critical section next, and this selection cannot be postponed indefinitely.
3. Bounded Waiting: There exists a bound 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.
Peterson’s solution
Consider two processes P0 and P1. For convenience, when presenting Pi, we use Pi to denote the other
process; that is, j == 1 - i.
The processes share two variables:
boolean flag [2] ; int turn;
Initially flag [0] = flag [1] = false, and the value of turn is immaterial (but is either 0 or 1).
The structure of process Pi is shown below.
do{
flag[i]=true
turn=j
while(flag[j] && turn==j);
critical section
flag[i]=false
Remainder section
}while(1);
To enter the critical section, process Pi first sets flag [il to be true and then sets turn to the value j, thereby
asserting that if the other process wishes to enter the critical section it can do so. If both processes try to enter at the
same time, turn will be set to both i and j at roughly the same time. Only one of these assignments will last; the other will
occur, but will be overwritten immediately. The eventual value of turn decides which of the two processes is allowed to
enter its critical section first.
We now prove that this solution is correct. We need to show that:
1. Mutual exclusion is preserved,
2. The progress requirement is satisfied,
3. The bounded-waiting requirement is met.
To prove property 1, we note that each Pi enters its critical section only if either flag [jl == false or turn == i. Also
note that, if both processes can be executing in their critical sections at the same time, then flag [i] ==flag [jl == true.
These two observations imply that P0 and P1 could not have successfully executed their while statements at about the
same time, since the value of turn can be either 0 or 1, but cannot be both. Hence, one of the processes say Pj-must have
successfully executed the while statement, whereas Pi had to execute at least one additional statement ("turn == j").
However, since, at that time, flag [j] == true, and turn == j, and this condition will persist as long as Pi is in its critical
section, the result follows:
To prove properties 2 and 3, we note that a process Pi can be prevented from entering the critical section only
if it is stuck in the while loop with the condition flag [j] == true and turn == j; this loop is the only one. If Pi is not ready to
enter the critical section, then flag [ j ] == false and Pi can enter its critical section. If Pi has set flag[j] to true and is also
executing in its while statement, then either turn == i or turn == j. If turn == i, then Pi will enter the critical section. If turn
== j, then Pi will enter the critical section. However, once Pi exits its critical section, it will reset flag [ jl to false, allowing
Pi to enter its critical section. If Pi resets flag [ j 1 to true, it must also set turn to i.
Thus, since Pi does not change the value of the variable turn while executing the while statement, Pi will enter the
critical section (progress) after at most one entry by Pi (bounded waiting).
Synchronization Hardware
As with other aspects of software, hardware features can make the programming task easier and improve
system efficiency. In this section, we present some simple hardware instructions that are available on many systems, and
show how they can be used effectively in solving the critical-section problem.
while(TestAndSet(lock));
critical section lock=false
Remainder section
}while(1);
Semaphores
The solutions to the critical-section problem presented before are not easy to generalize to more complex
problems. To overcome this difficulty, we can use a synchronization tool called a semaphore. A semaphore S is an integer
variable that, apart from initialization, is accessed only through two standard atomic operations: wait and signal. These
operations were originally termed P (for wait; from the Dutch proberen, to test) and V (for signal; from verhogen, to
increment). The classical definition of wait in pseudocode is
wait(S) {
while (S <=
0)
; // no-op
S --
;}
The classical definitions of signal in pseudocode is
Signal(S){
S++;
}
Modifications to the integer value of the semaphore in the wait and signal operations must be executed
indivisibly. That is, when one process modifies the semaphore value, no other process can simultaneously modify that
same semaphore value. In addition, in the case of the wait (S), the testing of the integer value of S (S 5 O), and its possible
modification (S--), must also be executed without interruption.
Usage
We can use semaphores to deal with the n-process critical-section problem. The n processes share a semaphore,
mutex (standing for mutual exclusion), initialized to 1. Each process Pi is organized as shown in Figure.We can also use
semaphores to solve various synchronization problems.
For example, consider two concurrently running processes: PI with a statement S1 and P2 with a statement S2. Suppose
that 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, and by inserting the statements in process PI, and the
statements
wait (synch) ;
s2;
in process P2. Because synch is initialized to 0, P2 will execute S2 only after PI has invoked
signal (synch) , which is after S1.
s1;
signal (synch) ; do { wait
(mutex) ; critical section
signal (mutex) ;
remainder section
} while (1);
Mutual-exclusion implementation with semaphores.
The main disadvantage of the mutual-exclusion solutions and of the semaphore definition given here, is that they
all require busy waiting. While a process is in its critical section, any other process that tries to enter its critical section must
loop continuously in the entry code. This continual looping is clearly a problem in a real multiprogramming system, where
a single CPU is
shared among many processes. Busy waiting wastes CPU cycles that some other process might be able to use
productively. This type of semaphore is also called a spinlock (because the process "spins" while waiting for the lock).
Spinlocks are useful in multiprocessor systems. The advantage of a spinlock is that no context switch is required when a
process must wait on a lock, and a context switch may take considerable time. Thus, when locks are expected to be held
for short times, spinlocks are useful.
To overcome the need for busy waiting, we can modify the definition of the wait and signal semaphore
operations. When a process executes the wait operation and finds that the semaphore value is not positive, it must wait.
However, rather than 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.)
typedef struct {
int value ;
struct process
*L;
) semaphore;
Each semaphore has an integer value and a list of processes. 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.
The wait semaphore operation can now be defined as
void wait(semaphore S)
{ S.value--; if (S.value <
0) { add this process to
S . L; block() ;
}
The signal semaphore operation can now be defined as
void signal(semaphore S) {
S.value++; if (S.value <= 0)
{
remove a process P from S . L ; wakeup
(PI) ;
}
The block operation suspends the process that invokes it. The wakeup(P1) operation resumes the execution of a blocked
process P. These two operations are provided by the operating system as basic system calls.
Initially S1 = 1, S2 = 0, and the value of integer C is set to the initial value of the counting semaphore S.
wait (S1)
; C--; i f
(C < 0) {
signal(S1
);
wait (S2) ;
}
signal(S1);
The bounded-buffer problem is commonly used to illustrate the power of synchronization primitives. We present
here a general structure of this scheme, without committing ourselves to any particular implementation. We assume that
the pool consists of n buffers, each capable of holding one item. The mutex semaphore provides mutual exclusion for
accesses to the buffer pool and is initialized to the value 1. The empty and full semaphores count the number of empty
and full buffers, respectively. The semaphore empty is initialized to the value n; the semaphore f u l l is initialized to the
value 0.
The code for the producer process is
do{
produce an item in nextp
...
wait (empty) ;
wait (mutex) ;
...
add nextp to buffer
...
do{
wait (full) ; wait (mutex) ;
...
remove an item from buffer to nextc
...
signal (mutex) ; signal (empty) ;
...
consume the item in nextc
...
) while (1);
Note the symmetry between the producer and the consumer. We can interpret this code as the producer producing full
buffers for the consumer, or as the consumer producing empty buffers for the producer.
A data object (such as a file or record) is to be shared among several concurrent processes. Some of these
processes may want only to read the content of the shared object, whereas others may want to update (that is, to read
and write) the shared object. We distinguish between these two types of processes by referring to those processes that
are interested in only reading as readers, and to the rest as writers. Obviously, if two readers access the shared data object
simultaneously, no adverse effects will result. However, if a writer and some other process (either a reader or a writer)
access the shared object simultaneously, chaos may ensue.
To ensure that these difficulties do not arise, we require that the writers have exclusive access to the shared
object. This synchronization problem is referred to as the readers-writers problem. Since it was originally stated, it has
been used to test nearly every new synchronization primitive. The readers-writers problem has several variations, all
involving priorities. The simplest one, referred to as the first readers-writers problem, requires that no reader will be kept
waiting unless a writer has already obtained permission to use the shared object. In other words, no reader should wait for
other readers to finish simply because a writer is waiting. The second readers-writers problem requires that, once a writer
is ready, that writer performs its write as soon as possible. In other words, if a writer is waiting to access the object, no new
readers may start reading.
A solution to either problem may result in starvation. In the first case, writers may starve; in the second case,
readers may starve. For this reason, other variants of the problem have been proposed. In this section, we present a
solution to the first readers-writers problem.
In the solution to the first readers-writers problem, the reader processes share the following data structures:
The semaphores mutex and wrt are initialized to 1; readcount is initialized to 0. The semaphore wrt is common to
both the reader and writer processes. The mutex semaphore is used to ensure mutual exclusion when the variable
readcount is updated. The readcount variable keeps track of how many processes are currently reading the object. The
semaphore wrt 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 code for a writer process is do{ wait (wrt) ;
...
writing is performed
...
signal(wrt);
}while(1);
Consider five philosophers who spend their lives thinking and eating. The philosophers share a common circular
table surrounded by five chairs, each belonging to one philosopher. In the center of the table is a bowl of rice, and the
table is laid with five single chopsticks. When a philosopher thinks, she does not interact with her colleagues. From time
to time, a philosopher gets hungry and tries to pick up the two chopsticks that are closest to her (the chopsticks that are
between her and her left and right neighbors). A philosopher may pick up only one chopstick at a time. Obviously, she
cannot pick up a chopstick that is already in the hand of a neighbor. When a hungry philosopher has both her chopsticks
at the same time, she eats without releasing her chopsticks. When she is finished eating, she puts down both of her
chopsticks and starts thinking again.
do {
wait (chopstick[i]) ; wait
(chopstick[(i+1) % 5] ) ;
...
eat
...
signal (chopstick [i] ; signal(chopstick[(i+1)
% 5] ) ;
...
think
...
) while (1);
.
The dining-philosophers problem is considered a classic synchronization problem, neither because of its practical
importance nor because computer scientists dislike philosophers, but because it is an example of a large class of
concurrency-control problems. It is a simple representation of the need to allocate several resources among several
processes in a deadlock- and starvation free manner.
One simple solution is to represent each chopstick by a semaphore. A philosopher tries to grab the 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.
Although this solution guarantees that no two neighbors are eating simultaneously, it nevertheless must be
rejected because it has the possibility of creating a deadlock. Suppose that all five philosophers become hungry
simultaneously, 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.
Use an asymmetric solution; that is, an odd philosopher picks up first her left chopstick and then her right
chopstick, whereas an even philosopher picks up her right chopstick and then her left chopstick.Finally, any satisfactory
solution to the dining-philosophers problem must guard against the possibility that one of the philosophers will starve to
death.
Monitors
Another high-level synchronization construct is the monitor type. A monitor is characterized by a set of
programmer-defined operators. The representation of a monitor type consists of declarations of variables whose values
define the state of an instance of the type, as well as the bodies of procedures or functions that implement operations
on the type. The syntax of a monitor is
.
The representation of a monitor type cannot be used directly by the various processes. Thus, a procedure defined within
a monitor can access only those variables declared locally within the monitor and its formal parameters. Similarly, the
local variables of a monitor can be accessed by only the local procedures.
The monitor construct ensures that only one process at a time can be active within the monitor. Consequently, the
programmer does not need to code this synchronization constraint explicitly. However, the monitor construct, as defined
so far, is not sufficiently powerful for modeling some
synchronization schemes. For this purpose, we need to define additional synchronization mechanisms. These
mechanisms are provided by the condition operations construct. A programmer who needs to write her own tailor-made
synchronization scheme can define one or more variables of type condition: condition x, y;
The only operations that can be invoked on a condition variable are wait and signal. The operation means that the process
invoking this operation is suspended until another process invokes
The x . signal (1 operation resumes exactly one suspended process. If no process is suspended, then the signal operation
has no effect; that is, the state of x is as though the operation were never executed (Figure 7.21). Contrast this operation
with the signal operation associated with semaphores, which always affects the state of the semaphore. Now suppose
that, when the x. signal () operation is invoked by a process P, there is a suspended process Q associated with condition
x. Clearly, if the operations suspended process Q is allowed to resume its execution, the signaling process P must wait.
Otherwise, both P and Q will be active simultaneously within the monitor. Note, however, that both processes can
conceptually continue with their execution. Two possibilities exist:
1. P either waits until Q leaves the monitor, or waits for another condition.
2. Q either waits until P leaves the monitor, or waits for another condition.
There are reasonable arguments in favor of adopting either option 1 or option 2. Since P was already executing
in the monitor, choice 2 seems more reasonable. However, if we allow process P to continue, the "logical" condition for
which Q was waiting may no longer hold by the time Q is resumed. Choice 1 was advocated by Hoare, mainly because
the preceding argument in favor of it translates directly to simpler and more elegant proof rules. A compromise between
these two choices was adopted in the language Concurrent C. When process P executes the signal operation, process Q
is immediately resumed. This model is less powerful than Hoare's, because a process cannot signal more than once during
a single procedure call.
Deadlocks
A set of process is in a deadlock state if each process in the set is waiting for an event that can be caused by only
another process in the set. In other words, each member of the set of deadlock processes is waiting for a resource that
can be released only by a deadlock process. None of the processes can run, none of them can release any resources, and
none of them can be awakened.
The resources may be either physical or logical. Examples of physical resources are Printers, Hard Disc Drives,
Memory Space, and CPU Cycles. Examples of logical resources are Files, Semaphores, and Monitors.
The simplest example of deadlock is where process 1 has been allocated non-shareable resources A (say a Hard
Disc drive) and process 2 has be allocated non-sharable resource B (say a printer). Now, if it turns out that process 1
needs resource B (printer) to proceed and process 2 needs resource A (Hard Disc drive) to proceed and these are the only
two processes in the system, each is blocked the other and all useful work in the system stops. This situation is termed
deadlock. The system is in deadlock state because each process holds a resource being requested by the other process
neither process is willing to release the resource it holds.
Coffman (1971) identified four conditions that must hold simultaneously for there to be a deadlock.
1. Mutual Exclusion Condition: The resources involved are non-shareable.
Resource-Allocation Graph
Deadlocks can be described in terms of a directed graph called a system resource-allocation graph.
This graph consists of a set of vertices V and a set of edges E. The set of vertices V is partitioned into two different
types of nodes P = {PI, P2, ..., Pn}, the set consisting of all the active processes in the system, and R = {R1, R2, ..., Rm}, the
set consisting of all resource types in the system.
A directed edge from process Pi to resource type Rj is denoted by Pi Rj; it signifies that process Pi requested an
instance of resource type Rj and is currently waiting for that resource. A directed edge Pi Rj is called a request edge.
A directed edge from resource type Rj to process Pi is denoted by Rj Pi; it signifies that an instance of resource
type Rj has been allocated to process Pi. A directed edge Rj Pi is called an assignment edge.
Pictorially, we represent each process Pi as a circle and each resource type Rj as a square. Since resource type Rj
may have more than one instance, we represent each such instance as a dot within the square. A request edge points to
only the square Rj, whereas an assignment edge must designate one of the dots in the square.
The resource-allocation graph shown below depicts the following situation. The sets
P, R, and E:
P={P1,P2,P3}
R={R1,R2,R3,R4}
E={P1→R1, P2→R3, R1→P2, R2→P2, R2→P1, R3→P3} Resource
instances:
• One instance of resource type R1
• Two instances of resource type R2
• One instance of resource type R3
• Three instances of resource type R4 Process states:
• Process PI is holding an instance of resource type R2, and is waiting for an instance of resource type R1.
• Process P2 is holding an instance of R1 and R2, and is waiting for an instance of resource type R3.
• Process P3 is holding an instance of R3.
Given the definition of a resource-allocation graph, it can be shown that, if the graph contains no cycles, then no
process in the system is deadlocked. If the graph does contain a cycle, then a deadlock may exist.
If each resource type has exactly one instance, then a cycle implies that a deadlock has occurred. If the cycle
involves only a set of resource types, each of which has only a single instance, then a deadlock has occurred. Each process
involved in the cycle is deadlocked. In this case, a cycle in the graph is both a necessary and a sufficient condition for the
existence of deadlock.
If each resource type has several instances, then a cycle does not necessarily imply that a deadlock has occurred.
In this case, a cycle in the graph is a necessary but not a sufficient condition for the existence of deadlock.
To illustrate this concept, let us return to the resource-allocation graph depicted in Figure. Suppose that process
P3 requests an instance of resource type R2. Since no resource instance is currently available, a request edge P3→ R2 is
added to the graph. At this point, two minimal cycles exist in the system:
DEADLOCK PREVENTION
DEADLOCK AVOIDANCE
This approach to the deadlock problem anticipates deadlock before it actually occurs. This approach employs an
algorithm to access the possibility that deadlock could occur and acting accordingly. If the necessary conditions for a
deadlock are in place, it is still possible to avoid deadlock by being careful when resources are allocated. It employs the
most famous deadlock avoidance algorithm that is the Banker’s algorithm.
A deadlock-avoidance algorithm dynamically examines the resource-allocation state to ensure that a circular
wait condition can never exist. The resource-allocation state is defined by the number of available and allocated
resources, and the maximum demands of the processes.
A system is said to be in a Safe State, if there is a safe execution sequence. An execution sequence is an ordering
for process execution such that each process runs until it terminates or blocked and all request for resources are
immediately granted if the resource is available.
A system is said to be in an Unsafe State, if there is no safe execution sequence. An unsafe state may not be
deadlocked, but there is at least one sequence of requests from processes that would make the system deadlocked.
The deadlock avoidance algorithm uses a variant of the resource-allocation graph to avoid deadlocked state. It
introduces a new type of edge, called a claim edge. A claim edge Pi Rj indicates that process Pi may request resource Rj
at some time in the future. This edge resembles a request edge in direction, but is represented by a dashed line. When
process Pi requests resource Rj, the claim edge Pi Rj is converted to a request edge. Similarly, when a resource Rj is
released by Pi, the assignment edge Rj Pi is reconverted to a claim edge Pi Rj.
Banker's algorithm
The Banker's algorithm is a resource allocation & deadlock avoidance algorithm developed by Edsger Dijkstra
that test for safety by simulating the allocation of pre-determined maximum possible amounts of all resources. Then it
makes a "safe-state" check to test for possible deadlock conditions for all other pending activities, before deciding
whether allocation should be allowed to continue.
The Banker's algorithm is run by the operating system whenever a process requests resources. The algorithm
prevents deadlock by denying or postponing the request if it determines that accepting the request could put the system
in an unsafe state (one where deadlock could occur).
1. request ≤ max, else set error as process has crossed maximum claim made by it.
2. request ≤ available, else process waits until resources are available.
Several data structures must be maintained to implement the banker's algorithm. These data structures encode the
state of the resource-allocation system. Let n be the number of processes in the system and m be the number of resource
types. We need the following data structures:
Available: A vector of length m indicates the number of available resources of each type. If Available[j] = k, there are k
instances of resource type Rj available.
Max: An n x m matrix defines the maximum demand of each process. If Max[i,j] = k, then process Pi may request at most k
instances of resource type Rj.
Allocation: An n x m matrix defines the number of resources of each type currently allocated to each process. If
Allocation[i,j] = k, then process Pi is currently allocated k instances of resource type Rj.
Need: An n x m matrix indicates the remaining resource need of each process. If Need[i,j] = k, then process Pi may need k
more instances of resource type Ri to complete its task. Note that Need[i,j] = Max[i,j] - Allocafion[i,j].
These data structures vary over time in both size and value. The vector Allocationi specifies the resources
currently allocated to process Pi; the vector Needi specifies the additional resources that process Pi may still request to
complete its task.
Safety Algorithm
The algorithm for finding out whether or not a system is in a safe state can be described as follows:
1. Let Work and Finish be vectors of length m and n, respectively. Initialize
Work := Available and Finisk[i] :=false for i = 1,2, ..., n. 2. Find an i such that
both a. Finisk[i] =false
b. Needi < Work.
If no such i exists, go to step 4.
3. Work := Work + Allocationi Finisk[i] := true go to step 2.
4. If Finish[i] = true for all i, then the system is in a safe state.
This algorithm may require an order of m x n2 operations to decide whether a state is safe.
Let Requesti be the request array for process Pi. Requesti [j] = k means process Pi wants k instances of resource
type Rj. When a request for resources is made by process Pi, the following actions are taken:
Consider a system with five processes P0 through P4 and three resource types A,B,C. Resource type A has 10
instances, resource type B has 5 instances, and resource type C has 7 instances. Suppose that, at time T0, the following
Q.2: Is the system in a safe state? If Yes, then what is the safe sequence?
Applying the Safety algorithm on the given system,
We must determine whether this new system state is safe. To do so, we again execute Safety algorithm on the above data
structures.
We claim that the system is currently in a safe state. Indeed, the sequence <PI, P3, P4, P2, P0> satisfies the safety criteria.
Suppose now that process P1 requests one additional instance of resource type A and two instances of resource type C,
We must determine whether this new system state is safe. To do so, we execute our safety algorithm and find that the
sequence <PI, P3, P4, P0, P2> satisfies our safety requirement. Hence, we can immediately grant the request of process
PI.
However, that when the system is in this state, a request for (3,3,0) by P4 cannot be granted, since the resources
are not available. A request for (0,2,0) by Po cannot be granted, even though the resources are available, since the
resulting state is unsafe.
DEADLOCK DETECTION
If a system does not employ either a deadlock-prevention or a deadlock avoidance algorithm, then a deadlock situation
may occur. In this environment, the system must provide:
• An algorithm that examines the state of the system to determine whether a deadlock has occurred.
• An algorithm to recover from the deadlock.
According to number of instances in each resource type, the Deadlock Detection algorithm can be classified into two
categories as follows:
1. Single Instance of Each Resource Type: If all resources have only a single instance, then it can define a deadlock
detection algorithm that uses a variant of the resource-allocation graph (is called a wait-for graph). A wait–for graph
can be draw by removing the nodes of type resource and collapsing the appropriate edges from the resource-allocation
graph.
An edge from Pi to Pj in a wait-for graph implies that process Pi is waiting for process Pj to release a resource
that Pi needs. An edge Pi Pj exists in a wait-for graph if and only if the corresponding resource allocation graph contains
two edges Pi Rq and Rq Pj for some resource Rq. For Example:
A deadlock exists in the system if and only if the wait-for graph contains a cycle. To detect deadlocks, the system
needs to maintain the wait-for graph and periodically to invoke an algorithm that searches for a cycle in the graph. An
algorithm to detect a cycle in a graph requires an order of n 2 operations, where n is the number of vertices in the graph.
2. Several Instances of a Resource Type: The following deadlock-detection algorithm is applicable to several instance of
a resource type. The algorithm employs several time-varying data structures:
Available: A vector of length m indicates the number of available resources of each type.
Allocation: An n x m matrix defines the number of resources of each type currently allocated to each process. Request:
An n x m matrix indicates the current request of each process. If Request[i,j] = k, then process Pi is requesting k more
instances of resource type Rj.
When a detection algorithm determines that a deadlock exists, then the system or operator is responsible for
handling deadlock problem. There are two options for breaking a deadlock.
1. Process Termination 2.
Resource preemption
3.
Process Termination
Resource Preemption
In resource preemption, the operator or system preempts some resources from processes and give these
resources to other processes until the deadlock cycle is broken.
If preemption is required to deal with deadlocks, then three issues need to be addressed:
1. Selecting a victim: The system or operator selects which resources and which processes are to be preempted based
on cost factor.
2. Rollback: The system or operator must roll back the process to some safe state and restart it from that state.
3. Starvation: The system or operator should ensure that resources will not always be preempted from the same process?
cases, such as error routines. ⇒ Dynamic loading does not require special support from the operating
system.
Dynamic Linking
⇒ Linking postponed until execution time.
⇒ Small piece of code (stub) used to locate the appropriate memory-resident library routine.
⇒ Stub replaces itself with the address of the routine and executes the routine.
⇒ Operating system needed to check if routine is in processes memory address. ⇒
Dynamic linking is particularly useful for libraries.
of memory swapped. ⇒ Modified versions of swapping are found on many systems (UNIX, Linux, and
Windows).
MEMORY ALLOCATION
The main memory must accommodate both the operating system and the various user
processes. We need to allocate different parts of the main memory in the most efficient way possible.
The main memory is usually divided into two partitions: one for the resident operating
system, and one for the user processes. We may place the operating system in either low memory or
high memory. The major factor affecting this decision is the location of the interrupt vector. Since
the interrupt vector is often in low memory, programmers usually place the operating system in low
memory as well.
There are following two ways to allocate memory for user processes:
1. Contiguous memory allocation
According to size of partitions, the multiple partition schemes are divided into two types:
i. Multiple fixed partition/ multiprogramming with fixed task(MFT)
ii. Multiple variable partition/ multiprogramming with variable task(MVT)
i. Multiple fixed partitions: Main memory is divided into a number of static partitions at system
generation time. In this case, any process whose size is less than or equal to the partition size can be
loaded into any available partition. If all partitions are full and no process is in the Ready or Running
state, the operating system can swap a process out of any of the partitions and load in another
process, so that there is some work for the processor.
Advantages: Simple to implement and little operating system overhead.
Disadvantage: * Inefficient use of memory due to internal fragmentation.
* Maximum number of active processes is fixed.
ii. Multiple variable partitions: With this partitioning, the partitions are of variable length and
number. When a process is brought into main memory, it is allocated exactly as much memory as it
requires and no more.
Advantages: No internal fragmentation and more efficient use of main memory.
Disadvantages: Inefficient use of processor due to the need for compaction to counter external
fragmentation. Partition Selection policy:
When the multiple memory holes (partitions) are large enough to contain a process, the operating
system must use an algorithm to select in which hole the process will be loaded. The partition
selection algorithm are as follows:
⇒ First-fit: The OS looks at all sections of free memory. The process is allocated to the first hole found
that is big enough size than the size of process.
Where p is an index into the page table and d is the displacement within the page.
Example:
Consider a page size of 4 bytes and a
physical memory of 32 bytes (8 pages), we
show how the user's view of memory can
be mapped into physical memory. Logical
address 0 is page 0, offset 0. Indexing into
the page table, we find that page 0 is in
frame 5. Thus, logical address 0 maps to
physical address 20 (= (5 x 4) + 0). Logical
address 3 (page 0, offset 3) maps to
physical address 23 (= (5 x 4) + 3). Logical
address 4 is page 1, offset 0; according to
the page table, page 1 is mapped to frame
6. Thus, logical address 4 maps to physical
address 24 (= (6 x 4) + 0). Logical address
13 maps to physical address 9(= (2 x 4)+1).
Each operating system has its own methods for storing page tables. Most operating systems allocate a
page table for each process. A pointer to the page table is stored with the other register values (like the
instruction counter) in the process control block. When the dispatcher is told to start a process, it must
reload the user registers and define the correct hardware page table values from the stored user page
table. Implementation of Page Table
⇒ Generally, Page table is kept in main memory. The Page Table Base Register (PTBR) points to the
page table. And Page-table length register (PRLR) indicates size of the page table.
2. Hashed Page Tables: This scheme is applicable for address space larger than 32bits. In this
scheme, the virtual page number is hashed into a page table. This page table contains a chain of
elements hashing to the same location. Virtual page numbers are compared in this chain
searching for a match. If a match is found, the corresponding physical frame is extracted.
Shared Pages
Shared code
⇒ One copy of read-only (reentrant) code shared among processes (i.e., text editors, compilers,
window systems).
⇒ Shared code must appear in same location in the logical address space of all processes.
Private code and data
⇒ Each process keeps a separate copy of the code and data.
⇒ The pages for the private code and data can appear anywhere in the logical address space.
The segment number is used as an index into the segment table. The offset d of the logical
address must be between 0 and the segment limit. If it is not, we trap to the operating system that
logical addressing attempt beyond end of segment. If this offset is legal, it is added to the segment
base to produce the address in physical memory of the desired byte. Consider we have five segments
numbered from 0 through 4. The segments are stored in physical memory as shown in figure. The
segment table has a separate entry for each segment, giving start address in physical memory (or
base) and the length of that segment (or limit). For example, segment 2 is 400 bytes long and begins
at location 4300. Thus, a reference to byte 53 of segment 2 is mapped onto location 4300 + 53 =
4353.
VIRTUAL MEMORY
Virtual memory is a technique that allows the execution of processes that may not be
completely in memory. Only part of the program needs to be in memory for execution. It means that
Logical address space can be much larger than physical address space. Virtual memory allows
processes to easily share files and address spaces, and it provides an efficient mechanism for process
creation.
Virtual memory is the separation of user logical memory from physical memory. This
separation allows an extremely large virtual memory to be provided for programmers when only a
smaller physical memory is available. Virtual memory makes the task of programming much easier,
because the programmer no longer needs to worry about the amount of physical memory available.
When a page references an invalid page, then it is called Page Fault. It means that page is not in
main memory. The procedure for handling page fault is as follows:
1. We check an internal table for this process, to determine whether the reference was a valid
or invalid memory access.
2. If the reference was invalid, we terminate the process. If it was valid, but we have not yet
brought in that page in to memory.
3. We find a free frame (by taking one from the free-frame list).
4. We schedule a disk operation to read the desired page into the newly allocated frame.
5. When the disk read is complete, we modify the internal table kept with the process and the
page table to indicate that the page is now in memory.
6. We restart the instruction that was interrupted by the illegal address trap. The process can
now access the page as though it had always been in memory.
PAGE REPLACEMENT
The page replacement is a mechanism that loads a page from disc to memory when a page of
memory needs to be allocated. Page replacement can be described as follows:
1. Find the location of the desired page on the disk.
2. Find a free frame:
a. If there is a free frame, use it.
b. If there is no free frame, use a page-replacement algorithm to select a victim frame.
c. Write the victim page to the disk; change the page and frame tables accordingly.
3. Read the desired page into the (newly) free frame; change the page and frame tables.
4. Restart the user process.
(Thrashing)