Re: [Revised document] Crossrelease lockdep

From: Byungchul Park
Date: Mon Aug 22 2016 - 00:16:21 EST


On Fri, Aug 19, 2016 at 09:39:59PM +0900, Byungchul Park wrote:
> Hello,
>
> I rewrote the document so that the need of crossrelease feature can be
> described logically. I think it's more important than to post specific
> implementation. Could you let me know your opinions about this?
>
> Thanks,
> Byungchul
>
> ----->8-----
>
> Crossrelease
> ============
>
> Started by Byungchul Park <byungchul.park@xxxxxxx>
>
> Contents:
>
> (*) Background.
>
> - What causes deadlock.
> - What lockdep detects.
> - How lockdep works.
>
> (*) Limitation.
>
> - Limit to typical lock.
> - Pros from the limitation.
> - Cons from the limitation.
>
> (*) Generalization.
>
> - Relax the limitation.
>
> (*) Crossrelease.
>
> - Introduce crossrelease.
> - Introduce commit.
>
> (*) Implementation.
>
> - Data structures.
> - How crossrelease works.
>
> (*) Optimizations.
>
> - Avoid duplication.
> - Avoid lock contention.
>
>
> ==========
> Background
> ==========
>
> What causes deadlock
> --------------------
>
> A deadlock occurs when a context is waiting for an event to be issued
> which cannot be issued because the context or another context who can
> issue the event is also waiting for an event to be issued which cannot
> be issued. Single context or more than one context both waiting for an
> event and issuing an event may paricipate in a deadlock.
>
> For example,
>
> A context who can issue event D is waiting for event A to be issued.
> A context who can issue event A is waiting for event B to be issued.
> A context who can issue event B is waiting for event C to be issued.
> A context who can issue event C is waiting for event D to be issued.
>
> A deadlock occurs when these four operations are run at a time because
> event D cannot be issued if event A isn't issued which in turn cannot be
> issued if event B isn't issued which in turn cannot be issued if event C
> isn't issued which in turn cannot be issued if event D isn't issued. No
> event can be issued since any of them never meets its precondition.
>
> We can easily recognize that each wait operation creates a dependency
> between two issuings e.g. between issuing D and issuing A like, 'event D
> cannot be issued if event A isn't issued', in other words, 'issuing
> event D depends on issuing event A'. So the whole example can be
> rewritten in terms of dependency,
>
> Do an operation making 'event D cannot be issued if event A isn't issued'.
> Do an operation making 'event A cannot be issued if event B isn't issued'.
> Do an operation making 'event B cannot be issued if event C isn't issued'.
> Do an operation making 'event C cannot be issued if event D isn't issued'.
>
> or,
>
> Do an operation making 'issuing event D depends on issuing event A'.
> Do an operation making 'issuing event A depends on issuing event B'.
> Do an operation making 'issuing event B depends on issuing event C'.
> Do an operation making 'issuing event C depends on issuing event D'.
>
> What causes a deadlock is a set of dependencies a chain of which forms a
> cycle, which means that issuing event D depending on issuing event A
> depending on issuing event B depending on issuing event C depending on
> issuing event D, finally depends on issuing event D itself, which means
> no event can be issued.
>
> Any set of operations creating dependencies causes a deadlock. The set
^^^
can
> of lock operations e.g. acquire and release is an example. Waiting for a
> lock to be released corresponds to waiting for an event and releasing a
> lock corresponds to issuing an event. So the description of dependency
> above can be altered to one in terms of lock.
>
> In terms of event, issuing event A depends on issuing event B if,
>
> Event A cannot be issued if event B isn't issued.
>
> In terms of lock, releasing lock A depends on releasing lock B if,
>
> Lock A cannot be released if lock B isn't released.
>
> CONCLUSION
>
> A set of dependencies a chain of which forms a cycle, causes a deadlock,
> no matter what creates the dependencies.
>
>
> What lockdep detects
> --------------------
>
> A deadlock actually occurs only when all operations creating problematic
> dependencies are run at a time. However, even if it has not happend, the
> deadlock potentially can occur if the problematic dependencies obviously
> exist. Thus it's meaningful to detect not only an actual deadlock but
> also its possibility. Lockdep does the both.
>
> Whether a deadlock actually occurs or not depends on several factors,
> which means a deadlock may not occur even though problematic
> dependencies exist. For example, what order contexts are switched in is
> a factor. A deadlock will occur when contexts are switched so that all
> operations causing a deadlock become run simultaneously.
>
> Lockdep tries to detect a deadlock or its possibility aggressively,
> though it also tries to avoid false positive detections. So lockdep is
> designed to consider all possible combinations of dependencies so that
> it can detect all potential possibilities of deadlock in advance. What
> lockdep tries in order to consider all possibilities are,
>
> 1. Use a global dependency graph including all dependencies.
>
> What lockdep checks is based on dependencies instead of what actually
> happened. So no matter which context or call path a new dependency is
> detected in, it's just referred to as a global factor.
>
> 2. Use lock classes than lock instances when checking dependencies.
>
> What actually causes a deadlock is lock instances. However, lockdep
> uses lock classes than its instances when checking dependencies since
> any instance of a same lock class can be altered anytime.
>
> So lockdep detects both an actual deadlock and its possibility. But the
> latter is more valuable than the former. When a deadlock actually
> occures, we can identify what happens in the system by some means or
> other even without lockdep. However, there's no way to detect possiblity
> without lockdep unless the whole code is parsed in head. It's terrible.
>
> CONCLUSION
>
> Lockdep does, the fisrt one is more valuable,
>
> 1. Detecting and reporting deadlock possibility.
> 2. Detecting and reporting a deadlock actually occured.
>
>
> How lockdep works
> -----------------
>
> What lockdep should do, to detect a deadlock or its possibility are,
>
> 1. Detect a new dependency created.
> 2. Keep the dependency in a global data structure esp. graph.
> 3. Check if any of all possible chains of dependencies forms a cycle.
> 4. Report a deadlock or its possibility if a cycle is detected.
>
> A graph used by lockdep to keep all dependencies looks like,
>
> A -> B - -> F -> G
> \ /
> -> E - -> L
> / \ /
> C -> D - -> H -
> \
> -> I -> K
> /
> J -
>
> where A, B,..., L are different lock classes.
>
> Lockdep adds a dependency into graph when a new dependency is detected.
> For example, it adds a dependency 'A -> B' when a dependency between
> releasing lock A and releasing lock B, which has not been added yet, is
> detected. It does same thing on other dependencies, too. See 'What
> causes deadlock' section.
>
> NOTE: Precisely speaking, a dependency is one between releasing a lock
> and releasing another lock as described in 'What causes deadlock'
> section. However from now on, we will describe a dependency as if it's
> one between a lock and another lock for simplicity. Then 'A -> B' can be
> described as a dependency between lock A and lock B.
>
> We already checked how a problematic set of dependencies causes a
> deadlock in 'What causes deadlock' section. This time let's check if a
> deadlock or its possibility can be detected using a problematic set of
> dependencies. Assume that 'A -> B', 'B -> E' and 'E -> A' were added in
> the sequence into graph. Then the graph finally will be,
>
> -> A -> B -> E -
> / \
> \ /
> ----------------
>
> where A, B and E are different lock classes.
>
> >From adding three dependencies, a cycle was created which means, by
> definition of dependency, the situation 'lock E must be released to
> release lock B which in turn must be released to release lock A which in
> turn must be released to release lock E which in turn must be released
> to release B and so on infinitely' can happen.
>
> Once the situation happens, no lock can be released since any of them
> can never meet each precondition. It's a deadlock. Lockdep can detect a
> deadlock or its possibility with checking if a cycle was created after
> adding each dependency into graph. This is how lockdep detects a
> deadlock or its possibility.
>
> CONCLUSION
>
> Lockdep detects a deadlock or its possibility with checking if a cycle
> was created after adding each dependency into graph.
>
>
> ==========
> Limitation
> ==========
>
> Limit to typical lock
> ---------------------
>
> Limiting what lockdep has to consider to only ones satisfying the
> following condition, the implementation of adding dependencies becomes
> simple while its capacity for detection becomes limited. Typical lock
> e.g. spin lock and mutex is the case. Let's check what pros and cons of
> it are, in next section.
>
> A lock should be released within the context holding the lock.
>
> CONCLUSION
>
> Limiting what lockdep has to consider to typical lock e.g. spin lock and
> mutex, the implmentation becomes simple while it has a limited capacity.
>
>
> Pros from the limitation
> ------------------------
>
> Given the limitation, when acquiring a lock, any lock being in
> held_locks of the acquire context cannot be released if the lock to
> acquire was not released yet. Yes. It's the exact case to add a new
> dependency 'A -> B' into graph, where lock A represents each lock being
> in held_locks and lock B represents the lock to acquire.
>
> For example, only considering typical lock,
>
> PROCESS X
> --------------
> acquire A
>
> acquire B -> add a dependency 'A -> B'
>
> acquire C -> add a dependency 'B -> C'
>
> release C
>
> release B
>
> release A
>
> where A, B and C are different lock classes.
>
> When acquiring lock A, there's nothing in held_locks of PROCESS X thus
> no dependency is added. When acquiring lock B, lockdep detects and adds
> a new dependency 'A -> B' between lock A being in held_locks and lock B.
> And when acquiring lock C, lockdep also adds another dependency 'B -> C'
> for same reason. They are added when acquiring each lock, simply.
>
> NOTE: Even though every lock being in held_locks depends on the lock to
> acquire, lockdep does not add all dependencies between them because all
> of them can be covered by other dependencies except one dependency
> between the lock on top of held_locks and the lock to acquire, which
> must be added.
>
> Besides, we can expect several advantages from the limitation.
>
> 1. Any lock being in held_locks cannot be released unconditionally if
> the context is stuck, thus we can easily identify a dependency when
> acquiring a lock.
>
> 2. Considering only locks being in local held_locks of a single context
> makes some races avoidable, even though it fails of course when
> modifying its global dependency graph.
>
> 3. To build a dependency graph, lockdep only needs to keep locks not
> released yet. However relaxing the limitation, it might need to keep
> even locks already released, additionally. See 'Crossrelease' section.
>
> CONCLUSION
>
> Given the limitation, the implementation becomes simple and efficient.
>
>
> Cons from the limitation
> ------------------------
>
> Given the limitation, lockdep is applicable only to typical lock. For
> example, page lock for page access or completion for synchronization
> cannot play with lockdep having the limitation. However since page lock
> or completion also causes a deadlock, it would be better to detect a
> deadlock or its possibility even for them.
>
> Can we detect deadlocks below with lockdep having the limitation?
>
> Example 1:
>
> PROCESS X PROCESS Y
> -------------- --------------
> mutext_lock A
> lock_page B
> lock_page B
> mutext_lock A // DEADLOCK
> unlock_page B
> mutext_unlock A
> mutex_unlock A
> unlock_page B
>
> where A and B are different lock classes.
>
> No, we cannot.
>
> Example 2:
>
> PROCESS X PROCESS Y PROCESS Z
> -------------- -------------- --------------
> mutex_lock A
> lock_page B
> lock_page B
> mutext_lock A // DEADLOCK
> mutext_unlock A
>
> unlock_page B held by X
> unlock_page B
> mutex_unlock A
>
> where A and B are different lock classes.
>
> No, we cannot.
>
> Example 3:
>
> PROCESS X PROCESS Y
> -------------- --------------
> mutex_lock A
> mutex_lock A
> mutex_unlock A
> wait_for_complete B // DEADLOCK
> complete B
> mutex_unlock A
>
> where A is a lock class and B is a completion variable.
>
> No, we cannot.
>
> CONCLUSION
>
> Given the limitation, lockdep cannot detect a deadlock or its
> possibility caused by page lock or completion.
>
>
> ==============
> Generalization
> ==============
>
> Relax the limitation
> --------------------
>
> Detecting and adding new dependencies into graph is very important for
> lockdep to work because adding a dependency means adding a chance to
> check if it causes a deadlock. More dependencies lockdep adds, more
> throughly it can work. Therefore Lockdep has to do its best to add as
> many true dependencies as possible.
>
> Relaxing the limitation, lockdep can add additional dependencies since
> it makes lockdep deal with additional ones creating the dependencies e.g.
> page lock or completion, which might be released in any context. Even so,
> it needs to be noted that behaviors adding dependencies created by
> typical lock don't need to be changed at all.
>
> For example, only considering typical lock, lockdep builds a graph like,
>
> A -> B - -> F -> G
> \ /
> -> E - -> L
> / \ /
> C -> D - -> H -
> \
> -> I -> K
> /
> J -
>
> where A, B,..., L are different lock classes, and upper case letters
> represent typical lock.
>
> After the relaxing, the graph will have additional dependencies like,
>
> A -> B - -> F -> G
> \ /
> -> E - -> L -> c
> / \ /
> C -> D - -> H -
> / \
> a - -> I -> K
> /
> b -> J -
>
> where a, b, c, A, B,..., L are different lock classes, and upper case
> letters represent typical lock while lower case letters represent
> non-typical lock e.g. page lock and completion.
>
> However, it might suffer performance degradation since relaxing the
> limitation with which design and implementation of lockdep become
> efficient might introduce inefficiency inevitably. Each option, that is,
> strong detection or efficient detection has its pros and cons, thus the
> right of choice between two options should be given to users.
>
> Choosing efficient detection, lockdep only deals with locks satisfying,
>
> A lock should be released within the context holding the lock.
>
> Choosing strong detection, lockdep deals with any locks satisfying,
>
> A lock can be released in any context.
>
> In the latter, of course, some contexts are not allowed if they
> themselves cause a deadlock. For example, acquiring a lock in irq-safe
> context before releasing the lock in irq-unsafe context is not allowed,
> which after all ends in a cycle of a dependency chain, meaning a
> deadlock. Otherwise, any contexts are allowed to release it.
>
> CONCLUSION
>
> Relaxing the limitation, lockdep adds additional dependencies and gets
> additional chances to check if they cause a deadlock. It makes lockdep
> additionally deal with what might be released in any context.
>
>
> ============
> Crossrelease
> ============
>
> Introduce crossrelease
> ----------------------
>
> To allow lockdep to add additional dependencies created by what might be
> released in any context, which we call 'crosslock', it's necessary to
> introduce a new feature which makes it possible to identify and add the
> dependencies. We call the feature 'crossrelease'. Crossrelease feature
> has to do,
>
> 1. Identify a new dependency created by crosslock.
> 2. Add the dependency into graph when identifying it.
>
> That's all. Once a meaningful dependency is added into graph, lockdep
> will work with the graph as it did. So the most important thing to do is
> to identify a dependency created by crosslock. Remind what a dependency
> is. For example, Lock A depends on lock B if 'lock A cannot be released
> if lock B isn't released'. See 'What causes deadlock' section.
>
> By definition, a lock depends on every lock having been added into
> held_locks in the lock's release context since the lock was acquired,
> because the lock cannot be released if the release context is stuck by
> any of dependent locks, not released. So lockdep should technically
> consider release contexts of locks to identify dependencies.
>
> It's no matter of course to typical lock because acquire context is same
> as release context for typical lock, which means lockdep would work with
> considering only acquire contexts for typical lock. However, for
> crosslock, lockdep cannot identify release context and any dependency
> until the crosslock will be actually released.
>
> Regarding crosslock, lockdep has to record all history by queueing all
> locks potentially creating dependencies so that real dependencies can be
> added using the history recorded when identifying release context. We
> call it 'commit', that is, to add dependencies in batches. See
> 'Introduce commit' section.
>
> Of course, some actual deadlocks caused by crosslock cannot be detected
> at the time it happened, because the deadlocks cannot be indentified and
> detected until the crosslock will be actually released. But this way
> deadlock possibility can be detected and it's worth just possibility
> detection of deadlock. See 'What lockdep does' section.
>
> CONCLUSION
>
> With crossrelease feature, lockdep can works with what might be released
> in any context, namely, crosslock.
>
>
> Introduce commit
> ----------------
>
> Crossrelease feature names it 'commit' to identify and add dependencies
> into graph in batches. Lockdep is already doing what commit does when
> acquiring a lock, for typical lock. However, that way must be changed
> for crosslock so that it identifies the crosslock's release context
> first and then does commit.
>
> The main reason why lockdep performs additional step, namely commit, for
> crosslock is that some dependencies by crosslock cannot be identified
> until the crosslock's release context is eventually identified, though
> some other dependencies by crosslock can. There are four kinds of
> dependencies to consider.
>
> 1. 'typical lock A -> typical lock B' dependency
>
> Just when acquiring lock B, lockdep can identify the dependency
> between lock A and lock B as it did. Commit is unnecessary.
>
> 2. 'typical lock A -> crosslock b' dependency
>
> Just when acquiring crosslock b, lockdep can identify the dependency
> between lock A and crosslock B as well. Commit is unnecessary, too.
>
> 3. 'crosslock a -> typical lock B' dependency
>
> When acquiring lock B, lockdep cannot identify the dependency. It can
> be identified only when crosslock a is released. Commit is necessary.
>
> 4. 'crosslock a -> crosslock b' dependency
>
> Creating this kind of dependency directly is unnecessary since it can
> be covered by other kinds of dependencies.
>
> Lockdep works without commit during dealing with only typical locks.
> However, it needs to perform commit step, once at least one crosslock is
> acquired, until all crosslocks in progress are released. Introducing
> commit, lockdep performs three steps i.e. acquire, commit and release.
> What lockdep should do in each step is like,
>
> 1. Acquire
>
> 1) For typical lock
>
> Lockdep does what it originally does and queues the lock so
> that lockdep can check dependencies using it at commit step.
>
> 2) For crosslock
>
> The crosslock is added to a global linked list so that lockdep
> can check dependencies using it at commit step.
>
> 2. Commit
>
> 1) For typical lock
>
> N/A.
>
> 2) For crosslock
>
> Lockdep checks and adds dependencies using data saved at acquire
> step, as if the dependencies were added without commit step.
>
> 3. Release
>
> 1) For typical lock
>
> No change.
>
> 2) For crosslock
>
> Lockdep just remove the crosslock from the global linked list,
> to which it was added at acquire step.
>
> CONCLUSION
>
> Lockdep can detect a deadlock or its possibility caused by what might be
> released in any context, using commit step, where it checks and adds
> dependencies in batches.
>
>
> ==============
> Implementation
> ==============
>
> Data structures
> ---------------
>
> Crossrelease feature introduces two main data structures.
>
> 1. pend_lock (or plock)
>
> This is an array embedded in task_struct, for keeping locks queued so
> that real dependencies can be added using them at commit step. So
> this data can be accessed locklessly within the owner context. The
> array is filled when acquiring a typical lock and consumed when doing
> commit. And it's managed in circular manner.
>
> 2. cross_lock (or xlock)
>
> This is a global linked list, for keeping all crosslocks in progress.
> The list grows when acquiring a crosslock and is shrunk when
> releasing the crosslock. lockdep_init_map_crosslock() should be used
> to initialize a crosslock instance instead of lockdep_init_map() so
> that lockdep can recognize it as crosslock.
>
> CONCLUSION
>
> Crossrelease feature uses two main data structures.
>
> 1. A pend_lock array for queueing typical locks in circular manner.
> 2. A cross_lock linked list for managing crosslocks in progress.
>
>
> How crossrelease works
> ----------------------
>
> Let's take look at how crossrelease feature works step by step, starting
> from how lockdep works without crossrelease feaure.
>
> For example, the below is how lockdep works for typical lock.
>
> RELEASE CONTEXT of A (= ACQUIRE CONTEXT of A)
> --------------------
> acquire A
>
> acquire B -> add a dependency 'A -> B'
>
> acquire C -> add a dependency 'B -> C'
>
> release C
>
> release B
>
> release A
>
> where A, B and C are different lock classes, and upper case letters
> represent typical lock.
>
> After adding 'A -> B', the dependency graph will be,
>
> A -> B
>
> where A and B are different lock classes, and upper case letters
> represent typical lock.
>
> And after adding 'B -> C', the graph will be,
>
> A -> B -> C
>
> where A, B and C are different lock classes, and upper case letters
> represent typical lock.
>
> What if applying commit on typical locks? It's not necessary for typical
> lock. Just for showing what commit does.
>
> RELEASE CONTEXT of A (= ACQUIRE CONTEXT of A)
> --------------------
> acquire A -> mark A as started (nothing before, no queueing)
>
> acquire B -> mark B as started and queue B
>
> acquire C -> mark C as started and queue C
>
> release C -> commit C (nothing queued since C started)
>
> release B -> commit B -> add a dependency 'B -> C'
>
> release A -> commit A -> add dependencies 'A -> B' and 'A -> C'
>
> where A, B and C are different lock classes, and upper case letters
> represent typical lock.
>
> After doing commit A, B and C, the dependency graph becomes like,
>
> A -> B -> C
>
> where A, B and C are different lock classes, and upper case letters
> represent typical lock.
>
> NOTE: A dependency 'A -> C' is optimized out.
>
> Here we can see the final graph is same as the graph built without
> commit. Of course the former way leads to finish building the graph
> earlier than the latter way, which means we can detect a deadlock or its
> possibility sooner. So the former way would be prefered if possible. But
> we cannot avoid using the latter way using commit, for crosslock.
>
> Let's look at how commit works for crosslock.
>
> RELEASE CONTEXT of a ACQUIRE CONTEXT of a
> -------------------- --------------------
> acquire a -> mark a as started
>
> (serialized by some means e.g. barrier)
>
> acquire D -> queue D
> acquire B -> queue B
> release D
> acquire C -> add 'B -> C' and queue C
> acquire E -> queue E
> acquire D -> add 'C -> D' and queue D
> release E
> release D
> release a -> commit a -> add 'a -> D' and 'a -> E'
> release C
>
> release B
>
> where a, B,..., E are different lock classes, and upper case letters
> represent typical lock while lower case letters represent crosslock.
>
> When acquiring crosslock a, no dependency can be added since there's
> nothing in the held_locks. However, crossrelease feature marks the
> crosslock as started, which means all locks to acquire from now are
> candidates which might create new dependencies later when identifying
> release context.
>
> When acquiring lock B, lockdep does what it originally does for typical
> lock and additionally queues the lock for later commit to refer to
> because it might be a dependent lock of the crosslock. It does same
> thing on lock C, D and E. And then two dependencies 'a -> D' and 'a -> E'
> are added when identifying the release context, at commit step.
>
> The final graph is, with crossrelease feature using commit,
>
> B -> C -
> \
> -> D
> /
> a -
> \
> -> E
>
> where a, B,..., E are different lock classes, and upper case letters
> represent typical lock while lower case letters represent crosslock.
>
> However, without crossrelease feature, the final graph will be,
>
> B -> C -> D
>
> where B and C are different lock classes, and upper case letters
> represent typical lock.
>
> The former graph has two more dependencies 'a -> D' and 'a -> E' giving
> additional chances to check if they cause a deadlock. This way lockdep
> can detect a deadlock or its possibility caused by crosslock. Again,
> behaviors adding dependencies created by only typical locks are not
> changed at all.
>
> CONCLUSION
>
> Crossrelease works using commit for crosslock, leaving behaviors adding
> dependencies between only typical locks unchanged.
>
>
> =============
> Optimizations
> =============
>
> Avoid duplication
> -----------------
>
> Crossrelease feature uses a cache like what lockdep already uses for
> dependency chains, but this time it's for caching one dependency like
> 'crosslock -> typical lock' crossing between two different context. Once
> that dependency is cached, same dependency will never be added any more.
> Even queueing unnecessary locks is also prevented based on the cache.
>
> CONCLUSION
>
> Crossrelease does not add any duplicate dependency.
>
>
> Avoid lock contention
> ---------------------
>
> To keep all typical locks for later use, crossrelease feature adopts a
> local array embedded in task_struct, which makes accesses to arrays
> lockless by forcing each array to be accessed only within each own
> context. It's like how held_locks is accessed. Lockless implmentation is
> important since typical locks are very frequently accessed.
>
> CONCLUSION
>
> Crossrelease avoids lock contection as far as possible.