ACCESS_ONCE()

ACCESS_ONCE()の使い道というか名前の由来がよく分からんかったから調べてみた。


とりあえず、ソースコードには下記コメントがある。

include/linux/compiler.h

/*
 * Prevent the compiler from merging or refetching accesses.  The compiler
 * is also forbidden from reordering successive instances of ACCESS_ONCE(),
 * but only when the compiler is aware of some particular ordering.  One way
 * to make the compiler aware of ordering is to put the two invocations of
 * ACCESS_ONCE() in different C statements.
 *
 * This macro does absolutely -nothing- to prevent the CPU from reordering,
 * merging, or refetching absolutely anything at any time.  Its main intended
 * use is to mediate communication between process-level code and irq/NMI
 * handlers, all running on the same CPU.
 */
#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))


まあ、volatileが付いているので、コンパイラが最適化とかしちゃわないようにしているんだろうということは分かるんだけど、なんでそれがACCESS_ONCE()って名前になるのかと。ぐーぐるさんに聞いてみたら、Linusの説明を発見。


ACCESS_ONCE (Linus Torvalds)

On Wed, 18 Nov 2009, Luck, Tony wrote:
>
> > Make sure compiler won't do weird things with limits. E.g. fetching
> > them twice may return 2 different values after writable limits are
> > implemented.
>
> -	if (size > task->signal->rlim[RLIMIT_MEMLOCK].rlim_cur)
> +	if (size > ACCESS_ONCE(task->signal->rlim[RLIMIT_MEMLOCK].rlim_cur))
>
> I don't see how this helps.  If someone else is changing limits while
> we are looking at them, then there is a race.  We either get the old
> or the new value.  Using ACCESS_ONCE (which on ia64 forces a "volatile"
> access, which will make the compiler generate "ld.acq" rather than a
> plain "ld") won't make any difference to this race.
>
> Please explain what issue you see with the current code.

The problem may not be in _that_ particular code, but imagine code like
this:

	if (a > MEMORY) {
		do1;
		do2;
		do3;
	} else {
		do2;
	}

where the compiler could actually turn this into (having noticed that
neither "do1" nor "do2" can alias with MEMORY):

	if (a > MEMORY)
		do1;
	do2;
	if (a > MEMORY)
		do3;

and now what you end up having is a situation where it's possible that
"do1" gets executed but "do3" does not (or vice versa).

Notice how when you look at the code, it looks impossible, and then you
get subtle security bugs.

Now, you may say that "but _my_ code doesn't have that "else" statement",
and maybe you're right. In fact, maybe the source code was really just

	if (a > MEMORY)
		return something();
	return do_something_else();

and you are _sure_ that the ACCESS_ONCE() cannot possibly be needed. But
what if those 'something()' and 'do_something_else()' were inlines, and
the compiler internally turns it into

	if (a > MEMORY) {
		ret = something();
	} else {
		ret = do_something_else();
	}
	return ret;

and you now hit the case above where part of it was shared after all, and
the compiler for some strange reason (register reload, whatever) ends up
doing it as two conditionals after all?

The thing is, you can't _prove_ that the compiler won't do it, especially
if you end up changing the code later (without thinking about the fact
that you're loading things without locking).

So the rule is: if you access unlocked values, you use ACCESS_ONCE(). You
don't say "but it can't matter". Because you simply don't know.

			Linus

なるほどです。コンパイラが最適化して、複数のif文に分けられて、複数回当該変数にアクセスしてしまう可能性があるのを避けるのね。



ああ、volatileについても今度ちゃんと調べようと思ってたの思い出した。