[go: up one dir, main page]

|
|
Log in / Subscribe / Register

ww-mutex-design.txt

Wait/Wound Deadlock-Proof Mutex Design
======================================

Please read mutex-design.txt first, as it applies to wait/wound mutexes too.

Motivation for WW-Mutexes
-------------------------

GPU's do operations that commonly involve many buffers.  Those buffers
can be shared across contexts/processes, exist in different memory
domains (for example VRAM vs system memory), and so on.  And with
PRIME / dmabuf, they can even be shared across devices.  So there are
a handful of situations where the driver needs to wait for buffers to
become ready.  If you think about this in terms of waiting on a buffer
mutex for it to become available, this presents a problem because
there is no way to guarantee that buffers appear in a execbuf/batch in
the same order in all contexts.  That is directly under control of
userspace, and a result of the sequence of GL calls that an application
makes.	Which results in the potential for deadlock.  The problem gets
more complex when you consider that the kernel may need to migrate the
buffer(s) into VRAM before the GPU operates on the buffer(s), which
may in turn require evicting some other buffers (and you don't want to
evict other buffers which are already queued up to the GPU), but for a
simplified understanding of the problem you can ignore this.

The algorithm that TTM came up with for dealing with this problem is quite
simple.  For each group of buffers (execbuf) that need to be locked, the caller
would be assigned a unique reservation id/ticket, from a global counter.  In
case of deadlock while locking all the buffers associated with a execbuf, the
one with the lowest reservation ticket (i.e. the oldest task) wins, and the one
with the higher reservation id (i.e. the younger task) unlocks all of the
buffers that it has already locked, and then tries again.

In the RDBMS literature this deadlock handling approach is called wait/wound:
The older tasks waits until it can acquire the contended lock. The younger tasks
needs to back off and drop all the locks it is currently holding, i.e. the
younger task is wounded.

Concepts
--------

Compared to normal mutexes two additional concepts/objects show up in the lock
interface for w/w mutexes:

Acquire context: To ensure eventual forward progress it is important the a task
trying to acquire locks doesn't grab a new reservation id, but keeps the one it
acquired when starting the lock acquisition. This ticket is stored in the
acquire context. Furthermore the acquire context keeps track of debugging state
to catch w/w mutex interface abuse.

W/w class: In contrast to normal mutexes the lock class needs to be explicit for
w/w mutexes, since it is required to initialize the acquire context.

Furthermore there are three different classe of w/w lock acquire functions:
- Normal lock acquisition with a context, using ww_mutex_lock
- Slowpath lock acquisition on the contending lock, used by the wounded task
  after having dropped all already acquired locks. These functions have the
  _slow postfix.
- Functions to only acquire a single w/w mutex, which results in the exact same
  semantics as a normal mutex. These functions have the _single postfix.

Of course, all the usual variants for handling wake-ups due to signals are also
provided.

Usage
-----

Three different ways to acquire locks within the same w/w class. Common
definitions for methods 1&2.

static DEFINE_WW_CLASS(ww_class);

struct obj {
	sct ww_mutex lock;
	/* obj data */
};

struct obj_entry {
	struct list_head *list;
	struct obj *obj;
};

Method 1, using a list in execbuf->buffers that's not allowed to be reordered.
This is useful if a list of required objects is already tracked somewhere.
Furthermore the lock helper can use propagate the -EALREADY return code back to
the caller as a signal that an object is twice on the list. This is useful if
the list is constructed from userspace input and the ABI requires userspace to
no have duplicate entries (e.g. for a gpu commandbuffer submission ioctl).

int lock_objs(struct list_head *list, struct ww_acquire_ctx *ctx)
{
	struct obj *res_obj = NULL;
	struct obj_entry *contended_entry = NULL;
	struct obj_entry *entry;

	ww_acquire_init(ctx, &ww_class);

retry:
	list_for_each_entry (list, entry) {
		if (entry == res_obj) {
			res_obj = NULL;
			continue;
		}
		ret = ww_mutex_lock(&entry->obj->lock, ctx);
		if (ret < 0) {
			contended_obj = entry;
			goto err;
		}
	}

	ww_acquire_done(ctx);
	return 0;

err:
	list_for_each_entry_continue_reverse (list, contended_entry, entry)
		ww_mutex_unlock(&entry->obj->lock);

	if (res_obj)
		ww_mutex_unlock(&res_obj->lock);

	if (ret == -EDEADLK) {
		/* we lost out in a seqno race, lock and retry.. */
		ww_mutex_lock_slow(&contended_entry->obj->lock, ctx);
		res_obj = contended_entry->obj;
		goto retry;
	}
	ww_acquire_fini(ctx);

	return ret;
}

Method 2, using a list in execbuf->buffers that can be reordered. Same semantics
of duplicate entry detection using -EALREADY as method 1 above. But the
list-reordering allows for a bit more idiomatic code.

int lock_objs(struct list_head *list, struct ww_acquire_ctx *ctx)
{
	struct obj_entry *entry, *entry2;

	ww_acquire_init(ctx, &ww_class);

	list_for_each_entry (list, entry) {
		ret = ww_mutex_lock(&entry->obj->lock, ctx);
		if (ret < 0) {
			entry2 = entry;

			list_for_each_entry_continue_reverse (list, entry2)
				ww_mutex_unlock(&entry->obj->lock);

			if (ret != -EDEADLK) {
				ww_acquire_fini(ctx);
				return ret;
			}

			/* we lost out in a seqno race, lock and retry.. */
			ww_mutex_lock_slow(&entry->obj->lock, ctx);

			/*
			 * Move buf to head of the list, this will point
			 * buf->next to the first unlocked entry,
			 * restarting the for loop.
			 */
			list_del(&entry->list);
			list_add(&entry->list, list);
		}
	}

	ww_acquire_done(ctx);
	return 0;
}

