Mutex guards in the Linux kernel

I found an interresting thread [1] while searching my inbox for something completely unrelated.

Peter Zijistra has written a few cleanup functions that where introduced in v6.4 with this commit:

commit 54da6a0924311c7cf5015533991e44fb8eb12773
Author: Peter Zijlstra <peterz@infradead.org>
Date:   Fri May 26 12:23:48 2023 +0200

    locking: Introduce __cleanup() based infrastructure

    Use __attribute__((__cleanup__(func))) to build:

     - simple auto-release pointers using __free()

     - 'classes' with constructor and destructor semantics for
       scope-based resource management.

     - lock guards based on the above classes.

    Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>

It adds functionality to "guard" locks. The guard wraps the lock, takes ownership of the given mutex and release it as soon the guard leaves the scope. In other words - no more forgotten locks due to early exits.

Compare this to the std::lock_guard class we have in C++.

Although this adds valuable functionality to the core, it's currently not widely used. In fact, it only has two users in the latest (v.6.6) kernel:

	$ git grep -l "guard(mutex)" 
	drivers/gpio/gpio-sim.c
	kernel/sched/core.c

Hands on

I have adapted ( [2], [3]) two of my drivers to make use of the guard locks. The adaptation is quickly made.

The features is located in linux/cleanup.h:

+#include <linux/cleanup.h>

Then we can start to make use of the guards. What I like is that the code will be simplier in two ways:

  • All the mutex_lock-pairs in the same scope could be replaced with guard(mutex)(&your->mutex).
  • The code can now return without taking any taken locks into account.

Together with device managed ( devm ) resources, you will end up with a code that clean up itself pretty good.

A typical adaption to guarded mutexes could look likt this:

	@@ -83,31 +85,26 @@ static int pxrc_open(struct input_dev *input)
		struct pxrc *pxrc = input_get_drvdata(input);
		int retval;

	-       mutex_lock(&pxrc->pm_mutex);
	+       guard(mutex)(&pxrc->pm_mutex);
		retval = usb_submit_urb(pxrc->urb, GFP_KERNEL);
		if (retval) {
			dev_err(&pxrc->intf->dev,
				"%s - usb_submit_urb failed, error: %d\n",
				__func__, retval);
	-               retval = -EIO;
	-               goto out;
	+               return -EIO;
		}

		pxrc->is_open = true;
	-
	-out:
	-       mutex_unlock(&pxrc->pm_mutex);
	-       return retval;
	+       return 0;
	 }

What it does is:

  • Removes the mutex_lock/mutex_unlock pair
  • Simplifies the error handling to just return in case of error
  • No need for the out: label anymore so remove it

Under the hood

The implementation makes use of the __attribute__((cleanup())) attribute that is available for both LLVM [4] and GCC [5].

Here is what the GCC documentation [5] says about the cleanup_function:

cleanup (cleanup_function)
The cleanup attribute runs a function when the variable goes out of scope. This attribute can only be applied to auto function scope variables; it may not be applied to parameters or variables with static storage duration.
The function must take one parameter, a pointer to a type compatible with the variable. The return value of the function (if any) is ignored.

If -fexceptions is enabled, then cleanup_function is run during the stack unwinding that happens during the processing of the exception.
Note that the cleanup attribute does not allow the exception to be caught, only to perform an action. It's undefined what happens if cleanup_function does not return normally.

To illustrate this, consider the following example:

#include <stdio.h>

void cleanup_func (int *x)
{
	printf("Tidy up for x as it's leaving its scope\n");
}

int main(int argc, char **argv)
{
	printf("Start\n");
	{
		int x __attribute__((cleanup(cleanup_func)));
		/* Do stuff */
	}
	printf("Exit\n");
}

We create a variable, x, declared with the cleanup attribute inside of its own scope. This makes that the cleanup_func() will be called as soon x goes out of scope.

Here is the output of the example above:

$ gcc main.c -o main && ./main
Start
Tidy up for x as it's leaving its scope
Exit

As you can see, the cleanup_func() is called in between of Start and Exit - as expected.