Egloos | Log-in
F/OSS study
F/OSS study
[Linux] ACCESS_ONCE()와 volatile
예전부터 ACCESS_ONCE() 매크로의 용법에 대해 풀리지 않는 의문이 있었는데
이번에 LWN에서 이를 해결해 주었기에 여기에 공유하고자 한다.

먼저 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))

보다시피 단순한(?) 한 줄짜리 매크로에 주석은 많이 달려있음을 알 수 있다.
이게 구체적으로 어떤 의미인지 살펴보기로 하자.

예전 글에서 언급한 적이 있듯이
최신의 컴파일러와 CPU는 프로그램을 실행 성능을 높이기 위해 다양한 (최적화) 작업을 수행하고 있으며
여기에서 언급한 merging, refetching, reordering 등의 기법이 이에 해당한다.

주석에서 얘기하듯이 ACCESS_ONCE 매크로는 컴파일러의 코드 생성에만 영향을 미치며
실제 실행 시 CPU가 수행하는 작업에는 영향을 주지 못한다는 것을 기억하자.

또한 한 가지 알아두어야 할 사실은
현재까지의 C 표준에서는 동시성에 대한 지원이 거의 없다는 점이다.
(향후 표준인 C11(?)에서는 이를 위한 언어적인 지원이 추가되었다고 한다.)
따라서 컴파일러는 생성될 코드가 오직 하나의 스레드에서만 실행된다고 가정하며
좀 더 구체적으로는 한 함수를 실행하는 동안 (전역) 변수의 값이 변경되지 않는다고 생각한다.

이를 바탕으로 위에서 언급한 기법들을 하나씩 살펴보도록 할 것이다.
먼저 merging은 여러 번의 메모리 접근을 하나로 합치는 것을 뜻한다.
가장 단순한 경우는 LWN 기사에 인용한 예제에서와 같이 loop에서 나타난다.

