요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
Critical section 안에서 생기는 역효과
volatile-considered-harmful.rst:28-50spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);
모든 접근자가 같은 locking rule을 지키면 the_lock을 보유한 동안 다른 CPU나 thread는 shared_data를 변경할 수 없다. Spinlock primitive는 memory barrier 역할도 하므로 compiler가 lock 경계를 넘어 data access를 이동하거나 lock 이전에 추론한 값을 그대로 재사용하지 못하게 한다.
shared_data를 volatile로 선언해도 lock은 여전히 필요하다. 오히려 compiler는 다른 실행 주체가 접근할 수 없는 critical section 내부에서도 register reuse나 중복 load 제거를 하지 못한다. 동시성 안전성은 늘지 않고 code만 느려질 수 있다.
Memory-mapped I/O는 accessor로 접근한다
volatile-considered-harmful.rst:52-59volatile storage class의 전통적인 사용처는 memory-mapped I/O register다. 하지만 Linux kernel은 I/O memory를 raw pointer로 직접 dereference하는 방식을 사용하지 않는다. Architecture에 따라 직접 접근이 동작하지 않거나 필요한 ordering과 access width를 만족하지 못할 수 있기 때문이다.
readl(), writel() 같은 I/O accessor가 compiler 최적화 억제, architecture별 instruction과 ordering 규칙을 캡슐화한다. Register block의 상위 동작을 보호할 lock이 별도로 필요할 수도 있지만, 그 경우에도 driver data에 volatile을 추가하는 것은 해결책이 아니다.
Busy wait에는 cpu_relax를 사용한다
volatile-considered-harmful.rst:61-71while (my_variable != what_i_want)
cpu_relax();
Processor가 값을 반복 확인해야 한다면 loop 안에서 cpu_relax()를 호출한다. Architecture에 따라 전력 소비를 줄이거나 SMT sibling에게 실행 자원을 양보하며, compiler barrier 역할도 한다. 단, cpu_relax()가 data-race 자체를 해결하는 것은 아니므로 값의 게시와 관찰에는 해당 protocol에 맞는 atomic operation이나 READ_ONCE(), barrier가 별도로 필요하다.
Busy waiting은 CPU 시간을 계속 소비하므로 짧고 불가피한 구간에만 사용한다. 기다림이 길어질 수 있다면 wait queue, completion 또는 sleep 가능한 synchronization을 검토한다.
volatile이 의미 있는 드문 경우
volatile-considered-harmful.rst:73-106- 직접 I/O memory access가 가능한 architecture에서 저수준 accessor 구현 자체가 volatile을 내부적으로 사용할 수 있다.
- Memory를 바꾸지만 compiler가 볼 수 있는 다른 side effect가 없는 inline asm은 제거를 막기 위해 asm volatile이 필요할 수 있다.
- jiffies는 특별한 locking 없이 읽을 수 있고 읽을 때마다 값이 달라질 수 있어 역사적으로 volatile이다. 같은 유형의 새 전역 변수를 추가하라는 선례는 아니다.
- I/O device가 coherent memory의 descriptor를 직접 갱신하는 경우, 해당 data pointer에 volatile이 정당할 때가 드물게 있다. Network adapter가 완료 descriptor pointer를 갱신하는 ring이 한 예다.
대부분의 kernel code에는 이 예외가 적용되지 않는다. Volatile을 새로 쓰는 patch는 동시성 model을 잘못 이해한 것으로 보일 가능성이 높아 추가 검토를 받는다. 기존 volatile을 제거하는 patch도 어떤 synchronization이 올바른 가시성과 ordering을 보장하는지 설명해야 한다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. _volatile_considered_harmful:
Why the "volatile" type class should not be used
------------------------------------------------
C programmers have often taken volatile to mean that the variable could be
changed outside of the current thread of execution; as a result, they are
sometimes tempted to use it in kernel code when shared data structures are
being used. In other words, they have been known to treat volatile types
as a sort of easy atomic variable, which they are not. The use of volatile in
kernel code is almost never correct; this document describes why.
The key point to understand with regard to volatile is that its purpose is
to suppress optimization, which is almost never what one really wants to
do. In the kernel, one must protect shared data structures against
unwanted concurrent access, which is very much a different task. The
process of protecting against unwanted concurrency will also avoid almost
all optimization-related problems in a more efficient way.
Like volatile, the kernel primitives which make concurrent access to data
safe (spinlocks, mutexes, memory barriers, etc.) are designed to prevent
unwanted optimization. If they are being used properly, there will be no
need to use volatile as well. If volatile is still necessary, there is
almost certainly a bug in the code somewhere. In properly-written kernel
code, volatile can only serve to slow things down.
Consider a typical block of kernel code::
spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);
If all the code follows the locking rules, the value of shared_data cannot
change unexpectedly while the_lock is held. Any other code which might
want to play with that data will be waiting on the lock. The spinlock
primitives act as memory barriers - they are explicitly written to do so -
meaning that data accesses will not be optimized across them. So the
compiler might think it knows what will be in shared_data, but the
spin_lock() call, since it acts as a memory barrier, will force it to
forget anything it knows. There will be no optimization problems with
accesses to that data.
If shared_data were declared volatile, the locking would still be
necessary. But the compiler would also be prevented from optimizing access
to shared_data _within_ the critical section, when we know that nobody else
can be working with it. While the lock is held, shared_data is not
volatile. When dealing with shared data, proper locking makes volatile
unnecessary - and potentially harmful.
The volatile storage class was originally meant for memory-mapped I/O
registers. Within the kernel, register accesses, too, should be protected
by locks, but one also does not want the compiler "optimizing" register
accesses within a critical section. But, within the kernel, I/O memory
accesses are always done through accessor functions; accessing I/O memory
directly through pointers is frowned upon and does not work on all
architectures. Those accessors are written to prevent unwanted
optimization, so, once again, volatile is unnecessary.
Another situation where one might be tempted to use volatile is
when the processor is busy-waiting on the value of a variable. The right
way to perform a busy wait is::
while (my_variable != what_i_want)
cpu_relax();
The cpu_relax() call can lower CPU power consumption or yield to a
hyperthreaded twin processor; it also happens to serve as a compiler
barrier, so, once again, volatile is unnecessary. Of course, busy-
waiting is generally an anti-social act to begin with.
There are still a few rare situations where volatile makes sense in the
kernel:
- The above-mentioned accessor functions might use volatile on
architectures where direct I/O memory access does work. Essentially,
each accessor call becomes a little critical section on its own and
ensures that the access happens as expected by the programmer.
- Inline assembly code which changes memory, but which has no other
visible side effects, risks being deleted by GCC. Adding the volatile
keyword to asm statements will prevent this removal.
- The jiffies variable is special in that it can have a different value
every time it is referenced, but it can be read without any special
locking. So jiffies can be volatile, but the addition of other
variables of this type is strongly frowned upon. Jiffies is considered
to be a "stupid legacy" issue (Linus's words) in this regard; fixing it
would be more trouble than it is worth.
- Pointers to data structures in coherent memory which might be modified
by I/O devices can, sometimes, legitimately be volatile. A ring buffer
used by a network adapter, where that adapter changes pointers to
indicate which descriptors have been processed, is an example of this
type of situation.
For most code, none of the above justifications for volatile apply. As a
result, the use of volatile is likely to be seen as a bug and will bring
additional scrutiny to the code. Developers who are tempted to use
volatile should take a step back and think about what they are truly trying
to accomplish.
Patches to remove volatile variables are generally welcome - as long as
they come with a justification which shows that the concurrency issues have
been properly thought through.
References
==========
[1] https://lwn.net/Articles/233481/
[2] https://lwn.net/Articles/233482/
Credits
=======
Original impetus and research by Randy Dunlap
Written by Jonathan Corbet
Improvements via comments from Satyam Sharma, Johannes Stezenbach, Jesper
Juhl, Heikki Orsila, H. Peter Anvin, Philipp Hahn, and Stefan
Richter.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
"volatile" 형식 한정자를 사용하지 말아야 하는 이유
1-26C 프로그래머는 흔히 volatile을 현재 실행 중인 스레드의 바깥에서 변수가 변경될 수 있다는 뜻으로 받아들인다. 그 결과 공유 자료 구조를 사용하는 커널 코드에도 volatile을 쓰고 싶은 유혹을 받곤 한다. 다시 말해 volatile 형식을 손쉬운 atomic 변수처럼 취급해 왔지만, volatile은 atomic 변수가 아니다. 커널 코드에서 volatile을 사용하는 것은 거의 언제나 올바르지 않다. 이 문서는 그 이유를 설명한다.
volatile과 관련해 이해해야 할 핵심은 그 목적이 최적화를 억제하는 데 있다는 점이다. 그러나 최적화 억제는 실제로 원하는 일이 거의 아니다. 커널에서는 공유 자료 구조를 원치 않는 동시 접근으로부터 보호해야 하며, 이는 최적화를 막는 일과는 매우 다른 문제다. 원치 않는 동시 접근을 막는 과정은 최적화 때문에 생길 수 있는 거의 모든 문제도 더 효율적인 방식으로 방지한다.
volatile과 마찬가지로 spinlock, mutex, memory barrier 등 자료에 대한 동시 접근을 안전하게 만드는 커널 primitive도 원치 않는 최적화를 막도록 설계되어 있다. 이 primitive를 올바르게 사용한다면 volatile을 함께 사용할 필요가 없다. 그래도 volatile이 필요하다면 코드 어딘가에 버그가 있을 가능성이 거의 확실하다. 올바르게 작성된 커널 코드에서 volatile은 실행 속도를 늦추는 역할밖에 하지 못한다.
Lock으로 보호되는 공유 자료
28-50다음과 같은 전형적인 커널 코드 블록을 생각해 보자.
spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);
모든 코드가 locking 규칙을 따른다면 the_lock을 보유하는 동안 shared_data의 값이 예기치 않게 바뀔 수 없다. 이 자료를 사용하려는 다른 코드는 lock을 기다리게 된다. Spinlock primitive는 memory barrier로 동작하며, 실제로 그렇게 동작하도록 명시적으로 작성되어 있다. 따라서 자료 접근이 spinlock 경계를 넘어가도록 최적화되지 않는다.
컴파일러는 shared_data에 어떤 값이 들어 있을지 알고 있다고 판단할 수 있다. 하지만 memory barrier 역할을 하는 spin_lock() 호출은 컴파일러가 이전에 알고 있던 내용을 잊도록 강제한다. 그러므로 해당 자료에 접근할 때 최적화로 인한 문제가 발생하지 않는다.
shared_data를 volatile로 선언하더라도 locking은 여전히 필요하다. 그와 동시에, 아무도 shared_data를 함께 사용하지 못한다는 사실을 알고 있는 critical section 안에서도 컴파일러가 shared_data 접근을 최적화할 수 없게 된다. Lock을 보유하는 동안 shared_data는 volatile한 자료가 아니다. 공유 자료를 다룰 때 올바른 locking은 volatile을 불필요하게 만들며, volatile을 잠재적으로 해롭게 만든다.
Memory-mapped I/O와 accessor
52-59volatile storage class는 원래 memory-mapped I/O register를 위해 마련되었다. 커널에서도 register 접근은 lock으로 보호해야 한다. 그러나 critical section 안에서 컴파일러가 register 접근을 최적화해 버리는 것 역시 원하지 않는다.
커널에서는 I/O memory에 항상 accessor function을 통해 접근한다. Pointer를 이용해 I/O memory에 직접 접근하는 방식은 권장되지 않으며 모든 architecture에서 동작하지도 않는다. 이 accessor들은 원치 않는 최적화를 방지하도록 작성되어 있다. 따라서 이 경우에도 volatile은 필요하지 않다.
Busy wait
61-71Processor가 어떤 변수의 값을 busy-wait 방식으로 기다릴 때도 volatile을 사용하고 싶을 수 있다. Busy wait를 올바르게 수행하는 방법은 다음과 같다.
while (my_variable != what_i_want)
cpu_relax();
cpu_relax() 호출은 CPU의 전력 소비를 줄이거나 hyperthreading으로 짝을 이룬 다른 processor에 실행 기회를 줄 수 있다. 또한 compiler barrier 역할도 하므로 이 경우에도 volatile은 필요하지 않다. 물론 busy-wait 자체가 일반적으로 다른 실행 주체에 비협조적인 동작이라는 점도 고려해야 한다.
volatile이 의미를 갖는 드문 경우
73-106커널에서도 volatile이 타당한 경우가 조금은 남아 있다.
- 앞에서 언급한 accessor function은 I/O memory 직접 접근이 동작하는 architecture에서 volatile을 사용할 수 있다. 본질적으로 accessor 호출 하나하나가 작은 critical section이 되어 프로그래머가 의도한 대로 접근이 일어나도록 보장한다.
- Memory를 변경하지만 그 밖에 눈에 보이는 side effect가 없는 inline assembly 코드는 GCC에 의해 삭제될 위험이 있다. asm statement에 volatile keyword를 추가하면 이러한 제거를 막는다.
- jiffies 변수는 참조할 때마다 다른 값을 가질 수 있지만 특별한 locking 없이 읽을 수 있다는 점에서 특수하다. 따라서 jiffies는 volatile일 수 있다. 그러나 같은 형식의 변수를 새로 추가하는 것은 강하게 권장되지 않는다. 이 점에서 jiffies는 Linus의 표현을 빌리면 '어리석은 legacy' 문제이며, 이를 고치는 수고가 얻는 이익보다 더 크다.
- I/O device가 변경할 수 있는 coherent memory의 자료 구조를 가리키는 pointer는 때때로 정당하게 volatile일 수 있다. Network adapter가 처리한 descriptor를 표시하기 위해 pointer를 변경하는 ring buffer가 이런 상황의 한 예다.
대부분의 코드에는 위의 어떤 volatile 사용 근거도 적용되지 않는다. 따라서 volatile 사용은 버그로 간주될 가능성이 크며 해당 코드는 추가적인 검토를 받게 된다. Volatile을 사용하고 싶은 개발자는 한 걸음 물러서서 자신이 실제로 달성하려는 일이 무엇인지 생각해야 한다.
Volatile 변수를 제거하는 patch는 일반적으로 환영받는다. 단, 동시성 문제를 올바르게 검토했음을 보여 주는 근거가 patch에 함께 제시되어야 한다.
참고문헌과 기여자
108-125[1] https://lwn.net/Articles/233481/
[2] https://lwn.net/Articles/233482/
최초의 문제 제기와 조사는 Randy Dunlap이 수행했다. Jonathan Corbet이 문서를 작성했다.
Satyam Sharma, Johannes Stezenbach, Jesper Juhl, Heikki Orsila, H. Peter Anvin, Philipp Hahn, Stefan Richter의 의견을 반영해 문서를 개선했다.
volatile은 atomic 변수가 아니다
volatile-considered-harmful.rst:4-26C programmer는 현재 실행 thread 밖에서 값이 바뀔 수 있다는 이유로 shared data에 volatile을 붙이려는 경우가 있다. 그러나 volatile은 atomic read-modify-write, mutual exclusion, cache coherence protocol의 동기화 의미나 CPU memory ordering을 제공하지 않는다.
volatile의 핵심 기능은 compiler optimization을 억제하는 것이다. Kernel에서 필요한 일은 shared data를 원치 않는 concurrent access로부터 보호하는 것이다. 이 둘은 전혀 다른 문제다.
Spinlock, mutex와 memory barrier 같은 kernel primitive는 필요한 범위의 compiler·CPU 재배치를 제한하면서 동시성 protocol까지 표현한다. 이 primitive를 올바르게 사용했다면 volatile은 필요하지 않다. 그런데도 volatile이 필요해 보인다면 locking이나 access protocol에 bug가 있을 가능성을 먼저 의심해야 한다.