You are here: Home » Content » Review question: Memory management
Quality
Affiliated with  (?)
This content is either by members of the organizations listed or about topics related to the organizations listed. Click each link to see a list of all content affiliated with the organization.
Lenses
Tags  (?)
These tags come from the endorsement, affiliation, and other lenses that include this content.

Review question: Memory management

Module by: Duong Anh Duc

Summary: Review question of Memory management

When synchronizing threads, why is it important that we make no assumptions about the relative speeds of the threads? Give an example of when such an assumption would help you to create an algorithm for mutual exclusion.

We can't make an assumption about the relative speeds of threads because the operating system can schedule them in any order and for any length of time. The actual speed will depend on the requirements of other processes in the system and the scheduling algorithm used by the operating system. If we base our synchronization on the speed of a thread, and its actual speed differs, then mutual exclusion could be violated.
If we could guarantee that one thread will run slower than another, then we would not need a synchronization mechanism. We would know that one thread would be done with a shared memory region before the other needed it. Another example is the fourth attempt to do software synchronization, given on page 201 of Stallings. In this example, two processes each check if the other wants to enter the critical section. If so, then they will delay for a period of time and retry. If we can guarantee that one will delay longer than the other, then this solution will work. However, it is possible that they will delay for the same time and collide again.

What are the advantages and disadvantages of using a hardware test-and-set instruction for mutual exclusion?

The main advantages of using test-and-set are that it works for any number of processes on a shared memory machine and it is easy to use and to verify. The main disadvantages are that it uses busy waiting and it can lead to starvation (waiting processes do not use a queue).

Briefly explain the producer/consumer problem and the readers/writers problem.

In the producer/consumer problem, a producer creates items and a consumer uses them. The items are stored in a shared buffer, which can be infinite or of a limited size. The producer and consumer must synchronize on the buffer contents so that items are not lost or consumed more than once. If the buffer is empty, the consumer must wait for the producer to create a new item. If a finite buffer is full, the producer must wait for the consumer to use an item.
In the readers/writers problem, multiple readers may access a shared memory location. However, only one writer may write to the memory location at a time, and no readers may be active while the write occurs. The solution may give priority to either readers or writers.

Explain the differences between the Hoare and the Lampson/Redell monitors.

The difference between these two monitors lies in how they handle a signal from a process that is currently in the monitor. In the Hoare definition, if there is some process waiting on the signal, then the signaling process must either exit the monitor immediately or be suspended within the monitor. If the signaling process does not exit, it is suspended and placed on an urgent queue. Control is then turned over to a process that is waiting for the signal.
In the Lampson/Redell monitor, the signal() primitive is replaced with notify(). With notify(), the condition queue is informed that the condition has occurred, but the notifying process continues. Once the notifying process leaves the monitor, then a process in the condition queue may enter. This process MUST recheck the condition it was waiting for, since it may no longer be true. If the condition is not true, it must wait again; otherwise it may proceed. The Lampson/Redell monitor also has a broadcast notification, which wakes up all processes waiting on a condition.

Why is message passing particularly useful for a distributed system?

Message passing is useful for a distributed system because the system does not have shared memory among the processors. Synchronization among the processors will require some form of message passing between them. Some other synchronization methods, such as semaphores and monitors, may still be used, but they will need to be implemented with messages (i.e. sent to a master processor in control of the shared memory location). The hardware synchronization methods are not applicable, as interrupts and atomic instructions work only on a single processor.
Show that the four conditions of deadlock apply to Figure 6.1a.
Figure 1
  • mutual exclusions - only one car can be in a part of the intersection at a time
  • hold and wait - a car may take one piece of the intersection and wait for another
  • no preemption - it is not possible to move a car once it has obtained a piece of the intersection
  • circular wait - a closed chain of cars waiting for each other exists

