Prompt and Applicable Context
A single-core, fixed-priority, preemptive real-time system has three tasks. A larger priority number means a higher priority:
| Task | Priority | Behavior | |---|---:|---| | H | 90 | Needs bus_mutex after waking and has a 10ms relative deadline | | M | 50 | Never uses the mutex and needs 20ms of uninterrupted CPU work after waking | | L | 10 | Already owns bus_mutex and has 3ms of CPU work left in the critical section |
At t=0, L owns the mutex. H then wakes, preempts L, attempts the mutex, and blocks. After H has been blocked for 1ms, M becomes runnable. Explain the execution order with an ordinary mutex and with priority inheritance, calculate a bound on H's lock wait, and address the following:
- what makes this an unbounded priority inversion;
- how priority propagates and is restored with multiple mutexes, waiters, and nested blocking;
- the trade-off between priority inheritance and priority-ceiling protocols;
- why an ordinary semaphore, a shorter time slice, or another increase to H's priority is not an equivalent fix;
- how scheduler traces and deadline metrics can prove that the fix works.
This question fits embedded, RTOS, real-time Linux, robotics, audio/video, industrial-control, and systems-software roles. The priorities and the 3ms, 20ms, and 10ms values are exercise constraints, not a claim about a particular RTOS priority range or a production threshold.
What the Interviewer Is Evaluating
The first layer is separating direct resource blocking from delay introduced by unrelated work. H waiting for L to release a resource is already a priority inversion. The part that destroys predictability is that M does not use the resource yet can repeatedly preempt the low-priority lock owner L, stretching H's wait by arbitrary amounts of medium-priority work.
The second layer is drawing the event sequence instead of reciting a definition. With an ordinary mutex, L is runnable after H blocks but is preempted when M wakes. H waits for M's 20ms plus L's remaining 3ms, about 23ms, which exceeds the 10ms deadline. With inheritance, H donates effective priority 90 to L at the instant it blocks. M cannot preempt L, so under the prompt's assumptions H waits for about L's remaining 3ms plus scheduler overhead.
The third layer is understanding implementation state. A lock needs an owner and priority-ordered waiters. When a task owns several locks, its effective priority is the maximum of its base priority and every live donation. If the highest waiter times out, is cancelled, or stops waiting because a lock is released, priority must be recomputed rather than reset unconditionally to the base value. If the boosted owner blocks on another lock, donation must propagate along the PI chain.
The fourth layer is mechanism boundaries. Inheritance limits unbounded delay caused by medium-priority tasks. It does not shorten I/O inside a critical section, interrupt-disabled regions, non-preemptible code, or hardware operations, and it does not eliminate deadlock. A priority ceiling trades advance configuration for a more static blocking analysis. A counting semaphore without a unique owner provides no definite task to boost.
Finally, the interviewer wants verification discipline. A strong answer compares lock-wait duration, time that L is runnable but not running while holding the lock, whether M enters that interval, H's deadline misses, and nested-lock and timeout paths. Average CPU utilization or “the system did not hang” does not prove a real-time bound.
Questions to Clarify Before Answering
- Which direction do priorities run, and what is the scheduling policy? The prompt says 90 is higher than 50 and specifies single-core fixed-priority preemption. Some RTOSes number priorities in the other direction, and a normal time-sharing scheduler does not support the same timeline directly.
- Is 3ms wall-clock time or actual CPU execution time? Here it is L's remaining CPU work inside the critical section. PI cannot make a device wait or non-preemptible region physically faster.
- When does H's 10ms deadline begin? Here it begins when H wakes. If the deadline starts at request arrival or periodic release, earlier queueing must also enter the response-time calculation.
- Are there interrupts or tasks above priority 90? The exercise excludes them from the first calculation. A real bound must include higher-priority interference, maximum interrupt-disabled time, scheduler overhead, and cache effects.
- Is
bus_mutexactually an owner-tracking mutex? A binary semaphore may look similar but need not have an owner, owner-only unlock, or PI semantics. - Does L own another lock or wait for one? That determines whether donation must propagate recursively and changes both the deadlock analysis and blocking bound.
- Which protocols does the target implementation support? POSIX exposes
PTHREADPRIOINHERITandPTHREADPRIOPROTECT. RTOS behavior for multiple locks, timeouts, recursive locks, and deboosting differs, so the exact version's documentation matters.
30-Second Answer Framework
“With an ordinary mutex, H blocks because L owns bus_mutex. L could otherwise finish its remaining 3ms, but after 1ms M at priority 50 preempts L at priority 10 and runs for 20ms. M never touches the mutex yet makes H at priority 90 wait indirectly. H's lock wait is therefore about 20+3=23ms, beyond the 10ms deadline. If medium-priority jobs can keep arriving, L's critical-section length no longer bounds that extra wait.
With priority inheritance, H's block raises L's effective priority to 90. M cannot preempt L after it wakes. L releases the mutex after about 3ms, recomputes its priority from remaining waiters and owned mutexes, and H acquires the lock. PI controls delay from unrelated medium-priority work. It does not promise zero blocking or repair a long critical section, deadlock, or interrupt-disabled code.
I would drive a deterministic release sequence and record H's block and unblock, L's ownership interval, M's execution intervals, and every deadline miss for ordinary and PI mutexes. I would also test chained donation, multiple waiters, waiter timeout, and incremental release of multiple locks to prove that both boosting and deboosting are correct.”
Step-by-Step Deep Dive
Step 1: Draw the Unbounded-Inversion Timeline for an Ordinary Mutex
Use H's wake-up as relative time 0ms. L already owns the lock and has 3ms of CPU work remaining:
| Relative time | Event | Result | |---|---|---| | 0ms | H wakes, preempts L, and requests bus_mutex | H blocks because L is the owner | | 0–1ms | L resumes | L executes 1ms and has 2ms left in the critical section | | 1ms | M wakes | Priority-50 M preempts priority-10 L | | 1–21ms | M runs for 20ms | H still waits; L is runnable but cannot run | | 21–23ms | L runs its remaining 2ms and unlocks | H finally becomes unblocked |
H waits about 23ms from wake-up to mutex acquisition, missing its 10ms deadline. If jobs like M can continue to arrive while H is blocked, H's wait is no longer bounded by L's remaining 3ms critical section. That is the unbounded inversion here. It does not mean an infinite wait is mathematically inevitable; it means the design supplies no finite, auditable blocking bound derived from the protected resource.
Distinguish deadlock, starvation, and overload. A deadlock has a wait cycle with no task able to release the required resource. Starvation need not involve a lock owner. CPU overload commonly makes several tasks miss deadlines. The inversion evidence chain is specific: H waits for a resource owned by L while M, which has no dependency on that resource, prevents L from executing.
Step 2: Add Priority Inheritance and Recalculate
When H blocks at 0ms, the mutex donates its highest waiter's priority 90 to owner L. L retains base priority 10 and temporarily has effective priority 90:
| Relative time | Event | Result | |---|---|---| | 0ms | H blocks on L's mutex | L inherits 90 and immediately resumes | | 1ms | Priority-50 M wakes | M cannot preempt effective-priority-90 L | | 0–3ms | L finishes the remaining critical section and unlocks | H's lock wait is about 3ms plus scheduler overhead | | About 3ms | H acquires the mutex | About 7ms of deadline budget remains |
The 3ms conclusion depends on the prompt: there is no higher-priority task, long interrupt-disabled region, non-preemptible section, page fault, or blocking I/O, and the mutex truly implements PI. Real response time must also include H's execution and all higher-priority interference. PI's central benefit is that M's 20ms no longer enters this resource-blocking interval for H.
Boosting changes effective priority; it must not permanently overwrite L's base priority. If no other donation remains after unlock, L returns to 10. If a priority-80 waiter still waits on another mutex that L owns, L may deboost only to 80, not directly to 10.
Step 3: Handle Multiple Waiters, Multiple Locks, and a PI Chain
Suppose L owns both R1 and R2, H90 waits on R1, and X70 waits on R2. L's effective priority is 90. After L releases R1, H no longer donates to L, but X still waits for R2, so L deboosts to 70. It returns to base priority 10 only after releasing R2. A timeout or cancellation by the highest waiter must trigger the same recomputation.
Now suppose L is waiting for R3, which K owns. Boosting only L cannot release H's lock because L itself cannot run past its wait. Priority 90 must propagate along H → R1 → L → R3 → K. K releases R3, allowing L to proceed and eventually release R1. Linux rt-mutex maintains this with a priority-ordered waiter set for each mutex and the highest waiter from each owned mutex in the owner's PI waiter set.
Chained donation does not repair bad lock ordering. If L waits for K while K waits for L, PI merely boosts tasks in a wait cycle; no executable release path appears. Engineering controls still include a global lock order, bounded nesting, no blocking I/O while holding a mutex, and an auditable worst-case critical-section duration.
Step 4: Compare Priority Inheritance with a Priority Ceiling
POSIX mutex protocols make the distinction concrete:
PTHREADPRIOINHERIT: an owner is boosted only when it actually blocks a higher-priority thread. Its effective priority becomes the maximum of its base priority and live waiter donations, and nested blocking propagates recursively.PTHREADPRIOPROTECT: while a thread owns a mutex, it executes at least at that mutex's configured priority ceiling, whether or not a waiter currently exists.
Inheritance pays runtime bookkeeping and chain-propagation costs when contention occurs but does not require advance knowledge of every user's maximum priority. A ceiling requires the task/resource set to be known and configured correctly, in exchange for a more static, analyzable blocking boundary. Whether a particular ceiling protocol prevents a class of deadlocks also depends on complete ceiling-admission rules in that system. The word “ceiling” alone does not prove deadlock freedom.
A POSIX initialization sketch is below. Production code must check implementation support, return values, scheduling privilege, and mutex lifetime:
pthread_mutexattr_t attr;
int rc = pthread_mutexattr_init(&attr);
if (rc == 0) {
rc = pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
}
if (rc == 0) {
rc = pthread_mutex_init(&bus_mutex, &attr);
}
pthread_mutexattr_destroy(&attr);Setting the attribute for ordinary SCHEDOTHER threads on Linux is not a hard real-time deadline guarantee. The scheduling class, real-time-priority privilege, PREEMPTRT or target-kernel behavior, and every other application blocking source still need validation.
Step 5: Choose the Correct Primitive and Shorten the Critical Section
PI depends on a definite owner. When a high-priority waiter blocks, the kernel must know which task to boost. Protect shared state with an owner-tracking mutex that the owner unlocks. A counting semaphore used for resource counts or notification may have no unique owner, so it provides no reliable donation target. A binary semaphore does not acquire mutex PI semantics merely because its value is limited to zero or one.
Even with PI, make the critical section calculable. Copy only required state under the lock. Move device transfers, logging, allocation, and calls that may sleep outside it. For serialized access to a slow device, a dedicated high-priority service task and message passing may be a better architecture. A lock-free structure may reduce mutex blocking but introduces reclamation, ABA, retries, and potentially worse worst-case execution time. Choose against deadline analysis, not a label.
Raising H's priority cannot solve this prompt: H is already the highest task and cannot run while blocked. A shorter time slice merely schedules M more often; it does not let priority-10 L outrank priority-50 M. Disabling preemption expands response latency for every task and moves the problem into a non-preemptible region.
Step 6: Verify the Blocking Bound with a Reproducible Trace
Create a fixed release order: L locks first, a barrier confirms ownership, H is released, then M is released 1ms after H blocks. Run ordinary, PI, and priority-ceiling mutex variants repeatedly and record:
- H's mutex-acquisition latency distribution and count of 10ms deadline misses;
- cumulative time L is runnable but not running while it owns the mutex;
- M's execution intervals while H is blocked;
- L's base and effective priorities, mutex owner, highest waiter, and unlock time;
- scheduler switches, wake-ups, interrupt-disabled time, and non-preemptible intervals.
The ordinary mutex trace should show M entering the interval and H waiting about 23ms. In the PI variant, M should not preempt during the interval where H waits and L is runnable while owning the mutex; under the prompt's load, H should wait near 3ms rather than 23ms. Add a second waiter at priority 70, a nested mutex, waiter timeout, and incremental release of L's two locks to verify boost and deboost against the highest current donation.
The acceptance criterion must not say “PI means no more timeouts.” Use a conditional bound: given the measured maximum critical section, highest-priority interference, maximum interrupt-disabled time, and scheduler-overhead budget, H's worst-case response remains below 10ms, including under stress and fault injection.
Strong Sample Answer
“I will first fix the priority direction and time origin: 90 is highest, and H's 10ms begins when H wakes. With an ordinary mutex, H preempts L, finds bus_mutex owned, and blocks. L resumes for 1ms, then priority-50 M wakes and preempts L for 20ms. L finally executes its remaining 2ms and unlocks. H therefore takes about 23ms from wake-up to mutex acquisition and definitely misses its deadline. M never uses the mutex, but indirectly prevents highest-priority H from running. If medium-priority work can continue to arrive, L's 3ms critical section cannot bound this extra blocking.
With PI, H donates priority 90 to L when it blocks. L's base priority remains 10 and its effective priority becomes 90, so M cannot preempt at 1ms. L completes the critical section in about 3ms and releases the mutex, then H acquires it. Resource blocking is therefore below 10ms under the prompt's assumptions. Higher-priority tasks, interrupt-disabled time, and scheduler overhead are excluded from that number and must be restored in a real response-time analysis.
The implementation cannot store one boosted Boolean. Each mutex needs an owner and highest waiter. Each owner takes the maximum of its base priority and donations across all owned mutexes. A timeout by the highest waiter or release of one lock causes recomputation. If L is blocked on K's lock, priority 90 must propagate to K along the PI chain. If there is a wait cycle, boosting cannot release any resource, so lock-order and deadlock checks remain necessary.
I would use one release sequence to compare an ordinary mutex with a PTHREADPRIOINHERIT mutex and capture scheduler switches, lock ownership, effective priority, H's block duration, and deadline misses. Direct passing evidence is that M no longer runs while H is blocked and runnable L owns the mutex, and H's wait converges from about 23ms to L's roughly 3ms remaining critical section plus accounted system interference. A second waiter, nested locks, timeout, and one-by-one unlocks verify chained boosting and correct deboosting.”
Common Mistakes
- Saying only “a low-priority task blocks a high-priority task” → this omits how M turns a finite critical section into unbounded interference → draw the full H-blocks, L-runnable, M-preempts sequence.
- Calculating only a 3ms wait for H → this ignores M's 20ms preemption under the ordinary mutex → follow the event order to about 23ms and compare it with 10ms.
- Claiming PI eliminates every priority inversion → H must still wait for L's remaining critical section → state that PI removes unbounded extension by unrelated medium-priority work.
- Resetting L directly to 10 after one unlock → a high-priority waiter may remain on another owned mutex → recompute effective priority from every live donation.
- Boosting only the direct owner → an owner blocked on another lock still cannot run → propagate along the PI chain and bound and observe chain depth.
- Treating PI as a deadlock repair → boosted tasks in a wait cycle still have no executable release path → retain lock ordering, timeout policy, and deadlock detection.
- Replacing the mutex with a binary semaphore while assuming PI → the semaphore may have no owner, leaving no task to boost → use an owner mutex that explicitly supports PI for resource protection.
- Treating
PTHREADPRIOINHERITas a hard-real-time switch → scheduling class, privilege, kernel preemption, interrupts, and other waits still affect deadlines → validate the full platform and calculate worst-case response time. - Checking only improved average latency → deboost, nested propagation, or timeout paths may still be wrong → verify tails, deadline misses, effective priorities, and multiple-lock boundaries.
Follow-Up Questions and Responses
Follow-Up 1: Why Call It Unbounded if M Runs for Exactly 20ms Here?
This exercise contains one M job, so the result is calculable at about 23ms. “Unbounded” describes a mechanism that gives the added delay no limit based on L's critical section. If M-class jobs can be released continuously, L can remain runnable without receiving CPU and H's wait grows with M work. PI removes M interference from this blocking interval; the remaining bound comes from the critical section and higher-priority interference.
Follow-Up 2: What If H Waits for L and L Waits for K?
Donate H's 90 to L, then propagate it through the mutex on which L waits to owner K. K executes its critical section at effective priority 90 and releases, allowing L to continue and eventually release H's lock. A trace should show boost and reverse deboost across H → mutex1 → L → mutex2 → K. Observing only L's boost does not prove chained PI is correct.
Follow-Up 3: What Priority Should L Have After a High-Priority Waiter Times Out?
Use the highest demand that remains. If H90 times out but X70 still waits on another mutex that L owns, L deboosts from 90 to 70. It returns to base priority 10 only after every donation disappears. Waiter insertion, departure, timeout, cancellation, and each unlock must maintain this priority-ordered state.
Follow-Up 4: Is a Priority Ceiling Always Better Than Inheritance?
No. A ceiling supports static blocking analysis when the task set and resource-access relationships are stable, but a bad ceiling can reject legitimate access or invalidate the analysis. Inheritance boosts on actual contention and needs less configuration, but it must maintain waiters, chain propagation, and dynamic deboosting. Choose against the RTOS's exact protocol, task-set stability, and acceptable runtime overhead.
Follow-Up 5: Why Can PI Not Repair I/O While Holding the Lock?
Boosting helps an owner obtain CPU earlier only while it is runnable. If L sleeps for an SPI transfer, storage, a page fault, or another event, it remains asleep. If execution has interrupts disabled or is non-preemptible, the scheduler cannot intervene either. Move slow work outside the critical section or use a service task or asynchronous protocol, then include the hardware's worst completion time in the budget.
Follow-Up 6: How Do You Prove the Attribute Really Works in Linux User Space?
Check return values from pthreadmutexattrsetprotocol and pthreadmutexinit, support for POSIXTHREADPRIOINHERIT, the threads' actual scheduling policies, and real-time-priority privilege. Then run a controlled H/M/L workload and trace scheduling plus futex or lock events. Confirm L's effective boost, absence of M in the critical interval, and correct deboost after unlock or waiter timeout. Successful initialization or an incidental p99 improvement is insufficient evidence.