요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. _local_ops:
=================================================
Semantics and Behavior of Local Atomic Operations
=================================================
:Author: Mathieu Desnoyers
This document explains the purpose of the local atomic operations, how
to implement them for any given architecture and shows how they can be used
properly. It also stresses on the precautions that must be taken when reading
those local variables across CPUs when the order of memory writes matters.
.. note::
Note that ``local_t`` based operations are not recommended for general
kernel use. Please use the ``this_cpu`` operations instead unless there is
really a special purpose. Most uses of ``local_t`` in the kernel have been
replaced by ``this_cpu`` operations. ``this_cpu`` operations combine the
relocation with the ``local_t`` like semantics in a single instruction and
yield more compact and faster executing code.
Purpose of local atomic operations
==================================
Local atomic operations are meant to provide fast and highly reentrant per CPU
counters. They minimize the performance cost of standard atomic operations by
removing the LOCK prefix and memory barriers normally required to synchronize
across CPUs.
Having fast per CPU atomic counters is interesting in many cases: it does not
require disabling interrupts to protect from interrupt handlers and it permits
coherent counters in NMI handlers. It is especially useful for tracing purposes
and for various performance monitoring counters.
Local atomic operations only guarantee variable modification atomicity wrt the
CPU which owns the data. Therefore, care must taken to make sure that only one
CPU writes to the ``local_t`` data. This is done by using per cpu data and
making sure that we modify it from within a preemption safe context. It is
however permitted to read ``local_t`` data from any CPU: it will then appear to
be written out of order wrt other memory writes by the owner CPU.
Implementation for a given architecture
=======================================
It can be done by slightly modifying the standard atomic operations: only
their UP variant must be kept. It typically means removing LOCK prefix (on
i386 and x86_64) and any SMP synchronization barrier. If the architecture does
not have a different behavior between SMP and UP, including
``asm-generic/local.h`` in your architecture's ``local.h`` is sufficient.
The ``local_t`` type is defined as an opaque ``signed long`` by embedding an
``atomic_long_t`` inside a structure. This is made so a cast from this type to
a ``long`` fails. The definition looks like::
typedef struct { atomic_long_t a; } local_t;
Rules to follow when using local atomic operations
==================================================
* Variables touched by local ops must be per cpu variables.
* *Only* the CPU owner of these variables must write to them.
* This CPU can use local ops from any context (process, irq, softirq, nmi, ...)
to update its ``local_t`` variables.
* Preemption (or interrupts) must be disabled when using local ops in
process context to make sure the process won't be migrated to a
different CPU between getting the per-cpu variable and doing the
actual local op.
* When using local ops in interrupt context, no special care must be
taken on a mainline kernel, since they will run on the local CPU with
preemption already disabled. I suggest, however, to explicitly
disable preemption anyway to make sure it will still work correctly on
-rt kernels.
* Reading the local cpu variable will provide the current copy of the
variable.
* Reads of these variables can be done from any CPU, because updates to
"``long``", aligned, variables are always atomic. Since no memory
synchronization is done by the writer CPU, an outdated copy of the
variable can be read when reading some *other* cpu's variables.
How to use local atomic operations
==================================
::
#include <linux/percpu.h>
#include <asm/local.h>
static DEFINE_PER_CPU(local_t, counters) = LOCAL_INIT(0);
Counting
========
Counting is done on all the bits of a signed long.
In preemptible context, use ``get_cpu_var()`` and ``put_cpu_var()`` around
local atomic operations: it makes sure that preemption is disabled around write
access to the per cpu variable. For instance::
local_inc(&get_cpu_var(counters));
put_cpu_var(counters);
If you are already in a preemption-safe context, you can use
``this_cpu_ptr()`` instead::
local_inc(this_cpu_ptr(&counters));
Reading the counters
====================
Those local counters can be read from foreign CPUs to sum the count. Note that
the data seen by local_read across CPUs must be considered to be out of order
relatively to other memory writes happening on the CPU that owns the data::
long sum = 0;
for_each_online_cpu(cpu)
sum += local_read(&per_cpu(counters, cpu));
If you want to use a remote local_read to synchronize access to a resource
between CPUs, explicit ``smp_wmb()`` and ``smp_rmb()`` memory barriers must be used
respectively on the writer and the reader CPUs. It would be the case if you use
the ``local_t`` variable as a counter of bytes written in a buffer: there should
be a ``smp_wmb()`` between the buffer write and the counter increment and also a
``smp_rmb()`` between the counter read and the buffer read.
Here is a sample module which implements a basic per cpu counter using
``local.h``::
/* test-local.c
*
* Sample module for local.h usage.
*/
#include <asm/local.h>
#include <linux/module.h>
#include <linux/timer.h>
static DEFINE_PER_CPU(local_t, counters) = LOCAL_INIT(0);
static struct timer_list test_timer;
/* IPI called on each CPU. */
static void test_each(void *info)
{
/* Increment the counter from a non preemptible context */
printk("Increment on cpu %d\n", smp_processor_id());
local_inc(this_cpu_ptr(&counters));
/* This is what incrementing the variable would look like within a
* preemptible context (it disables preemption) :
*
* local_inc(&get_cpu_var(counters));
* put_cpu_var(counters);
*/
}
static void do_test_timer(unsigned long data)
{
int cpu;
/* Increment the counters */
on_each_cpu(test_each, NULL, 1);
/* Read all the counters */
printk("Counters read from CPU %d\n", smp_processor_id());
for_each_online_cpu(cpu) {
printk("Read : CPU %d, count %ld\n", cpu,
local_read(&per_cpu(counters, cpu)));
}
mod_timer(&test_timer, jiffies + 1000);
}
static int __init test_init(void)
{
/* initialize the timer that will increment the counter */
timer_setup(&test_timer, do_test_timer, 0);
mod_timer(&test_timer, jiffies + 1);
return 0;
}
static void __exit test_exit(void)
{
timer_shutdown_sync(&test_timer);
}
module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Mathieu Desnoyers");
MODULE_DESCRIPTION("Local Atomic Ops");
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
Local atomic operation의 의미와 동작
1-24Local atomic operation의 의미와 동작 (Semantics and Behavior of Local Atomic Operations)
저자: Mathieu Desnoyers
이 문서는 local atomic operation의 목적, 주어진 아키텍처에서 이를 구현하는 방법, 올바른 사용법을 설명합니다. 또한 메모리 쓰기 순서가 중요할 때 CPU를 가로질러 local 변수를 읽으면서 지켜야 할 주의 사항을 강조합니다.
`local_t` 기반 operation은 일반적인 kernel 용도로 권장되지 않습니다. 정말 특별한 목적이 아니라면 `this_cpu` operation을 사용하십시오. Kernel에서 `local_t`를 사용하던 대부분의 위치는 `this_cpu` operation으로 대체되었습니다. `this_cpu` operation은 relocation과 `local_t`와 유사한 의미를 하나의 instruction으로 결합하므로, 더 작은 code를 만들고 더 빠르게 실행됩니다.
Local atomic operation의 목적
25-45Local atomic operation의 목적 (Purpose of local atomic operations)
Local atomic operation은 빠르고 재진입성이 높은 per-CPU counter를 제공하기 위한 것입니다. CPU 사이의 동기화에 일반적으로 필요한 `LOCK` prefix와 memory barrier를 제거하여 표준 atomic operation의 성능 비용을 줄입니다.
빠른 per-CPU atomic counter는 여러 상황에서 유용합니다. Interrupt handler로부터 보호하기 위해 interrupt를 비활성화할 필요가 없고, NMI handler에서도 일관된 counter를 유지할 수 있습니다. 특히 tracing과 여러 성능 모니터링 counter에 유용합니다.
Local atomic operation은 data를 소유한 CPU에 대해서만 변수 변경의 atomicity를 보장합니다. 따라서 `local_t` data에는 반드시 하나의 CPU만 써야 합니다. 이를 위해 per-CPU data를 사용하고 preemption-safe context 안에서만 수정합니다. 어느 CPU에서든 `local_t` data를 읽는 것은 허용되지만, 그 값은 소유 CPU가 수행한 다른 memory write와 비교해 순서가 뒤바뀐 것처럼 보일 수 있습니다.
아키텍처별 구현
46-62주어진 아키텍처를 위한 구현 (Implementation for a given architecture)
표준 atomic operation을 조금 수정하여 구현할 수 있으며, UP variant만 남겨야 합니다. 일반적으로 i386과 x86_64에서는 `LOCK` prefix를 제거하고 SMP synchronization barrier를 모두 제거합니다. 아키텍처가 SMP와 UP에서 서로 다른 동작을 갖지 않는다면 해당 아키텍처의 `local.h`에서 `asm-generic/local.h`를 include하는 것으로 충분합니다.
`local_t` type은 structure 안에 `atomic_long_t`를 포함하여 opaque `signed long`으로 정의합니다. 이렇게 하면 이 type을 `long`으로 cast하려는 시도가 실패합니다. 정의는 다음과 같습니다.
typedef struct { atomic_long_t a; } local_t;
사용 시 준수 규칙
63-85Local atomic operation을 사용할 때 따라야 할 규칙
- Local operation이 접근하는 변수는 per-CPU 변수여야 합니다.
- 이 변수에는 소유 CPU만 써야 합니다.
- 소유 CPU는 process, irq, softirq, nmi 등 어떤 context에서든 local operation으로 자신의 `local_t` 변수를 갱신할 수 있습니다.
- Process context에서 local operation을 사용할 때는 per-CPU 변수를 얻은 시점과 실제 local operation을 수행하는 시점 사이에 process가 다른 CPU로 migration되지 않도록 preemption 또는 interrupt를 비활성화해야 합니다.
- Mainline kernel의 interrupt context에서는 이미 local CPU에서 preemption이 비활성화된 채 실행되므로 별도 조치가 필요하지 않습니다. 다만 -rt kernel에서도 올바르게 동작하도록 preemption을 명시적으로 비활성화하는 편이 권장됩니다.
- Local CPU 변수를 읽으면 현재 CPU의 복사본을 얻습니다.
- 정렬된 `long` 변수의 update는 항상 atomic이므로 어느 CPU에서든 이 변수를 읽을 수 있습니다. 그러나 writer CPU가 memory synchronization을 수행하지 않으므로 다른 CPU의 변수를 읽을 때 오래된 복사본을 볼 수 있습니다.
Local atomic operation 사용 준비
86-97Local atomic operation 사용법 (How to use local atomic operations)
`linux/percpu.h`와 `asm/local.h`를 include하고 `DEFINE_PER_CPU()`와 `LOCAL_INIT(0)`으로 per-CPU `local_t` counter를 선언하고 초기화합니다.
#include <linux/percpu.h>
#include <asm/local.h>
static DEFINE_PER_CPU(local_t, counters) = LOCAL_INIT(0);
Counter 갱신
98-115Counting
Counting은 `signed long`의 모든 bit를 사용합니다.
Preemptible context에서는 local atomic operation을 `get_cpu_var()`와 `put_cpu_var()`로 감쌉니다. 그러면 per-CPU 변수에 쓰는 동안 preemption이 비활성화됩니다.
local_inc(&get_cpu_var(counters));
put_cpu_var(counters);
이미 preemption-safe context에 있다면 대신 `this_cpu_ptr()`을 사용할 수 있습니다.
local_inc(this_cpu_ptr(&counters));
Counter 읽기와 memory ordering
116-135Counter 읽기 (Reading the counters)
Local counter는 다른 CPU에서도 읽어 합계를 계산할 수 있습니다. 다만 CPU를 가로질러 `local_read()`로 관찰한 data는 그 data를 소유한 CPU에서 일어난 다른 memory write와 비교해 순서가 뒤바뀐 것으로 간주해야 합니다.
long sum = 0;
for_each_online_cpu(cpu)
sum += local_read(&per_cpu(counters, cpu));
Remote `local_read()`를 CPU 사이의 resource access 동기화에 사용하려면 writer CPU와 reader CPU에 각각 명시적인 `smp_wmb()`와 `smp_rmb()` memory barrier가 필요합니다. 예를 들어 `local_t` 변수를 buffer에 기록한 byte 수의 counter로 사용한다면, buffer write와 counter increment 사이에 `smp_wmb()`를 두고 counter read와 buffer read 사이에 `smp_rmb()`를 두어야 합니다.
기본 per-CPU counter 예제 module
136-202다음 sample module은 `local.h`를 사용하여 기본 per-CPU counter를 구현합니다.
/* test-local.c
*
* Sample module for local.h usage.
*/
#include <asm/local.h>
#include <linux/module.h>
#include <linux/timer.h>
static DEFINE_PER_CPU(local_t, counters) = LOCAL_INIT(0);
static struct timer_list test_timer;
/* IPI called on each CPU. */
static void test_each(void *info)
{
/* Increment the counter from a non preemptible context */
printk("Increment on cpu %d\n", smp_processor_id());
local_inc(this_cpu_ptr(&counters));
/* This is what incrementing the variable would look like within a
* preemptible context (it disables preemption) :
*
* local_inc(&get_cpu_var(counters));
* put_cpu_var(counters);
*/
}
static void do_test_timer(unsigned long data)
{
int cpu;
/* Increment the counters */
on_each_cpu(test_each, NULL, 1);
/* Read all the counters */
printk("Counters read from CPU %d\n", smp_processor_id());
for_each_online_cpu(cpu) {
printk("Read : CPU %d, count %ld\n", cpu,
local_read(&per_cpu(counters, cpu)));
}
mod_timer(&test_timer, jiffies + 1000);
}
static int __init test_init(void)
{
/* initialize the timer that will increment the counter */
timer_setup(&test_timer, do_test_timer, 0);
mod_timer(&test_timer, jiffies + 1);
return 0;
}
static void __exit test_exit(void)
{
timer_shutdown_sync(&test_timer);
}
module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Mathieu Desnoyers");
MODULE_DESCRIPTION("Local Atomic Ops");
`test_each()`는 각 CPU에서 IPI context로 실행되어 자신의 counter를 증가시킵니다. 주석에는 preemptible context에서 `get_cpu_var()`와 `put_cpu_var()`를 사용하는 형태도 함께 제시됩니다.
`do_test_timer()`는 `on_each_cpu()`로 모든 counter를 갱신한 뒤, 모든 online CPU의 값을 `local_read()`로 읽어 출력하고 timer를 다시 예약합니다. `test_init()`은 timer를 시작하며 `test_exit()`은 `timer_shutdown_sync()`로 안전하게 종료합니다.
Module metadata는 license를 `GPL`, 저자를 Mathieu Desnoyers, 설명을 `Local Atomic Ops`로 선언합니다.
요약과 해설
local_ops.rst:1-202`local_t`는 소유 CPU 내부에서 빠르고 재진입 가능한 per-CPU counter update를 제공하지만 CPU 사이의 synchronization은 제공하지 않습니다. 일반적인 새 code에서는 relocation과 local semantics를 한 instruction으로 결합하는 `this_cpu` operation을 우선 사용해야 합니다.
Writer는 반드시 per-CPU data의 소유 CPU 하나로 제한하고 process context에서는 migration을 막아야 합니다. 다른 CPU가 값을 읽는 것은 가능하지만 다른 memory write와의 ordering은 보장되지 않으므로 resource 동기화에 사용하려면 `smp_wmb()`와 `smp_rmb()`를 명시해야 합니다.
문서의 예제는 `DEFINE_PER_CPU(local_t, counters)`, `local_inc()`, `this_cpu_ptr()`, `local_read()`를 timer와 IPI에 결합해 모든 CPU의 counter를 갱신하고 합산하는 전체 흐름을 보여 줍니다.