Thread (1 message) 1 message, 1 author, 2020-05-13

Re: [RFC 02/17] dma-fence: basic lockdep annotations

flat view

From: Daniel Vetter <hidden>
Date: 2020-05-13 08:30:33
Also in: amd-gfx, dri-devel, intel-gfx, linux-media, lkml

On Tue, May 12, 2020 at 11:19 AM Chris Wilson [off-list ref] wrote:
Quoting Daniel Vetter (2020-05-12 10:08:47)
quoted
On Tue, May 12, 2020 at 10:04:22AM +0100, Chris Wilson wrote:
quoted
Quoting Daniel Vetter (2020-05-12 09:59:29)
quoted
Design is similar to the lockdep annotations for workers, but with
some twists:

- We use a read-lock for the execution/worker/completion side, so that
  this explicit annotation can be more liberally sprinkled around.
  With read locks lockdep isn't going to complain if the read-side
  isn't nested the same way under all circumstances, so ABBA deadlocks
  are ok. Which they are, since this is an annotation only.

- We're using non-recursive lockdep read lock mode, since in recursive
  read lock mode lockdep does not catch read side hazards. And we
  _very_ much want read side hazards to be caught. For full details of
  this limitation see

  commit e91498589746065e3ae95d9a00b068e525eec34f
  Author: Peter Zijlstra [off-list ref]
  Date:   Wed Aug 23 13:13:11 2017 +0200

      locking/lockdep/selftests: Add mixed read-write ABBA tests

- To allow nesting of the read-side explicit annotations we explicitly
  keep track of the nesting. lock_is_held() allows us to do that.

- The wait-side annotation is a write lock, and entirely done within
  dma_fence_wait() for everyone by default.

- To be able to freely annotate helper functions I want to make it ok
  to call dma_fence_begin/end_signalling from soft/hardirq context.
  First attempt was using the hardirq locking context for the write
  side in lockdep, but this forces all normal spinlocks nested within
  dma_fence_begin/end_signalling to be spinlocks. That bollocks.

  The approach now is to simple check in_atomic(), and for these cases
  entirely rely on the might_sleep() check in dma_fence_wait(). That
  will catch any wrong nesting against spinlocks from soft/hardirq
  contexts.

The idea here is that every code path that's critical for eventually
signalling a dma_fence should be annotated with
dma_fence_begin/end_signalling. The annotation ideally starts right
after a dma_fence is published (added to a dma_resv, exposed as a
sync_file fd, attached to a drm_syncobj fd, or anything else that
makes the dma_fence visible to other kernel threads), up to and
including the dma_fence_wait(). Examples are irq handlers, the
scheduler rt threads, the tail of execbuf (after the corresponding
fences are visible), any workers that end up signalling dma_fences and
really anything else. Not annotated should be code paths that only
complete fences opportunistically as the gpu progresses, like e.g.
shrinker/eviction code.

The main class of deadlocks this is supposed to catch are:

Thread A:

        mutex_lock(A);
        mutex_unlock(A);

        dma_fence_signal();

Thread B:

        mutex_lock(A);
        dma_fence_wait();
        mutex_unlock(A);

Thread B is blocked on A signalling the fence, but A never gets around
to that because it cannot acquire the lock A.

Note that dma_fence_wait() is allowed to be nested within
dma_fence_begin/end_signalling sections. To allow this to happen the
read lock needs to be upgraded to a write lock, which means that any
other lock is acquired between the dma_fence_begin_signalling() call and
the call to dma_fence_wait(), and still held, this will result in an
immediate lockdep complaint. The only other option would be to not
annotate such calls, defeating the point. Therefore these annotations
cannot be sprinkled over the code entirely mindless to avoid false
positives.

v2: handle soft/hardirq ctx better against write side and dont forget
EXPORT_SYMBOL, drivers can't use this otherwise.

Cc: linux-media@vger.kernel.org
Cc: linaro-mm-sig@lists.linaro.org
Cc: linux-rdma@vger.kernel.org
Cc: amd-gfx@lists.freedesktop.org
Cc: intel-gfx@lists.freedesktop.org
Cc: Chris Wilson <redacted>
Cc: Maarten Lankhorst <maarten.lankhorst@linux.intel.com>
Cc: Christian König <christian.koenig@amd.com>
Signed-off-by: Daniel Vetter <redacted>
---
 drivers/dma-buf/dma-fence.c | 53 +++++++++++++++++++++++++++++++++++++
 include/linux/dma-fence.h   | 12 +++++++++
 2 files changed, 65 insertions(+)
diff --git a/drivers/dma-buf/dma-fence.c b/drivers/dma-buf/dma-fence.c
index 6802125349fb..d5c0fd2efc70 100644
--- a/drivers/dma-buf/dma-fence.c
+++ b/drivers/dma-buf/dma-fence.c
@@ -110,6 +110,52 @@ u64 dma_fence_context_alloc(unsigned num)
 }
 EXPORT_SYMBOL(dma_fence_context_alloc);

+#ifdef CONFIG_LOCKDEP
+struct lockdep_map     dma_fence_lockdep_map = {
+       .name = "dma_fence_map"
+};
Not another false global sharing lockmap.
It's a global contract, it needs a global lockdep map. And yes a big
reason for the motivation here is that i915-gem has a tremendous urge to
just redefine all these global locks to fit to some local interpretation
of what's going on.
No, you can build the global contract out of the actual contracts
between fence drivers. If you introduce a struct lockdep_map *map into
the fence_ops (so the fence_ops can remain const), you gain correctness
at the cost of having to run through all possible interactions once.
You can also then do if ops->lockmap ?: &global_fence_lockmap for
piecemeal conversion of drivers that do not already use lockmaps for
contract enforcement of their fence waits.
I'm not quite sure whether you're actually proposing to have locking
contracts per drivers, since that seems rather out of ... I dunno. But
if that's what you want, that just doesn't make any sense at all:

- Locking is rather core to kernel programming, aside from a few other
things like hard/softirq/preempt/... disabled sections and how
recursion works for these, or where and what you're allowed to
allocate memory. Lockdep, might_sleep and a bunch of other such debug
checks help us enforce that. If you instead go with every driver does
what they please yolo, then you don't have an abstraction, all you
have is smashing a rose and rose and Rose into one thing because they
have the same 4 letter name. It's just an interface that can be used
only when understanding every single implementation in detail - really
not something that's an abstraction. Yes I've seen some of these
dubious abstractions in i915, merged fairly recently, that doesn't
make them a good idea.

- You need to test the full NxN matrix (yes you need to test the
driver against itself in this world, since testing against something
fake like vgem doesn't cut it). That's nuts. Strike that, that's
impossible.

- Review is impossible, because the documentation can be summed up as
"yolo". Without clear rules all review can do is check every code
against every other piece of code, on every change. That's impossible,
because we humans are mere mortals, and we're left with push&pray
engineering, which really isn't.

The other issue with this approach is that it's full on platform
problem in extremis. Instead of extending the shared abstraction or
adding new useful functionality, i915-gem has resorted to reinpreting
rules to fix local problems. That leads to stuff like roughly

if (mutex_lock_timeout(HZ*10) == -ETIME) {
    /* I guess we deadlocked, try to bail out */
}

except it's for fences. That's neither solid engineering - we don't
generally let the kernel deadlock on itself to test whether maybe it
was a deadlock or not, nor is this solid upstreaming in a open source
project - we fix the problems where they are, not work around them
just in our own driver.
-Daniel
--
Daniel Vetter
Software Engineer, Intel Corporation
+41 (0) 79 365 57 48 - http://blog.ffwll.ch
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help