Unlocking works the same way for both methods 1&2:

void unlock_objs(struct list_head *list, struct ww_acquire_ctx *ctx)
{
	struct obj_entry *entry;

	list_for_each_entry (list, entry)
		ww_mutex_unlock(&entry->obj->lock);

	ww_acquire_fini(ctx);
}

Method 3 is useful if the list of objects is constructed ad-hoc and not upfront,
e.g. when adjusting edges in a graph where each node has its own ww_mutex lock,
and edges can only be changed when holding the locks of all involved nodes. w/w
mutexes are a natural fit for such a case for two reasons:
- They can handle lock-acquisition in any order which allows us to start walking
  a graph from a starting point and then iteratively discovering new edges and
  locking down the nodes those edges connect to.
- Due to the -EALREADY return code signalling that a given objects is already
  held there's no need for additional book-keeping to break cycles in the graph
  or keep track off which looks are already held (when using more than one node
  as a starting point).

Note that this approach differs in two important ways from the above methods:
- Since the list of objects is dynamically constructed (and might very well be
  different when retrying due to hitting the -EDEADLK wound condition) there's
  no need to keep any object on a persistent list when it's not locked. We can
  therefore move the list_head into the object itself.
- Otoh the dynamic object list construction also means that the -EALREADY return
  code can't be propagated.

Note also that methodes 1&2 and method 3 can be combined, e.g. to first lock a
list of starting nodes (passed in from userspace) using one of the above
methods. And then lock any additional objects affected by the operations using
method 3 below. The backoff/retry procedure will be a bit more involved, since
when the dynamic locking step hits -EDEADLK we also need to unlock all the
objects acquired with the fixed list. But the w/w mutex debug checks will catch
any interface misuse for these cases.

Also, method 3 can't fail the lock acquisition step since it doesn't return
-EALREADY. Of course this would be different when using the _interruptible
variants, but that's outside of the scope of these examples here.

struct obj {
	struct ww_mutex ww_mutex;
	struct list_head locked_list;
};

static DEFINE_WW_CLASS(ww_class);

void __unlock_objs(struct list_head *list, struct ww_acquire_ctx *ctx)
{
	struct obj entry;

	for_each_safe (list, entry) {
		/* need to do that before unlocking, since only the current lock holder is
		allowed to use object */
		list_del(entry->locked_list);
		ww_mutex_unlock(entry->ww_mutex)
	}
}

void lock_objs(struct list_head *list, struct ww_acquire_ctx *ctx)
{
	struct list_head locked_buffers;
	struct obj obj = NULL, entry;

	ww_acquire_init(ctx, &ww_class);

retry:
	/* re-init loop start state */
	loop {
		/* magic code which walks over a graph and decides which objects
		 * to lock */

		ret = ww_mutex_lock(obj->ww_mutex, ctx);
		if (ret == -EALREADY) {
			/* we have that one already, get to the next object */
			continue;
		}
		if (ret == -EDEADLK) {
			__unlock_objs(list, ctx);

			ww_mutex_lock_slow(obj);
			list_add(locked_buffers, entry->locked_list);
			goto retry;
		}

		/* locked a new object, add it to the list */
		list_add(locked_buffers, entry->locked_list);
	}

	ww_acquire_done(ctx);
	return 0;
}

void unlock_objs(struct list_head *list, struct ww_acquire_ctx *ctx)
{
	__unlock_objs(list, ctx);
	ww_acquire_fini(ctx);
}

Method 4: Only lock one single objects. In that case deadlock detection and
prevention is obviously overkill, since with grabbing just one lock you can't
produce a deadlock within just one class. To simplify this case the w/w mutex
api provides a set of lock/unlock_single functions which don't require an
acquire context.

Implementation Details
----------------------

Design:
  ww_mutex currently encapsulates a struct mutex, this means no extra overhead for
  normal mutex locks, which are far more common. As such there is only a small
  increase in code size if wait/wound mutexes are not used.

  In general, not much contention is expected. The locks are typically used to
  serialize access to resources for devices. The only way to make wakeups
  smarter would be at the cost of adding a field to struct mutex_waiter. This
  would add overhead to all cases where normal mutexes are used, and
  ww_mutexes are generally less performance sensitive.

Lockdep:
  Special care has been taken to warn for as many cases of api abuse
  as possible. Some common api abuses will be caught with
  CONFIG_DEBUG_MUTEXES, but CONFIG_PROVE_LOCKING is recommended.

  Some of the errors which will be warned about:
   - Forgetting to call ww_acquire_fini or ww_acquire_init.
   - Attempting to lock more mutexes after ww_acquire_done.
   - Attempting to lock more mutexes after -EDEADLK,
     before calling ww_mutex_lock_slow.

   - Calling ww_mutex_lock_slow with while still holding some mutexes.
   - Calling ww_mutex_lock_slow on the wrong mutex,
     or before -EDEADLK was returned.

   - Unlocking mutexes with the wrong unlock function.
   - Calling one of the ww_acquire_* twice on the same context.
   - Using a different ww_class for the mutex than for the ww_acquire_ctx.
   - Normal lockdep errors that can result in deadlocks.

  Some of the lockdep errors that can result in deadlocks:
   - Calling ww_acquire_init to initialize a second ww_acquire_ctx before
     having called ww_acquire_fini on the first.
   - Mixing ww_mutex_lock and ww_mutex_lock_single.
   - 'normal' deadlocks that can occur.

FIXME: Update this section once we have the TASK_DEADLOCK task state flag magic
implemented.


to post comments


Copyright © 2013, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds