CS452 F23 Lecture Notes
Lecture 18 - 30 Nov 2023
1. Priority Inversions
1.1. Papers
1.2. Priority Inheritance Protocol and Priority Ceiling Protocol
- Basis of PTHREAD-PRIO-PROTECT scheduling
- Periodic task model, as described in the previous lecture
- except jobs can now interact through shared, mutex-protected critical sections
- CS452 kernels don’t have shared critical sections, but they do have message passing
- A CS452 server task is similar to a critical section
- each server request corresponds to the calling process executing the critical section
- sequential nature of the server ensures mutual exclusion (one request at a time)
- A CS452 server task is similar to a critical section
- The analysis in the paper allows for properly nested critical sections
- a job in a critical section can try to enter a different critical section
- in a CS452 kernel, this would correspond to a server making a blocking request to another server as part of handling a request
- or locking a section of track while holding lock on some other section of track
- Shared critical sections (or servers) can result in priority inversions
- Inversion example: (\(J_i\) is a job belonging to task \(\tau_i\), lower subscripts are higher priority)
- \(J_3\) locks mutex and enters critical section
- \(J_1\) starts, preempts \(J_3\), but tries to enter the same critical section as \(J_3\)
- How long will \(J_1\) block?
- arbitrarily long time - not just duration of a single critical section execution
- \(J_1\) could be pre-empted by stream of \(J_2\) jobs, extending its hold on S indefinitely
1.2.1. Basic Priority Inheritance Procotol
- Job in a critical section executes at the highest priority of all jobs it blocks
- Priority Inheritance is transitive
- If \(J_3\) blocks \(J_2\) and \(J_2\) blocks \(J_1\), \(J_3\) inherits \(J_1\) priority
- even if \(J_3\) is blocking \(J_2\) on critical section \(M_a\), and \(J_2\) is blocking \(J_1\) on another critical section \(M_b\)
- example:
- \(J_3\) locks \(M_b\)
- \(J_2\) preempts \(J_3\), locks \(M_a\), then tries to lock \(M_b\) and blocks
- \(J_3\)’s inherits \(J_2\)’s priority
- \(J_1\) preempts \(J_1\) and tries to lock \(M_a\)
- \(J_3\) inherits \(J_1\)’s priority
- If \(J_3\) blocks \(J_2\) and \(J_2\) blocks \(J_1\), \(J_3\) inherits \(J_1\) priority
- Analysis:
- Interested in blocking caused by inversions, i.e., higher priority job forced to wait for lower priority job
- lower priority jobs can always be blocked by higher priority jobs - not considering that here
- Two types of inversion blocking
- direct blocking: earlier example, \(J_1\) cannot enter critical section because \(J_3\) is already in it
- push-through blocking: job blocked by a lower-priority job that has temporarily inherited higher priority
- \(J_3\) in critical section \(M_a\)
- \(J_2\) starts, preempting \(J_3\)
- \(J_1\) starts, preempting \(J_2\)
- \(J_1\) tries to enter \(M_a\), blocks (direct blocking)
- \(J_3\) inherits \(J_1\)’s priorty and runs, even though \(J_2\) is runnable (push through blocking of \(J_2\))
- How much inversion-caused blocking can a job experience?
- \(J_1\) can be blocked by \(J_2\) only if \(J_2\) is in blocking critical section when \(J_1\) starts
- later, \(J_1\) will always be running or blocked by something running at \(J_1\) priority or more
- So, if there are \(n\) jobs with priority less than \(J_i\), \(J_i\) can be blocked for duration of
at most \(n\) critical sections - one per job.
- when \(J_i\) starts, all of those \(n\) jobs must be in critical sections when \(J_i\) starts
- note that \(J_i\) need not even use the critical section to be blocked by it
- because of push through blocking
- consider example in which \(J_2\) is blocked by \(J_3\) even through \(J_2\) doesn’t use any critical sections
- Also, if there are \(m\) semaphores which can block \(J_1\), \(J_1\) can be blocked at most once per semaphore
- So - max blocking is determined by number of blocking semaphores \(m\) and number of lower priority tasks \(n\)
- can do static analysis of semaphore use in jobs to determine number of potential blocking semaphores
- \(J_1\) can be blocked by \(J_2\) only if \(J_2\) is in blocking critical section when \(J_1\) starts
- Interested in blocking caused by inversions, i.e., higher priority job forced to wait for lower priority job
- Problems with the Priority Inheritance procotocl
- Problem 2: long chains
- blocking is bounded, but still have to wait for up to \(\min(m,n)\) critical section lengths in worst case
- worst case: \(J_4\) locks \(M_4\), gets preempted by \(J_3\), which locks \(M_3\), which gets preempted by \(J_2\), which locks \(M_2\)
- now, \(J_1\) preempts \(J_2\), and then tried to access \(M_2\), \(M_3\), and \(M_4\) - will have to wait for each critical section.
- worst case: \(J_4\) locks \(M_4\), gets preempted by \(J_3\), which locks \(M_3\), which gets preempted by \(J_2\), which locks \(M_2\)
- blocking is bounded, but still have to wait for up to \(\min(m,n)\) critical section lengths in worst case
- Problems 1: deadlocks
- \(J_2\) locks critical section \(M_a\), wants to make access to critical section \(M_b\) next
- But it gets preempted by \(J_1\), which locks \(M_b\) and then tries to lock \(M_a\) (deadlock)
- deadlock is not caused by job priorities, but priorities can be used to avoid it
- Problem 2: long chains
1.2.2. Priority Ceiling Protocol
- Like Priority Inheritance Protocol, but try to avoid deadlocks and chains
- priority ceiling of a critical section/mutex is the highest priority of jobs that use that critical section
- The protocol:
- When job \(J\) wants to lock semaphore \(S\), it will block if
- \(S\) is already locked by some other job \(J^*\)
- \(J\) is said to be blocked by \(J^*\)
- \(J\)’s priority is not higher than the highest priority ceiling among all critical sections locked by other jobs
- \(S\) is already locked by some other job \(J^*\)
- this introduces a new type of blocking: ceiling blocking
- job \(J\) can’t run because ceiling test prevents it from acquiring an available semaphore
- \(J\) is said to be blocked by the job \(J^*\) which currently holds the semaphore with the the largest priority ceiling
- If \(J\) gets blocked, the job \(J^*\) that blocks \(J\) inherits \(J\)’s priority until \(J^*\) leaves its critical section
- When job \(J\) wants to lock semaphore \(S\), it will block if
- Priority Ceilings
- to determine ceiling for each critical section, need to know which jobs might use it
- determine by analysis of application
- degenerate case: assume any job might use any critical section
- In this case, when some task is in a critical section, no other task can enter any critical section
- like collapsing all critical sections into one
- In this case, when some task is in a critical section, no other task can enter any critical section
- to determine ceiling for each critical section, need to know which jobs might use it
Example:
Figure 1: PCP Protocol Example (from Priority Inheritance Protocols: An Approach to Real-Time Synchronization)
- What this means:
- Consider all of the critical sections that job \(J_i\) might try to access
- all have a priority ceiling of at least \(i\)
- once some task has locked one of those critical sections:
- no other task can lock another, unless it has priority higher than \(i\)
- if \(J_i\) tries to lock anything, it will ceiling block
- Consider all of the critical sections that job \(J_i\) might try to access
- Properties of this protocol:
- no deadlocks, under arbitrary semaphore accesses with nesting
- Example of deadlock under basic priority inheritance protocol:
- \(J_2\) acquires \(M_A\)
- \(J_1\) starts, preempts \(J_2\), acquires \(M_B\)
- \(J_1\) tries to acquire \(M_A\) and blocks, \(J_2\) inherits its priority
- \(J_2\) tries to acquire \(M_B\) and blocks - DEADLOCK
- Same scenario under the Priority Ceiling Protocol
- both \(M_A\) and \(M_B\) have priority ceiling 1, since \(J_1\) accesses both
- \(J_2\) acquires \(M_A\)
- \(J_1\) starts, preempts \(J_2\), tries to acquire \(M_B\), but ceiling blocking prevents that
- \(J_2\) inherits priority 1, acquires \(M_B\)
- \(J_2\) releases \(M_B\) and \(M_A\) and reverts to priority 1
- \(J_1\) acquires \(M_B\) then \(M_A\)
- Example of deadlock under basic priority inheritance protocol:
- In general, Priority Ceiling Protocol prevents transitive blocking
- If \(J_1\) is blocked by lower priority \(J_2\), \(J_1\) cannot hold any locks
- thus, nothing can wait for \(J_2\) while it is waiting for \(J_1\)
- thus, no wait-for cycles (deadlocks)
- If \(J_1\) is blocked by lower priority \(J_2\), \(J_1\) cannot hold any locks
- Maximum blocking (due to lower priority jobs) is duration of one critical section
- once a critical section needed by \(J_1\) is locked, no other critical section needed by \(J_1\) can get locked unless locker is higher priority than \(J_1\)
- Example: re-consider earlier worst-case lock chain example
- all three critical sections have priority ceiling of 1, since they are accessed by \(J_1\)
- \(J_4\) can lock \(M_4\) since nothing else is locked
- \(J_3\) is prevented from locking \(M_3\), because \(J_3\)’s priority is below \(M_3\)’s priority ceiling. \(J_4\) inherits priority 3, since it is blocking \(J_3\)
- similarly, \(J_2\) cannot lock \(M_2\), and \(J_4\) will inherit priority 2
- When \(J_1\) runs and tries to access \(M_2\), it will block for the same reason, and \(J_4\) will inherit priority 1.
- When \(J_4\) releases \(M_4\), \(J_1\) will resume and lock \(M_2\) since nothing else is locked
- \(J_1\) will then proceed to lock \(M_3\) and \(M_4\) without delay
- no new job with priority less than \(J_1\)’s can ever lock something that \(J_1\) needs, since that something’s priority ceiling will be at least 1
- no deadlocks, under arbitrary semaphore accesses with nesting