먼저 커널의 mutex 코드에서 발췌한 원본 코드를 살펴보도록 하자.

    for (;;) {
        struct task_struct *owner;

        owner = ACCESS_ONCE(lock->owner);
        if (owner && !mutex_spin_on_owner(lock, owner))
            break;

여기서 lock->owner 변수를 매 loop에서 접근하고 있음을 볼 수 있다.
이 함수 내에서는 해당 변수를 변경하는 부분이 없으므로, (ACCESS_ONCE가 없다면)
위에서 말한 대로 컴파일러는 해당 변수가 함수 내에서 변경되지 않는다고 가정하여
다음과 같이 해당 변수를 읽어오는 부분을 매번 수행하는 대신
loop 밖으로 옮겨서 한 번 만 읽어오도록 하는 최적화를 수행할 수 있다.
(이를 LICM - Loop Invariant Code Motion이라고 부르기도 한다.)

    owner = lock->owner;
    for (;;) {
        if (owner && !mutex_spin_on_owner(lock, owner))
            break;

하지만 lock->owner는 (다른 실행 경로에서) mutex를 해지하면 변경되므로
코드가 이렇게 생성되어서는 곤란하다. 따라서 ACCESS_ONCE를 통해 이를 방지할 수 있다.

다른 기법으로 refetching이 있는데 이는 이름대로 변수를 한 번 읽는 대신
여러 번 읽어오도록 하는 것이며, 여기서 ACCESS_ONCE의 이름이 나온 것이라 추측된다.
마찬가지로 위의 예제에서 ACCESS_ONCE가 사용되지 않았다면
컴파일러는 다음과 같은 코드를 생성하게 될 지도 모른다.

    for (;;) {
        if (lock->owner && !mutex_spin_on_owner(lock, lock->owner))
            break;

이 경우도 첫 번째 접근 이후에 lock->owner의 값이 변경된다면
두 번째 접근에서는 다른 값이 읽힐 것이므로, 코드는 올바르게 동작하지 않을 것이다.

이는 특히 레지스터가 부족한 (x86과 같은) 아키텍처에서 문제가 될 수 있는데
컴파일러는 자주 사용되는 변수를 최대한 레지스터에 저장하기를 원하지만
레지스터의 수가 적으므로 다른 일을 하기 위해 현재 저장된 값 대신 다른 값을 사용하다가
나중에 필요해지면 그 값을 다시 메모리로부터 읽어올 수 있기 때문이다.

마지막으로 reordering은 서로 연관되지 않은 두 변수 사이의 접근 순서를 바꾸는 경우인데
일반적으로는 문제가 되지 않을 것이지만 (동기화와 같은?) 특수한 경우에는 이를 조절하고 싶을 수 있다.
(물론 SMP 환경이라면 최소한 memory barrier라도 사용해야 진정한 동기화가 가능할 테지만..)
이 때는 ACCESS_ONCE를 별도의 문장으로 분리해서 사용하라고 언급하고 있는데
이는 아마도 (함수 인자와 같이) 한 문장 내에서 그 실행 순서를 명확히 결정할 수 없는 경우를
방지하기 위한 조언이라 생각되며, 이에 대해서는 별도의 예제를 보이지는 않을 것이다.

지금까지 살펴본 경우에 대해 ACCESS_ONCE를 이용하면 해결이 가능하다고 했는데,
위에서 보다시피 이는 단순히 해당 변수에 volatile 특성을 부여하여 접근하는 것 뿐이다.

변수가 volatile 특성을 가지면 컴파일러는 해당 변수에 대한 메모리 접근이
side-effect를 가진다고 가정하여, 접근 순서 및 횟수 등을 변경하지 않도록 주의를 기울인다.

그렇다면 volatile이 이러한 문제를 모두 해결해 준다면
왜 처음부터 해당 변수를 volatile로 선언하지 않았을까 하는 의문이 들게 된다.

이에 대해서는 Linux Torvalds님이 몇 차례 언급한 바가 있는데
기본적으로 volatile은 컴파일러 최적화를 막기 때문에 사용해서는 안된다는 얘기였다.
동시성에 관련된 문제는 어차피 spinlock과 같은 동기화 기법을 통해 풀어야 하며
해당 변수를 volatile로 선언하는 것은 critical section 내에서의 적법한 최적화까지도
불필요하게 막아버리기 때문에 권장되지 않는다.

또한 ACCESS_ONCE의 주석의 마지막 부분에도 언급되어 있듯이
volatile은 CPU 내부의 메모리 접근 최적화에는 아무런 영향을 주지 못하기 때문에
SMP 환경에서 여러 CPU들이 동시에 실행되는 경우에는 동기화할 수가 없다.
따라서 ACCESS_ONCE의 주 목적은 현재 CPU에서 실행되는 인터럽트 핸들러와의
동기화 만을 위한 것이며, 따라서 해당 context를 잘 이해하고 사용해야 한다.

더구나 프로그래머들에게 volatile을 사용하면 동시성 문제가 해결될 것이라는
잘못된 믿음을 줄 수 있기 때문에 더욱 사용해서는 안된다는 입장이다.
리눅스 커널에서 volatile로 선언된 변수는 아마도 jiffies가 유일할 것이며
(Linus님의 표현에 따르면 "stupid legacy" 때문이다)
새로이 volatile 선언을 추가하는 것은 세심한 주의가 필요할 것이다.


=== 참고 문헌 ===
 * https://lwn.net/Articles/508991/
 * http://www.kernel.org/doc/Documentation/volatile-considered-harmful.txt

by namhyung | 2012/08/12 01:22 | Kernel | 트랙백 | 핑백(1) | 덧글(2)
트랙백 주소 : http://studyfoss.egloos.com/tb/5682616
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Linked at sss : [C] volati.. at 2012/08/23 20:15

... volatile을 쓰면 컴파일러가 최적화를 할 수 있는데도, 그것을 막는 꼴이 되기 때문에 ACCESS_ONCE()를 사용하는 것이 더 좋다.http://studyfoss.egloos.com/5682616 ... more

Commented by smartdolph at 2012/09/06 11:18
우와.. 정말 엄청난 내공이시네요..부럽숩니다...ㅠㅠ 그리고 항상 궁금하던게 있었는데, 컴파일러가 merging, refetching, reordering등의 작업을 수행하는 경우 이를 쉽게 확인하기 위한 방법 없을까요??
Commented by ffdwsfefd at 2014/11/11 20:26
그것은 현실 정보를 매우 도움이 부분과 함께 믿을 수에있어. 난 당신이 약이 특정 유용한 정보를 공유 내용 해요. 당신은 우리 모두가이 같은 정보를 저장해야합니다. 덕분에 기부에 관한 많은.
writing a good essay
http://www.writingessays.biz/

:         :

:

비공개 덧글

◀ 이전 페이지 다음 페이지 ▶

카테고리
General
Application
System
Kernel
Book
Tips
태그
C build patch kernel linux CAaQA3 glibc git algorithm block-layer x86 scm synchronization bash compiler computer-architecture binutils elf perf vcs script gcc emacs SMP blktrace awk sed CARM memory documentation
전체보기
이글루 파인더

최근 등록된 덧글
informsi yang bagus dan b..
by agen qnc at 06/22
informasi yang bagus dan b..
by agen qnc at 06/22
informasi yang bagus dan b..
by agen qnc at 06/22
최근 등록된 트랙백
Tod's Ferrari Homme
by Tods Pas Cher,Kodak did ..
Mocassin Femme
by Mocassins Homme, I got so..
natural garcinia cambogia
by
rss

skin by jiinny


X