mirror of
https://github.com/AuxXxilium/linux_dsm_epyc7002.git
synced 2024-12-26 01:45:38 +07:00
875 lines
26 KiB
Plaintext
875 lines
26 KiB
Plaintext
|
Crossrelease
|
||
|
============
|
||
|
|
||
|
Started by Byungchul Park <byungchul.park@lge.com>
|
||
|
|
||
|
Contents:
|
||
|
|
||
|
(*) Background
|
||
|
|
||
|
- What causes deadlock
|
||
|
- How lockdep works
|
||
|
|
||
|
(*) Limitation
|
||
|
|
||
|
- Limit lockdep
|
||
|
- Pros from the limitation
|
||
|
- Cons from the limitation
|
||
|
- Relax the limitation
|
||
|
|
||
|
(*) Crossrelease
|
||
|
|
||
|
- Introduce crossrelease
|
||
|
- Introduce commit
|
||
|
|
||
|
(*) Implementation
|
||
|
|
||
|
- Data structures
|
||
|
- How crossrelease works
|
||
|
|
||
|
(*) Optimizations
|
||
|
|
||
|
- Avoid duplication
|
||
|
- Lockless for hot paths
|
||
|
|
||
|
(*) APPENDIX A: What lockdep does to work aggresively
|
||
|
|
||
|
(*) APPENDIX B: How to avoid adding false dependencies
|
||
|
|
||
|
|
||
|
==========
|
||
|
Background
|
||
|
==========
|
||
|
|
||
|
What causes deadlock
|
||
|
--------------------
|
||
|
|
||
|
A deadlock occurs when a context is waiting for an event to happen,
|
||
|
which is impossible because another (or the) context who can trigger the
|
||
|
event is also waiting for another (or the) event to happen, which is
|
||
|
also impossible due to the same reason.
|
||
|
|
||
|
For example:
|
||
|
|
||
|
A context going to trigger event C is waiting for event A to happen.
|
||
|
A context going to trigger event A is waiting for event B to happen.
|
||
|
A context going to trigger event B is waiting for event C to happen.
|
||
|
|
||
|
A deadlock occurs when these three wait operations run at the same time,
|
||
|
because event C cannot be triggered if event A does not happen, which in
|
||
|
turn cannot be triggered if event B does not happen, which in turn
|
||
|
cannot be triggered if event C does not happen. After all, no event can
|
||
|
be triggered since any of them never meets its condition to wake up.
|
||
|
|
||
|
A dependency might exist between two waiters and a deadlock might happen
|
||
|
due to an incorrect releationship between dependencies. Thus, we must
|
||
|
define what a dependency is first. A dependency exists between them if:
|
||
|
|
||
|
1. There are two waiters waiting for each event at a given time.
|
||
|
2. The only way to wake up each waiter is to trigger its event.
|
||
|
3. Whether one can be woken up depends on whether the other can.
|
||
|
|
||
|
Each wait in the example creates its dependency like:
|
||
|
|
||
|
Event C depends on event A.
|
||
|
Event A depends on event B.
|
||
|
Event B depends on event C.
|
||
|
|
||
|
NOTE: Precisely speaking, a dependency is one between whether a
|
||
|
waiter for an event can be woken up and whether another waiter for
|
||
|
another event can be woken up. However from now on, we will describe
|
||
|
a dependency as if it's one between an event and another event for
|
||
|
simplicity.
|
||
|
|
||
|
And they form circular dependencies like:
|
||
|
|
||
|
-> C -> A -> B -
|
||
|
/ \
|
||
|
\ /
|
||
|
----------------
|
||
|
|
||
|
where 'A -> B' means that event A depends on event B.
|
||
|
|
||
|
Such circular dependencies lead to a deadlock since no waiter can meet
|
||
|
its condition to wake up as described.
|
||
|
|
||
|
CONCLUSION
|
||
|
|
||
|
Circular dependencies cause a deadlock.
|
||
|
|
||
|
|
||
|
How lockdep works
|
||
|
-----------------
|
||
|
|
||
|
Lockdep tries to detect a deadlock by checking dependencies created by
|
||
|
lock operations, acquire and release. Waiting for a lock corresponds to
|
||
|
waiting for an event, and releasing a lock corresponds to triggering an
|
||
|
event in the previous section.
|
||
|
|
||
|
In short, lockdep does:
|
||
|
|
||
|
1. Detect a new dependency.
|
||
|
2. Add the dependency into a global graph.
|
||
|
3. Check if that makes dependencies circular.
|
||
|
4. Report a deadlock or its possibility if so.
|
||
|
|
||
|
For example, consider a graph built by lockdep that looks like:
|
||
|
|
||
|
A -> B -
|
||
|
\
|
||
|
-> E
|
||
|
/
|
||
|
C -> D -
|
||
|
|
||
|
where A, B,..., E are different lock classes.
|
||
|
|
||
|
Lockdep will add a dependency into the graph on detection of a new
|
||
|
dependency. For example, it will add a dependency 'E -> C' when a new
|
||
|
dependency between lock E and lock C is detected. Then the graph will be:
|
||
|
|
||
|
A -> B -
|
||
|
\
|
||
|
-> E -
|
||
|
/ \
|
||
|
-> C -> D - \
|
||
|
/ /
|
||
|
\ /
|
||
|
------------------
|
||
|
|
||
|
where A, B,..., E are different lock classes.
|
||
|
|
||
|
This graph contains a subgraph which demonstrates circular dependencies:
|
||
|
|
||
|
-> E -
|
||
|
/ \
|
||
|
-> C -> D - \
|
||
|
/ /
|
||
|
\ /
|
||
|
------------------
|
||
|
|
||
|
where C, D and E are different lock classes.
|
||
|
|
||
|
This is the condition under which a deadlock might occur. Lockdep
|
||
|
reports it on detection after adding a new dependency. This is the way
|
||
|
how lockdep works.
|
||
|
|
||
|
CONCLUSION
|
||
|
|
||
|
Lockdep detects a deadlock or its possibility by checking if circular
|
||
|
dependencies were created after adding each new dependency.
|
||
|
|
||
|
|
||
|
==========
|
||
|
Limitation
|
||
|
==========
|
||
|
|
||
|
Limit lockdep
|
||
|
-------------
|
||
|
|
||
|
Limiting lockdep to work on only typical locks e.g. spin locks and
|
||
|
mutexes, which are released within the acquire context, the
|
||
|
implementation becomes simple but its capacity for detection becomes
|
||
|
limited. Let's check pros and cons in next section.
|
||
|
|
||
|
|
||
|
Pros from the limitation
|
||
|
------------------------
|
||
|
|
||
|
Given the limitation, when acquiring a lock, locks in a held_locks
|
||
|
cannot be released if the context cannot acquire it so has to wait to
|
||
|
acquire it, which means all waiters for the locks in the held_locks are
|
||
|
stuck. It's an exact case to create dependencies between each lock in
|
||
|
the held_locks and the lock to acquire.
|
||
|
|
||
|
For example:
|
||
|
|
||
|
CONTEXT X
|
||
|
---------
|
||
|
acquire A
|
||
|
acquire B /* Add a dependency 'A -> B' */
|
||
|
release B
|
||
|
release A
|
||
|
|
||
|
where A and B are different lock classes.
|
||
|
|
||
|
When acquiring lock A, the held_locks of CONTEXT X is empty thus no
|
||
|
dependency is added. But when acquiring lock B, lockdep detects and adds
|
||
|
a new dependency 'A -> B' between lock A in the held_locks and lock B.
|
||
|
They can be simply added whenever acquiring each lock.
|
||
|
|
||
|
And data required by lockdep exists in a local structure, held_locks
|
||
|
embedded in task_struct. Forcing to access the data within the context,
|
||
|
lockdep can avoid racy problems without explicit locks while handling
|
||
|
the local data.
|
||
|
|
||
|
Lastly, lockdep only needs to keep locks currently being held, to build
|
||
|
a dependency graph. However, relaxing the limitation, it needs to keep
|
||
|
even locks already released, because a decision whether they created
|
||
|
dependencies might be long-deferred.
|
||
|
|
||
|
To sum up, we can expect several advantages from the limitation:
|
||
|
|
||
|
1. Lockdep can easily identify a dependency when acquiring a lock.
|
||
|
2. Races are avoidable while accessing local locks in a held_locks.
|
||
|
3. Lockdep only needs to keep locks currently being held.
|
||
|
|
||
|
CONCLUSION
|
||
|
|
||
|
Given the limitation, the implementation becomes simple and efficient.
|
||
|
|
||
|
|
||
|
Cons from the limitation
|
||
|
------------------------
|
||
|
|
||
|
Given the limitation, lockdep is applicable only to typical locks. For
|
||
|
example, page locks for page access or completions for synchronization
|
||
|
cannot work with lockdep.
|
||
|
|
||
|
Can we detect deadlocks below, under the limitation?
|
||
|
|
||
|
Example 1:
|
||
|
|
||
|
CONTEXT X CONTEXT Y CONTEXT Z
|
||
|
--------- --------- ----------
|
||
|
mutex_lock A
|
||
|
lock_page B
|
||
|
lock_page B
|
||
|
mutex_lock A /* DEADLOCK */
|
||
|
unlock_page B held by X
|
||
|
unlock_page B
|
||
|
mutex_unlock A
|
||
|
mutex_unlock A
|
||
|
|
||
|
where A and B are different lock classes.
|
||
|
|
||
|
No, we cannot.
|
||
|
|
||
|
Example 2:
|
||
|
|
||
|
CONTEXT X CONTEXT Y
|
||
|
--------- ---------
|
||
|
mutex_lock A
|
||
|
mutex_lock A
|
||
|
wait_for_complete B /* DEADLOCK */
|
||
|
complete B
|
||
|
mutex_unlock A
|
||
|
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 locks or completions.
|
||
|
|
||
|
|
||
|
Relax the limitation
|
||
|
--------------------
|
||
|
|
||
|
Under the limitation, things to create dependencies are limited to
|
||
|
typical locks. However, synchronization primitives like page locks and
|
||
|
completions, which are allowed to be released in any context, also
|
||
|
create dependencies and can cause a deadlock. So lockdep should track
|
||
|
these locks to do a better job. We have to relax the limitation for
|
||
|
these locks to work with lockdep.
|
||
|
|
||
|
Detecting dependencies is very important for lockdep to work because
|
||
|
adding a dependency means adding an opportunity to check whether it
|
||
|
causes a deadlock. The more lockdep adds dependencies, the more it
|
||
|
thoroughly works. Thus Lockdep has to do its best to detect and add as
|
||
|
many true dependencies into a graph as possible.
|
||
|
|
||
|
For example, considering only typical locks, lockdep builds a graph like:
|
||
|
|
||
|
A -> B -
|
||
|
\
|
||
|
-> E
|
||
|
/
|
||
|
C -> D -
|
||
|
|
||
|
where A, B,..., E are different lock classes.
|
||
|
|
||
|
On the other hand, under the relaxation, additional dependencies might
|
||
|
be created and added. Assuming additional 'FX -> C' and 'E -> GX' are
|
||
|
added thanks to the relaxation, the graph will be:
|
||
|
|
||
|
A -> B -
|
||
|
\
|
||
|
-> E -> GX
|
||
|
/
|
||
|
FX -> C -> D -
|
||
|
|
||
|
where A, B,..., E, FX and GX are different lock classes, and a suffix
|
||
|
'X' is added on non-typical locks.
|
||
|
|
||
|
The latter graph gives us more chances to check circular dependencies
|
||
|
than the former. However, it might suffer performance degradation since
|
||
|
relaxing the limitation, with which design and implementation of lockdep
|
||
|
can be efficient, might introduce inefficiency inevitably. So lockdep
|
||
|
should provide two options, strong detection and efficient detection.
|
||
|
|
||
|
Choosing efficient detection:
|
||
|
|
||
|
Lockdep works with only locks restricted to be released within the
|
||
|
acquire context. However, lockdep works efficiently.
|
||
|
|
||
|
Choosing strong detection:
|
||
|
|
||
|
Lockdep works with all synchronization primitives. However, lockdep
|
||
|
suffers performance degradation.
|
||
|
|
||
|
CONCLUSION
|
||
|
|
||
|
Relaxing the limitation, lockdep can add additional dependencies giving
|
||
|
additional opportunities to check circular dependencies.
|
||
|
|
||
|
|
||
|
============
|
||
|
Crossrelease
|
||
|
============
|
||
|
|
||
|
Introduce crossrelease
|
||
|
----------------------
|
||
|
|
||
|
In order to allow lockdep to handle additional dependencies by what
|
||
|
might be released in any context, namely 'crosslock', we have to be able
|
||
|
to identify those created by crosslocks. The proposed 'crossrelease'
|
||
|
feature provoides a way to do that.
|
||
|
|
||
|
Crossrelease feature has to do:
|
||
|
|
||
|
1. Identify dependencies created by crosslocks.
|
||
|
2. Add the dependencies into a dependency graph.
|
||
|
|
||
|
That's all. Once a meaningful dependency is added into graph, then
|
||
|
lockdep would work with the graph as it did. The most important thing
|
||
|
crossrelease feature has to do is to correctly identify and add true
|
||
|
dependencies into the global graph.
|
||
|
|
||
|
A dependency e.g. 'A -> B' can be identified only in the A's release
|
||
|
context because a decision required to identify the dependency can be
|
||
|
made only in the release context. That is to decide whether A can be
|
||
|
released so that a waiter for A can be woken up. It cannot be made in
|
||
|
other than the A's release context.
|
||
|
|
||
|
It's no matter for typical locks because each acquire context is same as
|
||
|
its release context, thus lockdep can decide whether a lock can be
|
||
|
released in the acquire context. However for crosslocks, lockdep cannot
|
||
|
make the decision in the acquire context but has to wait until the
|
||
|
release context is identified.
|
||
|
|
||
|
Therefore, deadlocks by crosslocks cannot be detected just when it
|
||
|
happens, because those cannot be identified until the crosslocks are
|
||
|
released. However, deadlock possibilities can be detected and it's very
|
||
|
worth. See 'APPENDIX A' section to check why.
|
||
|
|
||
|
CONCLUSION
|
||
|
|
||
|
Using crossrelease feature, lockdep can work with what might be released
|
||
|
in any context, namely crosslock.
|
||
|
|
||
|
|
||
|
Introduce commit
|
||
|
----------------
|
||
|
|
||
|
Since crossrelease defers the work adding true dependencies of
|
||
|
crosslocks until they are actually released, crossrelease has to queue
|
||
|
all acquisitions which might create dependencies with the crosslocks.
|
||
|
Then it identifies dependencies using the queued data in batches at a
|
||
|
proper time. We call it 'commit'.
|
||
|
|
||
|
There are four types of dependencies:
|
||
|
|
||
|
1. TT type: 'typical lock A -> typical lock B'
|
||
|
|
||
|
Just when acquiring B, lockdep can see it's in the A's release
|
||
|
context. So the dependency between A and B can be identified
|
||
|
immediately. Commit is unnecessary.
|
||
|
|
||
|
2. TC type: 'typical lock A -> crosslock BX'
|
||
|
|
||
|
Just when acquiring BX, lockdep can see it's in the A's release
|
||
|
context. So the dependency between A and BX can be identified
|
||
|
immediately. Commit is unnecessary, too.
|
||
|
|
||
|
3. CT type: 'crosslock AX -> typical lock B'
|
||
|
|
||
|
When acquiring B, lockdep cannot identify the dependency because
|
||
|
there's no way to know if it's in the AX's release context. It has
|
||
|
to wait until the decision can be made. Commit is necessary.
|
||
|
|
||
|
4. CC type: 'crosslock AX -> crosslock BX'
|
||
|
|
||
|
When acquiring BX, lockdep cannot identify the dependency because
|
||
|
there's no way to know if it's in the AX's release context. It has
|
||
|
to wait until the decision can be made. Commit is necessary.
|
||
|
But, handling CC type is not implemented yet. It's a future work.
|
||
|
|
||
|
Lockdep can work without commit for typical locks, but commit step is
|
||
|
necessary once crosslocks are involved. Introducing commit, lockdep
|
||
|
performs three steps. What lockdep does in each step is:
|
||
|
|
||
|
1. Acquisition: For typical locks, lockdep does what it originally did
|
||
|
and queues the lock so that CT type dependencies can be checked using
|
||
|
it at the commit step. For crosslocks, it saves data which will be
|
||
|
used at the commit step and increases a reference count for it.
|
||
|
|
||
|
2. Commit: No action is reauired for typical locks. For crosslocks,
|
||
|
lockdep adds CT type dependencies using the data saved at the
|
||
|
acquisition step.
|
||
|
|
||
|
3. Release: No changes are required for typical locks. When a crosslock
|
||
|
is released, it decreases a reference count for it.
|
||
|
|
||
|
CONCLUSION
|
||
|
|
||
|
Crossrelease introduces commit step to handle dependencies of crosslocks
|
||
|
in batches at a proper time.
|
||
|
|
||
|
|
||
|
==============
|
||
|
Implementation
|
||
|
==============
|
||
|
|
||
|
Data structures
|
||
|
---------------
|
||
|
|
||
|
Crossrelease introduces two main data structures.
|
||
|
|
||
|
1. hist_lock
|
||
|
|
||
|
This is an array embedded in task_struct, for keeping lock history so
|
||
|
that dependencies can be added using them at the commit step. Since
|
||
|
it's local data, it can be accessed locklessly in the owner context.
|
||
|
The array is filled at the acquisition step and consumed at the
|
||
|
commit step. And it's managed in circular manner.
|
||
|
|
||
|
2. cross_lock
|
||
|
|
||
|
One per lockdep_map exists. This is for keeping data of crosslocks
|
||
|
and used at the commit step.
|
||
|
|
||
|
|
||
|
How crossrelease works
|
||
|
----------------------
|
||
|
|
||
|
It's the key of how crossrelease works, to defer necessary works to an
|
||
|
appropriate point in time and perform in at once at the commit step.
|
||
|
Let's take a look with examples step by step, starting from how lockdep
|
||
|
works without crossrelease for typical locks.
|
||
|
|
||
|
acquire A /* Push A onto held_locks */
|
||
|
acquire B /* Push B onto held_locks and add 'A -> B' */
|
||
|
acquire C /* Push C onto held_locks and add 'B -> C' */
|
||
|
release C /* Pop C from held_locks */
|
||
|
release B /* Pop B from held_locks */
|
||
|
release A /* Pop A from held_locks */
|
||
|
|
||
|
where A, B and C are different lock classes.
|
||
|
|
||
|
NOTE: This document assumes that readers already understand how
|
||
|
lockdep works without crossrelease thus omits details. But there's
|
||
|
one thing to note. Lockdep pretends to pop a lock from held_locks
|
||
|
when releasing it. But it's subtly different from the original pop
|
||
|
operation because lockdep allows other than the top to be poped.
|
||
|
|
||
|
In this case, lockdep adds 'the top of held_locks -> the lock to acquire'
|
||
|
dependency every time acquiring a lock.
|
||
|
|
||
|
After adding 'A -> B', a dependency graph will be:
|
||
|
|
||
|
A -> B
|
||
|
|
||
|
where A and B are different lock classes.
|
||
|
|
||
|
And after adding 'B -> C', the graph will be:
|
||
|
|
||
|
A -> B -> C
|
||
|
|
||
|
where A, B and C are different lock classes.
|
||
|
|
||
|
Let's performs commit step even for typical locks to add dependencies.
|
||
|
Of course, commit step is not necessary for them, however, it would work
|
||
|
well because this is a more general way.
|
||
|
|
||
|
acquire A
|
||
|
/*
|
||
|
* Queue A into hist_locks
|
||
|
*
|
||
|
* In hist_locks: A
|
||
|
* In graph: Empty
|
||
|
*/
|
||
|
|
||
|
acquire B
|
||
|
/*
|
||
|
* Queue B into hist_locks
|
||
|
*
|
||
|
* In hist_locks: A, B
|
||
|
* In graph: Empty
|
||
|
*/
|
||
|
|
||
|
acquire C
|
||
|
/*
|
||
|
* Queue C into hist_locks
|
||
|
*
|
||
|
* In hist_locks: A, B, C
|
||
|
* In graph: Empty
|
||
|
*/
|
||
|
|
||
|
commit C
|
||
|
/*
|
||
|
* Add 'C -> ?'
|
||
|
* Answer the following to decide '?'
|
||
|
* What has been queued since acquire C: Nothing
|
||
|
*
|
||
|
* In hist_locks: A, B, C
|
||
|
* In graph: Empty
|
||
|
*/
|
||
|
|
||
|
release C
|
||
|
|
||
|
commit B
|
||
|
/*
|
||
|
* Add 'B -> ?'
|
||
|
* Answer the following to decide '?'
|
||
|
* What has been queued since acquire B: C
|
||
|
*
|
||
|
* In hist_locks: A, B, C
|
||
|
* In graph: 'B -> C'
|
||
|
*/
|
||
|
|
||
|
release B
|
||
|
|
||
|
commit A
|
||
|
/*
|
||
|
* Add 'A -> ?'
|
||
|
* Answer the following to decide '?'
|
||
|
* What has been queued since acquire A: B, C
|
||
|
*
|
||
|
* In hist_locks: A, B, C
|
||
|
* In graph: 'B -> C', 'A -> B', 'A -> C'
|
||
|
*/
|
||
|
|
||
|
release A
|
||
|
|
||
|
where A, B and C are different lock classes.
|
||
|
|
||
|
In this case, dependencies are added at the commit step as described.
|
||
|
|
||
|
After commits for A, B and C, the graph will be:
|
||
|
|
||
|
A -> B -> C
|
||
|
|
||
|
where A, B and C are different lock classes.
|
||
|
|
||
|
NOTE: A dependency 'A -> C' is optimized out.
|
||
|
|
||
|
We can see the former graph built without commit step is same as the
|
||
|
latter graph built using commit steps. Of course the former way leads to
|
||
|
earlier finish for building the graph, which means we can detect a
|
||
|
deadlock or its possibility sooner. So the former way would be prefered
|
||
|
when possible. But we cannot avoid using the latter way for crosslocks.
|
||
|
|
||
|
Let's look at how commit steps work for crosslocks. In this case, the
|
||
|
commit step is performed only on crosslock AX as real. And it assumes
|
||
|
that the AX release context is different from the AX acquire context.
|
||
|
|
||
|
BX RELEASE CONTEXT BX ACQUIRE CONTEXT
|
||
|
------------------ ------------------
|
||
|
acquire A
|
||
|
/*
|
||
|
* Push A onto held_locks
|
||
|
* Queue A into hist_locks
|
||
|
*
|
||
|
* In held_locks: A
|
||
|
* In hist_locks: A
|
||
|
* In graph: Empty
|
||
|
*/
|
||
|
|
||
|
acquire BX
|
||
|
/*
|
||
|
* Add 'the top of held_locks -> BX'
|
||
|
*
|
||
|
* In held_locks: A
|
||
|
* In hist_locks: A
|
||
|
* In graph: 'A -> BX'
|
||
|
*/
|
||
|
|
||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
It must be guaranteed that the following operations are seen after
|
||
|
acquiring BX globally. It can be done by things like barrier.
|
||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
|
||
|
acquire C
|
||
|
/*
|
||
|
* Push C onto held_locks
|
||
|
* Queue C into hist_locks
|
||
|
*
|
||
|
* In held_locks: C
|
||
|
* In hist_locks: C
|
||
|
* In graph: 'A -> BX'
|
||
|
*/
|
||
|
|
||
|
release C
|
||
|
/*
|
||
|
* Pop C from held_locks
|
||
|
*
|
||
|
* In held_locks: Empty
|
||
|
* In hist_locks: C
|
||
|
* In graph: 'A -> BX'
|
||
|
*/
|
||
|
acquire D
|
||
|
/*
|
||
|
* Push D onto held_locks
|
||
|
* Queue D into hist_locks
|
||
|
* Add 'the top of held_locks -> D'
|
||
|
*
|
||
|
* In held_locks: A, D
|
||
|
* In hist_locks: A, D
|
||
|
* In graph: 'A -> BX', 'A -> D'
|
||
|
*/
|
||
|
acquire E
|
||
|
/*
|
||
|
* Push E onto held_locks
|
||
|
* Queue E into hist_locks
|
||
|
*
|
||
|
* In held_locks: E
|
||
|
* In hist_locks: C, E
|
||
|
* In graph: 'A -> BX', 'A -> D'
|
||
|
*/
|
||
|
|
||
|
release E
|
||
|
/*
|
||
|
* Pop E from held_locks
|
||
|
*
|
||
|
* In held_locks: Empty
|
||
|
* In hist_locks: D, E
|
||
|
* In graph: 'A -> BX', 'A -> D'
|
||
|
*/
|
||
|
release D
|
||
|
/*
|
||
|
* Pop D from held_locks
|
||
|
*
|
||
|
* In held_locks: A
|
||
|
* In hist_locks: A, D
|
||
|
* In graph: 'A -> BX', 'A -> D'
|
||
|
*/
|
||
|
commit BX
|
||
|
/*
|
||
|
* Add 'BX -> ?'
|
||
|
* What has been queued since acquire BX: C, E
|
||
|
*
|
||
|
* In held_locks: Empty
|
||
|
* In hist_locks: D, E
|
||
|
* In graph: 'A -> BX', 'A -> D',
|
||
|
* 'BX -> C', 'BX -> E'
|
||
|
*/
|
||
|
|
||
|
release BX
|
||
|
/*
|
||
|
* In held_locks: Empty
|
||
|
* In hist_locks: D, E
|
||
|
* In graph: 'A -> BX', 'A -> D',
|
||
|
* 'BX -> C', 'BX -> E'
|
||
|
*/
|
||
|
release A
|
||
|
/*
|
||
|
* Pop A from held_locks
|
||
|
*
|
||
|
* In held_locks: Empty
|
||
|
* In hist_locks: A, D
|
||
|
* In graph: 'A -> BX', 'A -> D',
|
||
|
* 'BX -> C', 'BX -> E'
|
||
|
*/
|
||
|
|
||
|
where A, BX, C,..., E are different lock classes, and a suffix 'X' is
|
||
|
added on crosslocks.
|
||
|
|
||
|
Crossrelease considers all acquisitions after acqiuring BX are
|
||
|
candidates which might create dependencies with BX. True dependencies
|
||
|
will be determined when identifying the release context of BX. Meanwhile,
|
||
|
all typical locks are queued so that they can be used at the commit step.
|
||
|
And then two dependencies 'BX -> C' and 'BX -> E' are added at the
|
||
|
commit step when identifying the release context.
|
||
|
|
||
|
The final graph will be, with crossrelease:
|
||
|
|
||
|
-> C
|
||
|
/
|
||
|
-> BX -
|
||
|
/ \
|
||
|
A - -> E
|
||
|
\
|
||
|
-> D
|
||
|
|
||
|
where A, BX, C,..., E are different lock classes, and a suffix 'X' is
|
||
|
added on crosslocks.
|
||
|
|
||
|
However, the final graph will be, without crossrelease:
|
||
|
|
||
|
A -> D
|
||
|
|
||
|
where A and D are different lock classes.
|
||
|
|
||
|
The former graph has three more dependencies, 'A -> BX', 'BX -> C' and
|
||
|
'BX -> E' giving additional opportunities to check if they cause
|
||
|
deadlocks. This way lockdep can detect a deadlock or its possibility
|
||
|
caused by crosslocks.
|
||
|
|
||
|
CONCLUSION
|
||
|
|
||
|
We checked how crossrelease works with several examples.
|
||
|
|
||
|
|
||
|
=============
|
||
|
Optimizations
|
||
|
=============
|
||
|
|
||
|
Avoid duplication
|
||
|
-----------------
|
||
|
|
||
|
Crossrelease feature uses a cache like what lockdep already uses for
|
||
|
dependency chains, but this time it's for caching CT type dependencies.
|
||
|
Once that dependency is cached, the same will never be added again.
|
||
|
|
||
|
|
||
|
Lockless for hot paths
|
||
|
----------------------
|
||
|
|
||
|
To keep all locks for later use at the commit step, crossrelease adopts
|
||
|
a local array embedded in task_struct, which makes access to the data
|
||
|
lockless by forcing it to happen only within the owner context. It's
|
||
|
like how lockdep handles held_locks. Lockless implmentation is important
|
||
|
since typical locks are very frequently acquired and released.
|
||
|
|
||
|
|
||
|
=================================================
|
||
|
APPENDIX A: What lockdep does to work aggresively
|
||
|
=================================================
|
||
|
|
||
|
A deadlock actually occurs when all wait operations creating circular
|
||
|
dependencies run at the same time. Even though they don't, a potential
|
||
|
deadlock exists if the problematic dependencies exist. Thus it's
|
||
|
meaningful to detect not only an actual deadlock but also its potential
|
||
|
possibility. The latter is rather valuable. When a deadlock occurs
|
||
|
actually, 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.
|
||
|
Lockdep does the both, and crossrelease only focuses on the latter.
|
||
|
|
||
|
Whether or not a deadlock actually occurs depends on several factors.
|
||
|
For example, what order contexts are switched in is a factor. Assuming
|
||
|
circular dependencies exist, a deadlock would occur when contexts are
|
||
|
switched so that all wait operations creating the dependencies run
|
||
|
simultaneously. Thus to detect a deadlock possibility even in the case
|
||
|
that it has not occured yet, lockdep should consider all possible
|
||
|
combinations of dependencies, trying to:
|
||
|
|
||
|
1. Use a global dependency graph.
|
||
|
|
||
|
Lockdep combines all dependencies into one global graph and uses them,
|
||
|
regardless of which context generates them or what order contexts are
|
||
|
switched in. Aggregated dependencies are only considered so they are
|
||
|
prone to be circular if a problem exists.
|
||
|
|
||
|
2. Check dependencies between classes instead of instances.
|
||
|
|
||
|
What actually causes a deadlock are instances of lock. However,
|
||
|
lockdep checks dependencies between classes instead of instances.
|
||
|
This way lockdep can detect a deadlock which has not happened but
|
||
|
might happen in future by others but the same class.
|
||
|
|
||
|
3. Assume all acquisitions lead to waiting.
|
||
|
|
||
|
Although locks might be acquired without waiting which is essential
|
||
|
to create dependencies, lockdep assumes all acquisitions lead to
|
||
|
waiting since it might be true some time or another.
|
||
|
|
||
|
CONCLUSION
|
||
|
|
||
|
Lockdep detects not only an actual deadlock but also its possibility,
|
||
|
and the latter is more valuable.
|
||
|
|
||
|
|
||
|
==================================================
|
||
|
APPENDIX B: How to avoid adding false dependencies
|
||
|
==================================================
|
||
|
|
||
|
Remind what a dependency is. A dependency exists if:
|
||
|
|
||
|
1. There are two waiters waiting for each event at a given time.
|
||
|
2. The only way to wake up each waiter is to trigger its event.
|
||
|
3. Whether one can be woken up depends on whether the other can.
|
||
|
|
||
|
For example:
|
||
|
|
||
|
acquire A
|
||
|
acquire B /* A dependency 'A -> B' exists */
|
||
|
release B
|
||
|
release A
|
||
|
|
||
|
where A and B are different lock classes.
|
||
|
|
||
|
A depedency 'A -> B' exists since:
|
||
|
|
||
|
1. A waiter for A and a waiter for B might exist when acquiring B.
|
||
|
2. Only way to wake up each is to release what it waits for.
|
||
|
3. Whether the waiter for A can be woken up depends on whether the
|
||
|
other can. IOW, TASK X cannot release A if it fails to acquire B.
|
||
|
|
||
|
For another example:
|
||
|
|
||
|
TASK X TASK Y
|
||
|
------ ------
|
||
|
acquire AX
|
||
|
acquire B /* A dependency 'AX -> B' exists */
|
||
|
release B
|
||
|
release AX held by Y
|
||
|
|
||
|
where AX and B are different lock classes, and a suffix 'X' is added
|
||
|
on crosslocks.
|
||
|
|
||
|
Even in this case involving crosslocks, the same rule can be applied. A
|
||
|
depedency 'AX -> B' exists since:
|
||
|
|
||
|
1. A waiter for AX and a waiter for B might exist when acquiring B.
|
||
|
2. Only way to wake up each is to release what it waits for.
|
||
|
3. Whether the waiter for AX can be woken up depends on whether the
|
||
|
other can. IOW, TASK X cannot release AX if it fails to acquire B.
|
||
|
|
||
|
Let's take a look at more complicated example:
|
||
|
|
||
|
TASK X TASK Y
|
||
|
------ ------
|
||
|
acquire B
|
||
|
release B
|
||
|
fork Y
|
||
|
acquire AX
|
||
|
acquire C /* A dependency 'AX -> C' exists */
|
||
|
release C
|
||
|
release AX held by Y
|
||
|
|
||
|
where AX, B and C are different lock classes, and a suffix 'X' is
|
||
|
added on crosslocks.
|
||
|
|
||
|
Does a dependency 'AX -> B' exist? Nope.
|
||
|
|
||
|
Two waiters are essential to create a dependency. However, waiters for
|
||
|
AX and B to create 'AX -> B' cannot exist at the same time in this
|
||
|
example. Thus the dependency 'AX -> B' cannot be created.
|
||
|
|
||
|
It would be ideal if the full set of true ones can be considered. But
|
||
|
we can ensure nothing but what actually happened. Relying on what
|
||
|
actually happens at runtime, we can anyway add only true ones, though
|
||
|
they might be a subset of true ones. It's similar to how lockdep works
|
||
|
for typical locks. There might be more true dependencies than what
|
||
|
lockdep has detected in runtime. Lockdep has no choice but to rely on
|
||
|
what actually happens. Crossrelease also relies on it.
|
||
|
|
||
|
CONCLUSION
|
||
|
|
||
|
Relying on what actually happens, lockdep can avoid adding false
|
||
|
dependencies.
|