Chapter 2: Background and Prior Work
2.1 Multiprocessor Real-Time Scheduling
2.1.6 Locking Protocols
2.1.6.2 Nested Locking
A job may require exclusive access to multiple shared resources at once. This can lead tonestedresource requests, where a task first acquires resource`aand then acquires resource`b. In other words, the critical section of`bmay be nested within the critical section of`a. Arbitrary nesting of critical sections may lead to deadlock. The classic example of deadlock is the situation where taskTiholds resource`aand blocks for access to resource`b, whileTj holds resource`band blocks for access to resource`a. Neither task makes progress, so the two tasks are blocked forever. Real-time correctness cannot be guaranteed for a system where 7Additional refinements to priority donation rules allow a donor to be scheduled under special conditions when its donee is suspended. However, this is merely a runtime optimization that does not improve schedulability analysis. We direct the interested reader to Brandenburg and Anderson (2013) for details.
deadlock is possible. There are three general approaches to supporting nested resource requests: group locks, totally-ordered nested requests, and deadlock-free locking protocols.
Group Locks. The first approach to manage nested locking is to define away the problem. This is done by protecting the set of nested shared resources with a singlegroup lock(Blocket al., 2007). A task must acquire this lock if it needs to access one or more of the resources protected by the group lock. While safe, this approach limits parallelism. For example, consider the situation where three resources,`a,`b, and`c, are protected by a single group lock. TaskTi requires resources`aand`b, while taskTjonly requires`c. The execution ofTiandTjis serialized when they contend for the group lock, even though they do not actually share the same resources.
Totally-Ordered Nested Requests. Another approach to supporting nested critical sections is to ensure that resources are acquired in an order that guarantees deadlock freedom. To do so, we enumerate all resources in a single sorted order `1,· · ·, `q. This ordering is observed by all tasks in a system. If a task requires simultaneous access to two resources, then it must acquire`ibefore resource`j, wherei< j. This generalizes to an arbitrary number of resources. It is easy to see how total ordering resolves the classic deadlock scenario. If tasksTiandTj both require access to resources`aand`b, then the tasks contend for`abefore they may contend for`b. No task can hold`bwhile it contends for`a, so nested locking is deadlock-free. A drawback to totally ordered nested requests is that it requires disciplined programming. A given resource ordering may also be at odds with the natural flow of program code. For example, although a taskTimay require access to both resources`aand`b, program code may begin using`blong before`a. However, the total ordering requires`ato be obtained early.
Deadlock-Free Locking Protocols. Deadlock freedom can also be guaranteed by a locking protocol al- gorithm. Classic (uniprocessor) real-time locking protocols that ensure deadlock freedom include the priority-ceiling protocol (PCP) (Shaet al., 1990) and the stack resource policy (SRP) (Baker, 1991). These locking protocols use rules that delay access to a shared resource, even if it is available, if immediate access maylead to deadlock at a later time.
Another technique that can guarantee deadlock freedom is the use ofdynamic group locks(DGLs) (Ward and Anderson, 2013). Under DGLs, a task issues requests for all resources it may requireatomically. DGLs leverage the combined atomic request to guarantee deadlock freedom. Consider the following scenario. A taskTirequires resources`aand`b, while taskTj requires resources`a, `b, and`c. Ti issues a combined
request for(`a, `b), andTj issues a combined request for(`a, `b, `c). The underlying locking protocol data structures for each resource are jointly updated atomically. Under FIFO-ordered locks, resources can be granted in any order without risk of deadlock. This is because whenever tasksTiandTjcontend for the same resources, the relative ordering between the tasks’ requests is the same in every FIFO queue. Thus, access to every resource is granted in the same order. This prevents the deadlock scenario where each task waits for resources held by the other.
We must point out two important details of DGLs. First, DGLs must maintain the illusion of obtaining resources through individual requests in order to maintain sporadic task model abstractions. This means that progress mechanisms that act on behalf of a taskTimay only be active on one lock at a time. We illustrate this point with an example. Suppose taskTiwaits for resources`aand`b, and priority inheritance is used as the progress mechanism for these locks. Either the resource holder of`aor the resource holder of`bmay inherit the priority ofTiat any given time instant, but not both.8 The second important detail of DGLs is that the underlying locking protocolimplementationmust support joint atomic updates. The data structures that manage unsatisfied lock requests are commonly protected by per-lock spinlocks that reside in the OS kernel. These spinlocks are only held while the data structures are modified. In order to support atomic DGL resource requests, all of the spinlocks related to the resources in a DGL request must be obtained before modifying the data structures of the individual locks. We have traded one multi-resource request problem (the request for DGL-protected resources) for another (the spinlocks that protect the locking protocol data structures of said resources)! We can resolve this problem in one of two ways. We may protect all locking protocol data structures with a single spinlock (i.e., a group lock). This may be appropriate, since spinlocks are held for only a short duration. However, this hurts parallelism, as all concurrently issued resource requests serialize on the DGL spinlock. A better approach is to obtain the necessary spinlocks in a total order. Thankfully, this trivial to implement in the OS kernel. Each spinlock has a unique memory address, so we obtain spinlocks in order of their memory addresses.