Briefly explain the necessary conditions for deadlock. For each of these conditions, explain how can deadlock be avoided by preventing the condition from occurring.

  • Mutual exclusion. Only one process at a time may access the critical section. It is not possible to avoid this condition without causing an error.
  • Hold-and-wait. A process may hold one resource while waiting for another. You can prevent this condition from occurring by requiring a process to acquire all resources it will need before it starts. Deadlock cannot occur since no process can wait for resources while it is running.
  • No preemption. A process may not be forced to relinquish a resource it is holding. If you allow preemption, then you can avoid deadlock by taking away resources from one process to satisfy a request by another process.
  • Circular wait. A chain of requests exists such that each process requests a resource that is held by the next process in the chain. If you impose an ordering on the resources, you can prevent deadlock since a chain cannot form.

Consider the following ways of handling deadlock: (1) banker's algorithm, (2) detect deadlock and kill thread, releasing all resources, (3) reserve all resources in advance, (4) restart thread and release all resources if thread needs to wait, (5) resource ordering, and (6) detect deadlock and roll back thread's actions.

  • One criterion to use in evaluating different approaches to deadlock is which approach permits the greatest concurrency. In other words, which approach allows the most threads to make progress without waiting when there is no deadlock. Give a rank order from 1 to 6 for each of the ways of handling deadlock just listed, where 1 allows the greatest degree of concurrency. Comment on your ordering.
I would rank these as:
  • deadlock detect and roll back thread's actions
  • deadlock detect and kill thread, releasing all resources
These two come first, because they let processes make requests as they want, and only later checks for deadlocks. If there is no deadlock, then processes are not affected. I rank rollback ahead of killing a thread, because it allows the thread to progress more.
  • banker's algorithm
This comes next since it checks every request. This will slow down processes if there is no deadlock, but will otherwise let them run as they need resources.
  • resource ordering
  • reserve all resources in advance
These prevent some threads from running when they otherwise could. I rank resource ordering ahead because a thread can progress with just a few resources. Requiring a thread to request everything in advance will result in fewer processes being able to run at a time.
  • restart thread and release all resources if thread needs to wait
I rank this last because the system will really slow down if any process that needs to wait (even if there is no deadlock!) has to start over again. In a busy system, it could be that no work ever gets done!
Another criterion is efficiency; in other words, which requires the least processor overhead. Rank order the approaches from 1 to 6, with 1 being the most efficient, assuming that deadlock is a very rare event. Comment on your ordering.
  • resource ordering
  • reserve all resources in advance
These come first since the restrictions can be made at compile time and no extra work is needed.
  • deadlock detect and roll back thread's actions
  • deadlock detect and kill thread, releasing all resources
These come in a close second because the deadlock detection can be done only once in a long time, since deadlock is assumed to be rare.
  • Restart thread and release all resources if thread needs to wait
This is pretty bad, since the processor has to do a lot of work to restart a process, and it will be doing this any time several processes request the same resource.
  • Banker's algorithm
This comes last, since some work must be done for every resource request, even if deadlock is rare.
  • Does your ordering from (b) change if deadlocks occur frequently?
If deadlocks are frequent, then deadlock detection may slip a notch to 5, but it should still be cheaper than running the banker's algorithm for every request.

Problems

Consider the following program:

const int n = 50;
int tally;
void total()
{
int count
for (count = 1; count <= n; count++)
{
tally++;
}
}
void main()
{
tally = 0;
parbegin(total(),total());
write(tally);
}
  • Determine the proper lower bound and upper bound on the final value of the shared variable tally output by this concurrent program. Assume processes can execute at any relative speed and that a value can only be incremented after it has been loaded into a register by a separate machine instruction.
The important statement to consider is tally := tally + 1. According to the question, the value of tally is first loaded into a register by a separate machine instruction, then incremented and stored back into the memory location given by tally. This means two processes can each load tally into their own registers (remember, these are stored as part of the context of a process and NOT shared between processes), then increment and store.
The following can result in a count of 50: P1 reads tally, P2 reads tally, P1 increments and stores, then P2 increments and stores. If this is repeated 50 times, the tally will be 50, since P2's store will overwrite P1's.
This is not the lower bound, however. It is possible for a count of 2 to occur:
  • P1 loads tally to a register.
  • P2 loads tally (still zero) and increments 49 times, stores 49.
  • P1 increments its register (zero) and stores 1 in tally.
  • P2 reads tally (now 1) and stores it in a register.
  • P1 loads tally (still 1) and increments it 49 times, stores 50.
  • P2 increments tally, stores 2.
