Re: [PATCH v2 0/2] Lock and Pointer guards

From: Paolo Bonzini
Date: Mon May 29 2023 - 08:10:48 EST


On Sat, May 27, 2023 at 9:18 PM Linus Torvalds
<torvalds@xxxxxxxxxxxxxxxxxxxx> wrote:
> It's also an example of something people need to be aware of: the
> auto-releasing is not ordered. That may or may not be a problem. It's
> not a problem for two identical locks, but it very much *can* be a
> problem if you mix different locks.

It is guaranteed. It would be nice to have it documented, but if you
look at the intermediate representation of this simple example:

#include <stdio.h>

int print_on_exit(void **f) {
if (*f) puts(*f);
*f = NULL;
}

int main(int argc) {
// prints "second" then "first"
void *__attribute__((__cleanup__(print_on_exit))) cleanup1 = "first";
void *__attribute__((__cleanup__(print_on_exit))) cleanup2 = "second";
}

... the implementation is introducing a scope and reusing the C++
try/finally implementation, and the "optimizations" that are allowed
when functions are not "throwing" anything. This is always the case
for C, so this (from f.c.005t.original, obtained with -c
-fdump-tree-all):

{
// the duplicated initializers are just an artifact of the AST
void * cleanup1 = (void *) "first";
void * cleanup2 = (void *) "second";

void * cleanup1 = (void *) "first";
try
{
void * cleanup2 = (void *) "second";
try
{

}
finally
{
print_on_exit (&cleanup2);
}
}
finally
{
print_on_exit (&cleanup1);
}
}
return 0;

becomes this as soon as exceptions are lowered (from f.c.013t.eh),
which is before any kind of optimization:

int main (int argc)
{
void * cleanup2;
void * cleanup1;
int D.3187;

cleanup1 = "first";
cleanup2 = "second";
print_on_exit (&cleanup2);
print_on_exit (&cleanup1);
cleanup1 = {CLOBBER(eol)};
cleanup2 = {CLOBBER(eol)};
D.3187 = 0;
goto <D.3188>;
<D.3188>:
return D.3187;
}

> So in the crazy above, the RCU lock may be released *after* the
> cgroup_mutex is released. Or before.
>
> I'm not convinced it's ordered even if you end up using explicit
> scopes, which you can obviously still do:

It is, see

void *__attribute__((__cleanup__(print_on_exit))) cleanup1 = "first";
{
void *__attribute__((__cleanup__(print_on_exit)))
cleanup2 = "second";
puts("begin nested scope");
}
puts("back to outer scope");

which prints

begin nested scope
second
back to outer scope
first

This is practically required by glibc's nasty
pthread_cleanup_push/pthread_cleanup_pop macros (the macros are nasty
even if you ignore glibc's implementation, but still):

# define pthread_cleanup_push(routine, arg) \
do { \
struct __pthread_cleanup_frame __clframe \
__attribute__ ((__cleanup__ (__pthread_cleanup_routine))) \
= { .__cancel_routine = (routine), .__cancel_arg = (arg), \
.__do_it = 1 };

/* Remove a cleanup handler installed by the matching pthread_cleanup_push.
If EXECUTE is non-zero, the handler function is called. */
# define pthread_cleanup_pop(execute) \
__clframe.__do_it = (execute); \
} while (0)

If the scope wasn't respected, pthread_cleanup_pop(1) would be broken
because pthread_cleanup_pop() must immediately execute the function if
its argument is nonzero.

> - I think the above is simpler and objectively better in every way
> from the explicitly scoped thing

It is simpler indeed, but a scope-based variant is useful too based on
my experience with QEMU. Maybe things like Python's with statement
have spoiled me, but I can't quite get used to the lone braces in

{
...
{
auto_release(mutex, &mutex);
...
}
...
}

So in QEMU we have both of them. In Linux that would be

auto_release(mutex, &mutex)

and

scoped(mutex, &mutex) {}

> - I do think that the auto-release can be very dangerous for locking,
> and people need to verify me about the ordering. Maybe the freeing
> order is well-defined.

It is but having it documented would be better.

> - I also suspect that to get maximum advantage of this all, we would
> have to get rid of -Wdeclaration-after-statement

Having both a declaration-based and a scope-based variant helps with
that too. You could add _Pragma("GCC diagnostic push/ignore/pop") to
the declaration-based macros but ouch... even without thinking of
whether clang supports it, which I didn't check.

Paolo

> That last point is something that some people will celebrate, but I do
> think it has helped keep our code base somewhat controlled, and made
> people think more about the variables they declare.