TODO Merge this into the correct chapters.
TODO
Typedef struct {
int value;
struct process *list;
} semaphore;
wait(semaphore *S) {
S -> value--;
if (S -> value < 0) {
// add this process to S -> list
sleep();
}
}
signal(semaphore *S) {
if (S -> value <= 0) {
// remove process from S -> list
wakeup(P);
}
}Basic system calls used:
Incorrect use of sempahore operations can cause deadlock and starvation.
Example: Misusing Semaphores
Interchanging Order
TODO
Replaces signal with wait
TODO
omits signal, wait, or both
TODO
High-level ADT that encapsulates data along with functions though which the data may be accessed and manipulated
Purpose: Guarantee mutual exclusion.
- Avoids lockout, deadlock, and starvation.
monitor monitor_name
{
// shared vars
procedure P1 (...) {...}
...
procedure Pn (...) {...}
initialization code (...) {...}
}Deadlock: Two or more processes wait indefinitely for an event that can only be caused by only one of the waiting processes.
Starvation: Indefinite blocking
Priority Inversion: Scheduling problem when lower-priority process holds a lock needed by a higher-priority process.
Indivisibility: When we say a critical section must be indivisibly/atomically, we are talking about the code which holds and releases the lock.
- The critical section itself can be big and doesn’t need to be done in a single cycle, it can be preempted safely (though, this can lead to starvation).
Priority Inversion Protocol: We temporarily assign the highest priority to the process currently executing in its critical section to prevent it getting starved.
Suppose you have n buffers, each can hold one item.
TODO Code
do {
...
// produce iteme
...
}Producer:
wait(empty): empty will inform us if there is spacewait(mutex): Will inform us if we can write.signal(mutex): Release lock.signal(full): Increments full.Consumer:
wait(full): Informs us if there is space.wait(mutex): Can we write?TODO Increment and decrement rules?
Idea? — To prevent deadlock, we have each process manage a dedicated boolean? notfull and notempty? How does that owrk?
In this problem, a dataset if shared among a number of concurrent processes.
Problem: How do you let multiple readers read at the same time?
Variations:
Note — Both may have starvation, leading to even more variations.
- On some systems, the kernel provides reader/writer locks to resolve this.
Shared Data:
rw_mutexmutexread_count.read_countThe idea basically, is the first reader will check if it’s safe to get into the CS (wait(rw_mutex)). After this, all the readers won’t have to wait for the rw_mutex, they’ll only ever wait to update the read_count. The last reader will release the lock and allow the writer to do its thing again.
TODO Code
Suppose you have a monitor:
TODO Define reader-writer Monitor
Philosophers spend their lives alternating between thinking and eating.
In the case of 5 philosophers sitting in a circle (with a single chopstick in-between each):
chopstick[5]) initialized to 1Issue: If every philosopher picks up a chopstick, they will get into deadlock.
One naive “solution” is to represent each chopstick with a semaphore.
semaphore chopstick[5];loop {// Waitwait(chopstick[i]); // (left)wait(chopStick[(i \text{$+$} 1) % 5]); // (right)// eat...// Releasesignal(chopstick[i]);signal(chopStick[(i \text{$+$} 1) % 5]);// think...}
Problem: If every single philosopher grabs their left chopstick at once (this is valid), this’ll result in deadlock.
/s — Unrelated, but one optimization is to put “think” at the start of the loop so that the philosophers think before they wait. But personally I don’t want them to think on an empty stomache.
1. Allow at most four philosophers to be sitting simultaneously at the table.
2.
Make the act of picking up both chopsticks a critical section (e.g., wrap in mutex). 1. Obvious Solution Issues: No synchronization (no defense against lockout). 2. This has the same synchronization issue. TODO How does this avoid deadlock(?). 3. TODO Monitor exampleExample: Grab two chopsticks at once.
semaphore chopstick[5]; loop { // Wait P(mutex); wait(chopstick[i]); // (left) wait(chopStick[(i \text{$+$} 1) % 5]); // (right) V(mutex); // eat... // Release signal(chopstick[i]); signal(chopStick[(i \text{$+$} 1) % 5]); // think... } semaphore chopstick[5]; loop { // Wait P(wait(chopstick[min(i, (i \text{$+$} 1) % 5]); P(wait(chopstick[max(i, (i \text{$+$} 1) % 5]); // eat... // Release signal(chopstick[i]); signal(chopStick[(i \text{$+$} 1) % 5]); // think... }
3. Asymmetric; odd-numbered philosophers pick up left-then-right chopstick, evens vice-versa.
4.
When a process is waiting for something that’ll never happens.
Deadlock arises if four conditions hold simultaneously:
Example — Dining Philosophers demonstrates all four properties.
Set of vertieces V and edges E.
Example TODO
If a resource allocation graph has a cycle, it may have a deadlock, specifically:
TODO Graphs
Real-World: #3 is used by most systems (UNIX, Windows, and Linux).
If we can break any of the four conditions, we can prevent deadlocks.
More on Issues:
- Hold and Wait: Low resource utilization, starvation.
- Preemption: This is applied to resources whose states can be easily saved and restored, like CPU registers and memory. But, it can’t be applied to resources like mutex locks and semaphores.
Requests that the system has some additional a priori information available.
Simplest and most-useful model: Each process must declare the maximum number of resources of each type that it may need.
Basically, we must be careful when allocating resources, to prevent circular waits.
On Safe State:
- Safe State: When the system can allocate resource to each process (up to its maximum) in some order and avoid deadlock.
Suppose a system with 12 resources.
| Process | Allocated | Maximum Needs |
|---|---|---|
| T_0 | 5 | 10 |
| T_1 | 2 | 4 |
| T_2 | 2 | 9 |
Our current availability is 12-9=3, but the processes could request up to 14 resources.
Suppose all the processes request their maximum resources.
How can we allocate resources without causing a deadlock?
The crux of it is that we must fulfill processes while thinking about the resources we would get back by getting them to finish.
(The correct way to serve in this situation would be T_1, T_0, T_2)
Use: Single-instances of resources.
TODO Drawing of Resource Allocation Graph with Claim Edge.
We introduce the claim edge, which says a process will use a process in the future.
On Behavior:
- A process request to use its claim at any time, which converts the claim edge into a request edge.
- When a request is served, it is converted to an assignment edge.
- Likewise, when a resource is released, the assignment edge reconverts to a claim.
Central Idea — Requests to convert request edges to assignment edges are only served if they don’t result in the formation of a cycle.
Use: Multiple-instances of resources.
When a process gets all its resources, it must return them in a finite amount of time.
Data Structure: We use an array and three n \times m matrices to do banker’s algorithm.
- Available: Vector of length m. If available[j]=k, there are k instances of resource type R_j available.
- Max: If Max[i,j] = k, then process P_i can request at most k instances of resource type R_j.
- Allocation: TODO
- Need: TODO
- This is merely Max - Allocation (TODO
TODO Algorithm, described using data structure.
Baically, every time a request is made, we run a huge calculation to see if this would leave us in a safe state.
TODO Wait-for graph? Detecting Deadlock??
Danger — Aborting a process, especially in its critical section, is dangerous (e.g., corrupting a file mid-write)
Abort all deadlocked processes. This guarantees the end of deadlock.
You can abort one deadlocked process at a time, checking if the deadlock cycle is eliminated.
Alternatively, you can do resource preemption to break one of the four criteria of deadlock.
Costs — It’s costly to repeatedly:
- Select a victim,
- Rollback to a safe state, and
- Ensure you aren’t starving (repeatedly targeting the same process).