The upper bound of 100 will occur if, for example, P1 always does the entire load, increment, and store before P2.
  • Suppose that an arbitrary number of these processes are permitted to execute in parallel under the assumption of part (a). What effect will this modification have on the range of final values of tally?
If there are N concurrent processes, then the lower bound is unaffected and the upper bound is N*50.

Consider the following program:

boolean blocked[2];
int turn;
void P(int id)
{
while(true)
{
blocked[id] = true;
while(turn != id)
{
while(blocked[1-id])
/* do nothing */;
turn = id;
}
/* critical section */
blocked[id] = false;
/* remainder */
}
}
void main()
{
blocked[0] = false;
blocked[1] = false;
turn = 0;
parbegin(P(0), P(1));
}
This software solution to the mutual exclusion program for two processes is proposed in [HYMA66]. Find a counterexample that demonstrates that this solution is incorrect. It is interesting to note that even the Communications of the ACM was fooled on this one.
Process P1 enters procedure P first and sets blocked[1] to true. Since turn = 0, it enters the first while loop. It does not enter the second while loop because blocked[0] is false. P1 is suspended and the OS runs P0. P0 enters procedure P, sets blocked[0] to true, and checks that turn = 0. It thus skips the first while loop and enters the critical region. While in the critical region, P0 is suspended and the OS runs P1 again. P1 has just finished checking that blocked[0] is false, but it is now true! P1 doesn't know this, sets turn = 1, and enters the critical section. Both processes are now in the critical section.

It should be possible to implement general semaphores using binary semaphores. We can use the operations semWaitB and semSignalB and two binary semaphores, delay and mutex. Consider the following:

void semWait(semaphore s)
{
semWaitB(mutex);
s--;
if (s < 0)
{
semSignalB(mutex);
semWaitB(delay);
}
else
semSignalB(mutex);
}
void semSignal(semaphore s)
{
semWaitB(mutex);
s++;
if (s <= 0)
semSignalB(delay);
semSignalB(mutex);
}
Initially, s is set to the desired semaphore value. Each semWait operation decrements s, and each semSignal operation increments s. The binary semaphore mutex, which is initialized to 1, assures that there is mutual exclusion for the updating of s. The binary semaphore delay, which is initialized to 0, is used to b lock processes.
There is a flaw in the preceding program. Demonstrate the flaw and propose a change that will fix it. Hint: Suppose two processes each call semWait(s) when s is initially 0, and after the first has just performed semSignalB(mutex) but not performed semWaitB(delay), the second call to semWait(s) proceeds to the same point. All that you need to do is move a single line of the program.
This problem is hard! We will score it as extra credit for those who get it right.
Set the semaphore s to 2. Two processes, P0 and P1, enter Wait() and the semaphore is decremented to 0. Both are suspended in the critical section. P2 enters Wait(), decrements s to -1, and is suspended just before it can execute WaitB(delay). P3 enters Wait(), decrements s to -2, and is also suspended just before it can execute WaitB(delay). P0 is activated, enters Signal(), increments s to -1, and executes SignalB(delay). This sets the binary semaphore delay to 1. P0 leaves Signal(). P1 is activated, enters Signal(), increments s to 0, and executes SignalB(delay). This has no effect, since delay is already 1; in effect, this signal is lost!! Now P2 is activated, executes WaitB(delay), sets delay to 0, and enters the critical section. Process P3 is now activated, executes WaitB(delay), and is suspended on the queue for the delay semaphore.
We now have a condition where s is 0, so there should be two processes in the critical section. However, since one of the signals was lost, there is only one process in the critical section. In addition, when P2 executes Signal(), it will increment s to 1, so it will not wake up P3. There is a possibility P3 will never wake up!
To fix this problem, remove just the word "else" from Wait() and place it before SignalB(mutex) in Signal(). Then, when a process executes SignalB(delay) in Signal(), it will not unlock mutex. No other process can execute Wait() or Signal() until a process is taken off the waiting queue for the delay semaphore. This process will then execute SignalB(mutex) from within Wait(), and operation can continue. In effect, we have ensured that no signals on the delay semaphore will be lost.

