← Documents Documentation/core-api/local_ops.rst GitHub 원문 ↗

Linux 6.18.37 · Core API

Semantics and Behavior of Local Atomic Operations

local_t 기반 per-CPU atomic counter의 목적, 아키텍처 구현 요건, preemption 규칙, remote read의 memory ordering과 실제 module 예제를 설명합니다.

Source pathDocumentation/core-api/local_ops.rst
Source versionLinux v6.18.37
TranslationDUJINLABS 전문 번역 + 해설

요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.

1. 요약·해설

원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.

요약과 해설

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를 갱신하고 합산하는 전체 흐름을 보여 줍니다.

2. 영어 원문 전체

번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.

원문 전체 펼치기
1
2 .. _local_ops:
3
4 =================================================
5 Semantics and Behavior of Local Atomic Operations
6 =================================================
7
8 :Author: Mathieu Desnoyers
9
10
11 This document explains the purpose of the local atomic operations, how
12 to implement them for any given architecture and shows how they can be used
13 properly. It also stresses on the precautions that must be taken when reading
14 those local variables across CPUs when the order of memory writes matters.
15
16 .. note::
17
18 Note that ``local_t`` based operations are not recommended for general
19 kernel use. Please use the ``this_cpu`` operations instead unless there is
20 really a special purpose. Most uses of ``local_t`` in the kernel have been
21 replaced by ``this_cpu`` operations. ``this_cpu`` operations combine the
22 relocation with the ``local_t`` like semantics in a single instruction and
23 yield more compact and faster executing code.
24
25
26 Purpose of local atomic operations
27 ==================================
28
29 Local atomic operations are meant to provide fast and highly reentrant per CPU
30 counters. They minimize the performance cost of standard atomic operations by
31 removing the LOCK prefix and memory barriers normally required to synchronize
32 across CPUs.
33
34 Having fast per CPU atomic counters is interesting in many cases: it does not
35 require disabling interrupts to protect from interrupt handlers and it permits
36 coherent counters in NMI handlers. It is especially useful for tracing purposes
37 and for various performance monitoring counters.
38
39 Local atomic operations only guarantee variable modification atomicity wrt the
40 CPU which owns the data. Therefore, care must taken to make sure that only one
41 CPU writes to the ``local_t`` data. This is done by using per cpu data and
42 making sure that we modify it from within a preemption safe context. It is
43 however permitted to read ``local_t`` data from any CPU: it will then appear to
44 be written out of order wrt other memory writes by the owner CPU.
45
46
47 Implementation for a given architecture
48 =======================================
49
50 It can be done by slightly modifying the standard atomic operations: only
51 their UP variant must be kept. It typically means removing LOCK prefix (on
52 i386 and x86_64) and any SMP synchronization barrier. If the architecture does
53 not have a different behavior between SMP and UP, including
54 ``asm-generic/local.h`` in your architecture's ``local.h`` is sufficient.
55
56 The ``local_t`` type is defined as an opaque ``signed long`` by embedding an
57 ``atomic_long_t`` inside a structure. This is made so a cast from this type to
58 a ``long`` fails. The definition looks like::
59
60 typedef struct { atomic_long_t a; } local_t;
61
62
63 Rules to follow when using local atomic operations
64 ==================================================
65
66 * Variables touched by local ops must be per cpu variables.
67 * *Only* the CPU owner of these variables must write to them.
68 * This CPU can use local ops from any context (process, irq, softirq, nmi, ...)
69 to update its ``local_t`` variables.
70 * Preemption (or interrupts) must be disabled when using local ops in
71 process context to make sure the process won't be migrated to a
72 different CPU between getting the per-cpu variable and doing the
73 actual local op.
74 * When using local ops in interrupt context, no special care must be
75 taken on a mainline kernel, since they will run on the local CPU with
76 preemption already disabled. I suggest, however, to explicitly
77 disable preemption anyway to make sure it will still work correctly on
78 -rt kernels.
79 * Reading the local cpu variable will provide the current copy of the
80 variable.
81 * Reads of these variables can be done from any CPU, because updates to
82 "``long``", aligned, variables are always atomic. Since no memory
83 synchronization is done by the writer CPU, an outdated copy of the
84 variable can be read when reading some *other* cpu's variables.
85
86
87 How to use local atomic operations
88 ==================================
89
90 ::
91
92 #include <linux/percpu.h>
93 #include <asm/local.h>
94
95 static DEFINE_PER_CPU(local_t, counters) = LOCAL_INIT(0);
96
97
98 Counting
99 ========
100
101 Counting is done on all the bits of a signed long.
102
103 In preemptible context, use ``get_cpu_var()`` and ``put_cpu_var()`` around
104 local atomic operations: it makes sure that preemption is disabled around write
105 access to the per cpu variable. For instance::
106
107 local_inc(&get_cpu_var(counters));
108 put_cpu_var(counters);
109
110 If you are already in a preemption-safe context, you can use
111 ``this_cpu_ptr()`` instead::
112
113 local_inc(this_cpu_ptr(&counters));
114
115
116
117 Reading the counters
118 ====================
119
120 Those local counters can be read from foreign CPUs to sum the count. Note that
121 the data seen by local_read across CPUs must be considered to be out of order
122 relatively to other memory writes happening on the CPU that owns the data::
123
124 long sum = 0;
125 for_each_online_cpu(cpu)
126 sum += local_read(&per_cpu(counters, cpu));
127
128 If you want to use a remote local_read to synchronize access to a resource
129 between CPUs, explicit ``smp_wmb()`` and ``smp_rmb()`` memory barriers must be used
130 respectively on the writer and the reader CPUs. It would be the case if you use
131 the ``local_t`` variable as a counter of bytes written in a buffer: there should
132 be a ``smp_wmb()`` between the buffer write and the counter increment and also a
133 ``smp_rmb()`` between the counter read and the buffer read.
134
135
136 Here is a sample module which implements a basic per cpu counter using
137 ``local.h``::
138
139 /* test-local.c
140 *
141 * Sample module for local.h usage.
142 */
143
144
145 #include <asm/local.h>
146 #include <linux/module.h>
147 #include <linux/timer.h>
148
149 static DEFINE_PER_CPU(local_t, counters) = LOCAL_INIT(0);
150
151 static struct timer_list test_timer;
152
153 /* IPI called on each CPU. */
154 static void test_each(void *info)
155 {
156 /* Increment the counter from a non preemptible context */
157 printk("Increment on cpu %d\n", smp_processor_id());
158 local_inc(this_cpu_ptr(&counters));
159
160 /* This is what incrementing the variable would look like within a
161 * preemptible context (it disables preemption) :
162 *
163 * local_inc(&get_cpu_var(counters));
164 * put_cpu_var(counters);
165 */
166 }
167
168 static void do_test_timer(unsigned long data)
169 {
170 int cpu;
171
172 /* Increment the counters */
173 on_each_cpu(test_each, NULL, 1);
174 /* Read all the counters */
175 printk("Counters read from CPU %d\n", smp_processor_id());
176 for_each_online_cpu(cpu) {
177 printk("Read : CPU %d, count %ld\n", cpu,
178 local_read(&per_cpu(counters, cpu)));
179 }
180 mod_timer(&test_timer, jiffies + 1000);
181 }
182
183 static int __init test_init(void)
184 {
185 /* initialize the timer that will increment the counter */
186 timer_setup(&test_timer, do_test_timer, 0);
187 mod_timer(&test_timer, jiffies + 1);
188
189 return 0;
190 }
191
192 static void __exit test_exit(void)
193 {
194 timer_shutdown_sync(&test_timer);
195 }
196
197 module_init(test_init);
198 module_exit(test_exit);
199
200 MODULE_LICENSE("GPL");
201 MODULE_AUTHOR("Mathieu Desnoyers");
202 MODULE_DESCRIPTION("Local Atomic Ops");
203

3. 한국어 전문 번역

영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.

Local atomic operation의 의미와 동작

1-24

Local 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-45

Local 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-85

Local 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-97

Local 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-115

Counting

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-135

Counter 읽기 (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`로 선언합니다.