CS452 F23 Lecture Notes
Lecture 18 - 30 Nov 2023

1. Priority Inversions

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)
    • 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
  • 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
  • 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.
    • 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

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
    • 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
  • 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
  • Example:

  • 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
  • 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\)
    • 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)
    • 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

Author: Ken Salem

Created: 2023-12-03 Sun 22:11