Jurassic Park consists of a dinosaur museum and a park for safari riding. There are m passengers and n single-passenger cars. Passengers wander around the museum for a while, then line up to take a ride in a safari car. When a car is available, it loads the one passenger it can hold and rides around the park for a random amount of time. If the n cars are all out riding passengers around, then a passenger who wants to ride waits; if a car is ready to load but there are no waiting passengers, then the car waits. Use semaphores to synchronize the m passenger processes and the n car processes.

The following skeleton code was found on a scrap of paper on the floor of the exam room. Grade it for correctness. Ignore syntax and missing variable declarations. Remember that P and V correspond to semWait and semSignal.
resource Jurassic_Park()
sem car_avail := 0;
sem car_taken := 0;
sem car_filled := 0;
sem passenger_released := 0;
process passenger(i := 1 to num_passengers)
do true ->
nap(int(random(1000*wander_time)));
P(car_avail);
V(car_taken);
P(car_filled);
P(passenger_released);
od
end passenger
process car(j := 1 to num_cars)
do true ->
V(car_avail);
P(car_taken);
V(car_filled);
nap(int(random(1000*ride_time)));
V(passenger_released);
od
end car
end Jurassic_park
The code has a major problem. A car can release a passenger that is NOT the passenger riding inside its car! This is because the V(passenger_released) in one car can match up with a P(passenger_released) for a passenger of another car. Fixing this requires using an array of semaphores, one per car or one per passenger, similar to the barbershop problem.

Show that message passing and semaphores have equivalent functionality by

  • Implementing message-passing using semaphores. Hint: Make use of a shared buffer area to hold mailboxes, each one consisting of an array of message slots.
Here is one way of doing this:
send(message,dest) {
wait(size[dest]);
wait(mutex[dest]);
mailbox[dest].append(message,self);
signal(mutex[dest]);
signal(recv[dest]);
}
receive() {
wait(recv[self]);
wait(mutex[self]);
message = mailbox[self].pop();
signal(mutex[self]);
signal(size[dest]);
return message;
}
In this code, all of the semaphores are stored in arrays indexed by a process identifier, so that each process gets its own mailbox. Size is a counting semaphore that is initialized to the number of messages that can be stored in the mailbox. Recv is a counting semaphore that is initialized to zero and keeps track of the number of messages available in the mailbox. Mutex is a binary semaphore that protects the critical regions where the mailbox array is being accessed.
  • Implementing a semaphore using message passing. Hint: Introduce a separate synchronization process.
Here is one way of doing this:
wait(sem) {
send("wait,sem",sync);
message = receive(sync);
}
signal(sem) {
send("signal,sem",sync);
message = receive(sync);
}
sync.main() {
while (1) {
message,sem = receive(src);
if message == "wait" {
value[sem]--;
if value[sem] < 0
waiting[sem].append(src);
else
send("OK",src);
} else if message == "signal" {
value[sem]++;
if value[sem] <= 0 {
src = waiting[sem].pop();
send("OK",src);
}
}
}
}
In this code, the wait() and signal() functions simply send messages to a synchronization process. The synchronization process causes the wait() to block by not returning a message if the semaphore is < 0. Once a signal() operation increments the semaphore, then a waiting process is woken by sending it a message.

Consider the following snapshot of a system. There are no outstanding unsatisfied requests for resources.

Available
r1 r2 r3 r4
2 1 0 0
  Current Allocation Maximum Demand Still Needs
Process r1 r2 r3 r4 r1 r2 r3 r4 r1 r2 r3 r4
P1 0 0 1 2 0 0 1 2 0 0 0 0
P2 2 0 0 0 2 7 5 0 0 7 5 0
P3 0 0 3 4 6 6 5 6 6 6 2 2
P4 2 3 5 4 4 3 5 6 2 0 0 2
P5 0 3 3 2 0 6 5 2 0 3 2 0
  • Compute what each process still might request and display in the columns labeled "still needs".
See the table above.
  • Is this system currently in a safe or unsafe state? Why?
This system is in a safe state since all processes can run. Here is one order that works:
Run P1, since it doesn't need anything. Add its current allocation back into Available = 2 1 1 2.
Run P4, Available = 4 4 6 6.
Run P5, Available = 4 7 9 8.
Run P2, Available = 6 7 9 8.
Run P3, Available = 6 7 12 12.
Notice that the final value of Available is equal to the sum of the original value of Available plus all of the original Allocations. This is a good check that we have not messed up along the way.
  • Is this system currently deadlocked? Why or why not?
This system is not currently deadlocked because all of the processes can run to completion.
  • Which processes, if any, are or may become deadlocked?
If the execution order given above is followed, then no processes will deadlock. However, with any other pattern of requests it is possible for the proccesses to deadlock. For example, see the next part.
  • If a request from P3 arrives for (0,1,0,0), can that request be safely granted immediately? In what state (deadlock, safe, unsafe) would immediately granting that whole request leave the system? Which processes, if any, are or may become deadlocked if this whole request is granted immediately?
If P3 requests (0,1,0,0) and this request is granted, then the state of the system would be:
Available
r1 r2 r3 r4
2 0 0 0
  Current Allocation Maximum Demand Still Needs
Process r1 r2 r3 r4 r1 r2 r3 r4 r1 r2 r3 r4
P1 0 0 1 2 0 0 1 2 0 0 0 0
P2 2 0 0 0 2 7 5 0 0 7 5 0
P3 0 1 3 4 6 6 5 6 6 5 2 2
P4 2 3 5 4 4 3 5 6 2 0 0 2
P5 0 3 3 2 0 6 5 2 0 3 2 0
Then P1 can run, Available = 2 0 1 2.
Then P4 can run, Available = 4 3 6 6.
Then P5 can run, Available = 4 6 9 8.
But neither P2 nor P3 can run. So if we grant the request, the system will be left in an unsafe state. Processes P2 and P3 may become deadlocked.

Apply the deadlock detection algorithm of Section 6.4 to the following data and show the results:

Available = ( 2 1 0 0 )
| 2 0 0 1 | | 0 0 1 0 |
Request = | 1 0 1 0 |Allocation = | 2 0 0 1 |
| 2 1 0 0 | | 0 1 2 0 |
The Request matrix represents the needs (or claim) of each process. Process P3 has a claim that is less than Available, so it can run and we then set Available to (2,2,2,0). Process P2 can now run and we set Available to (4,2,2,1). Process P1 can now run and Available is finally (4,2,3,1). The system is not deadlocked.

Consider a system with a total of 150 units of memory, allocated to three processes as shown:

Process Max Hold
1 70 45
2 60 40
3 60 15
Apply the banker's algorithm to determine whether it would be safe to grant each of the following requests. If yes, indicate a sequence of terminations that could be guaranteed possible. If no, show the reduction of the resulting allocation table.
  • A fourth process arrives, with a maximum memory need of 60 and an initial need of 25 units.
We can represent the system by:
Process Max Hold Need
1 70 45 25
2 60 40 20
3 60 15 45
4 60 25 35
Available = 150 - 45 - 40 - 15 - 25 = 25.
P1 can run, Available = 70.
P2 can run, Available = 110.
P3 can run, Available = 125.
P4 can run, Available = 150.
It is safe to grant the request.
  • A fourth process arrives, with a maximum memory need of 60 and an initial need of 35 units.
We can represent the system by:
Process Max Hold Need
1 70 45 25
2 60 40 20
3 60 15 45
4 60 35 25
Available = 150 - 45 - 40 - 15 - 35 = 15.
It is not safe to grant the request, because no process can run with only 15 units of memory available.

Comments, questions, feedback, criticisms?

Send feedback