← Documents Documentation/locking/preempt-locking.rst GitHub 원문 ↗

Linux 6.18.37 · Locking

Preemptible kernel의 올바른 locking

Per-CPU pointer, CPU-local state와 lock owner를 preemption으로부터 보호하는 세 규칙과 preempt_disable 사용 범위를 설명합니다.

Source pathDocumentation/locking/preempt-locking.rst
Source versionLinux v6.18.37
TranslationDUJINLABS 전문 번역 + 해설

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

1. 요약·해설

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

규칙 1: per-CPU data를 명시적으로 보호한다

preempt-locking.rst:8-39
tux[smp_processor_id()] = some_value;
/* 여기서 preempt되고 다른 CPU에서 재개될 수 있다. */
something = tux[smp_processor_id()];

Per-CPU data는 다른 CPU가 같은 instance를 만지지 않는다는 이유로 SMP lock이 없을 수 있습니다. 하지만 task가 두 access 사이에서 preempt된 뒤 다른 CPU로 migration되면 첫 번째와 두 번째 smp_processor_id()가 달라집니다. 전체 pointer 획득과 사용 구간을 preemption disable 또는 적절한 local lock으로 묶어야 합니다.

규칙 2: context switch가 보존하지 않는 CPU state

preempt-locking.rst:40-56

Architecture별 CPU register나 mode 중 kernel context switch가 자동으로 보존하지 않는 state를 다룰 때는 preemption을 막아야 합니다. 문서의 x86 FPU 예처럼 kernel FPU 사용 중 다른 task로 전환되면 register content가 손상됩니다. kernel_fpu_begin()과 kernel_fpu_end()처럼 해당 architecture helper가 이미 보호를 제공하는 경우 그 API를 사용합니다.

규칙 3: 획득한 task가 해제한다

preempt-locking.rst:57-67

한 task가 lock을 획득하고 다른 task가 대신 해제하게 만들면 owner semantics와 preemption count가 깨집니다. 비동기 작업 완료를 기다려야 한다면 각 task가 자신의 code path에서 lock을 획득하고 해제하고, task 사이의 완료 전달은 completion이나 wait queue 같은 event mechanism으로 분리합니다.

preempt counter와 nesting

preempt-locking.rst:68-124
API효과
preempt_disable()preempt count 증가
preempt_enable()count 감소, 필요하면 즉시 reschedule
preempt_enable_no_resched()count를 줄이되 즉시 reschedule하지 않음
preempt_check_resched()필요한 reschedule 수행
preempt_count()현재 nesting count 조회

Disable과 enable은 중첩 가능하며 마지막 enable에서만 다시 preemptible해집니다. Critical variable의 첫 reference부터 마지막 reference까지 전부 포함해야 합니다. Pointer만 얻을 때 disable하고 pointer를 사용할 때 enable하면 migration 후 잘못된 per-CPU object를 접근합니다.

일반 kernel에서 spinlock 보유나 IRQ disabled 상태가 암묵적으로 preemption을 막을 수 있지만 복잡한 함수 호출을 포함한 code에서 IRQ flag만 믿는 것은 위험합니다. cond_resched()나 printk() 경로가 reschedule check를 만들 수 있으므로 필요한 의미를 explicit primitive로 표현합니다.

Local IRQ disable로 preemption을 막을 때

preempt-locking.rst:125-145

local_irq_disable()과 local_irq_save()도 현재 CPU에서 preemption event를 막을 수 있지만 need_resched가 설정되는 경로를 만들지 않아야 합니다. Lock macro 내부처럼 복원 시점의 preemption check가 보장되는 경우를 제외하면 explicit preemption disable을 우선합니다.

이 문서는 역사적인 일반 kernel semantics를 설명합니다. PREEMPT_RT에서는 spinlock_t와 local_lock의 구현 의미가 달라지므로 locktypes 문서의 RT 규칙을 함께 적용해야 합니다.

2. 영어 원문 전체

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

원문 전체 펼치기
1 ===========================================================================
2 Proper Locking Under a Preemptible Kernel: Keeping Kernel Code Preempt-Safe
3 ===========================================================================
4
5 :Author: Robert Love <rml@tech9.net>
6
7
8 Introduction
9 ============
10
11
12 A preemptible kernel creates new locking issues. The issues are the same as
13 those under SMP: concurrency and reentrancy. Thankfully, the Linux preemptible
14 kernel model leverages existing SMP locking mechanisms. Thus, the kernel
15 requires explicit additional locking for very few additional situations.
16
17 This document is for all kernel hackers. Developing code in the kernel
18 requires protecting these situations.
19
20
21 RULE #1: Per-CPU data structures need explicit protection
22 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23
24
25 Two similar problems arise. An example code snippet::
26
27 struct this_needs_locking tux[NR_CPUS];
28 tux[smp_processor_id()] = some_value;
29 /* task is preempted here... */
30 something = tux[smp_processor_id()];
31
32 First, since the data is per-CPU, it may not have explicit SMP locking, but
33 require it otherwise. Second, when a preempted task is finally rescheduled,
34 the previous value of smp_processor_id may not equal the current. You must
35 protect these situations by disabling preemption around them.
36
37 You can also use put_cpu() and get_cpu(), which will disable preemption.
38
39
40 RULE #2: CPU state must be protected.
41 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
42
43
44 Under preemption, the state of the CPU must be protected. This is arch-
45 dependent, but includes CPU structures and state not preserved over a context
46 switch. For example, on x86, entering and exiting FPU mode is now a critical
47 section that must occur while preemption is disabled. Think what would happen
48 if the kernel is executing a floating-point instruction and is then preempted.
49 Remember, the kernel does not save FPU state except for user tasks. Therefore,
50 upon preemption, the FPU registers will be sold to the lowest bidder. Thus,
51 preemption must be disabled around such regions.
52
53 Note, some FPU functions are already explicitly preempt safe. For example,
54 kernel_fpu_begin and kernel_fpu_end will disable and enable preemption.
55
56
57 RULE #3: Lock acquire and release must be performed by same task
58 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59
60
61 A lock acquired in one task must be released by the same task. This
62 means you can't do oddball things like acquire a lock and go off to
63 play while another task releases it. If you want to do something
64 like this, acquire and release the task in the same code path and
65 have the caller wait on an event by the other task.
66
67
68 Solution
69 ========
70
71
72 Data protection under preemption is achieved by disabling preemption for the
73 duration of the critical region.
74
75 ::
76
77 preempt_enable() decrement the preempt counter
78 preempt_disable() increment the preempt counter
79 preempt_enable_no_resched() decrement, but do not immediately preempt
80 preempt_check_resched() if needed, reschedule
81 preempt_count() return the preempt counter
82
83 The functions are nestable. In other words, you can call preempt_disable
84 n-times in a code path, and preemption will not be reenabled until the n-th
85 call to preempt_enable. The preempt statements define to nothing if
86 preemption is not enabled.
87
88 Note that you do not need to explicitly prevent preemption if you are holding
89 any locks or interrupts are disabled, since preemption is implicitly disabled
90 in those cases.
91
92 But keep in mind that 'irqs disabled' is a fundamentally unsafe way of
93 disabling preemption - any cond_resched() or cond_resched_lock() might trigger
94 a reschedule if the preempt count is 0. A simple printk() might trigger a
95 reschedule. So use this implicit preemption-disabling property only if you
96 know that the affected codepath does not do any of this. Best policy is to use
97 this only for small, atomic code that you wrote and which calls no complex
98 functions.
99
100 Example::
101
102 cpucache_t *cc; /* this is per-CPU */
103 preempt_disable();
104 cc = cc_data(searchp);
105 if (cc && cc->avail) {
106 __free_block(searchp, cc_entry(cc), cc->avail);
107 cc->avail = 0;
108 }
109 preempt_enable();
110 return 0;
111
112 Notice how the preemption statements must encompass every reference of the
113 critical variables. Another example::
114
115 int buf[NR_CPUS];
116 set_cpu_val(buf);
117 if (buf[smp_processor_id()] == -1) printf(KERN_INFO "wee!\n");
118 spin_lock(&buf_lock);
119 /* ... */
120
121 This code is not preempt-safe, but see how easily we can fix it by simply
122 moving the spin_lock up two lines.
123
124
125 Preventing preemption using interrupt disabling
126 ===============================================
127
128
129 It is possible to prevent a preemption event using local_irq_disable and
130 local_irq_save. Note, when doing so, you must be very careful to not cause
131 an event that would set need_resched and result in a preemption check. When
132 in doubt, rely on locking or explicit preemption disabling.
133
134 Note in 2.5 interrupt disabling is now only per-CPU (e.g. local).
135
136 An additional concern is proper usage of local_irq_disable and local_irq_save.
137 These may be used to protect from preemption, however, on exit, if preemption
138 may be enabled, a test to see if preemption is required should be done. If
139 these are called from the spin_lock and read/write lock macros, the right thing
140 is done. They may also be called within a spin-lock protected region, however,
141 if they are ever called outside of this context, a test for preemption should
142 be made. Do note that calls from interrupt context or bottom half/ tasklets
143 are also protected by preemption locks and so may use the versions which do
144 not check preemption.
145

3. 한국어 전문 번역

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

Preemptible kernel에서 새로 생기는 문제

1-18

Robert Love가 작성한 이 문서는 preemptible kernel에서 kernel code를 preempt-safe하게 유지하는 locking 규칙을 설명한다.

Preemptible kernel은 concurrency와 reentrancy라는 새로운 locking 문제를 드러낸다. 이 문제는 SMP에서 생기는 문제와 본질적으로 같다. Linux preemptible kernel model은 기존 SMP locking mechanism을 활용하므로 추가로 명시적인 locking이 필요한 상황은 많지 않지만, kernel code를 작성할 때는 아래 상황을 반드시 보호해야 한다.

규칙 1: per-CPU 자료는 명시적으로 보호한다

21-37
struct this_needs_locking tux[NR_CPUS];
tux[smp_processor_id()] = some_value;
/* task is preempted here... */
something = tux[smp_processor_id()];

이 code에는 두 문제가 있다. 자료가 per-CPU라서 SMP용 명시적 lock이 없을 수 있지만 preemption에 대해서는 보호가 필요하다. 또한 preempt된 task가 다시 schedule될 때는 다른 CPU에서 실행될 수 있으므로 이전 smp_processor_id()와 현재 값이 같다는 보장이 없다.

이 구간을 보호하려면 전후에서 preemption을 disable한다. Preemption을 disable하는 get_cpu()와 다시 enable하는 put_cpu()를 사용할 수도 있다.

규칙 2: CPU state를 보호한다

40-54

Preemption이 가능한 상태에서는 context switch를 거쳐 보존되지 않는 CPU 구조와 state를 보호해야 한다. 구체적인 항목은 architecture에 따라 다르다.

예를 들어 x86에서 kernel이 FPU mode에 들어갔다가 나오는 구간은 preemption을 disable해야 하는 critical section이다. Kernel이 floating-point instruction을 실행하는 도중 preempt되면 문제가 된다. Kernel은 user task가 아닌 kernel code를 위해 FPU state를 일반적으로 저장하지 않기 때문이다.

일부 FPU helper는 이미 preempt-safe하다. kernel_fpu_begin()은 preemption을 disable하고 kernel_fpu_end()는 다시 enable한다.

규칙 3: 같은 task가 lock을 획득하고 해제한다

57-65

한 task가 획득한 lock은 반드시 같은 task가 해제해야 한다. 한 task가 lock을 획득한 뒤 다른 task가 대신 해제하게 만들어서는 안 된다. 비슷한 동작이 필요하다면 같은 code path에서 lock을 획득하고 해제하고, caller는 다른 task가 발생시키는 event를 기다리도록 설계한다.

Preemption 제어 API와 nesting

68-90

Preemption 아래에서 자료를 보호하는 직접적인 방법은 critical region 동안 preemption을 disable하는 것이다.

API동작
preempt_enable()preempt counter를 감소시킨다
preempt_disable()preempt counter를 증가시킨다
preempt_enable_no_resched()counter를 줄이지만 즉시 preempt하지 않는다
preempt_check_resched()필요하면 reschedule한다
preempt_count()현재 preempt counter를 반환한다

이 function은 중첩할 수 있다. 한 code path에서 preempt_disable()을 n번 호출했다면 n번째 preempt_enable()을 호출할 때까지 preemption이 다시 enable되지 않는다. Kernel configuration에서 preemption을 사용하지 않으면 이 statement는 아무 동작도 하지 않도록 정의된다.

어떤 lock이든 보유하고 있거나 interrupt가 disable된 동안에는 preemption도 암묵적으로 disable되므로 별도의 호출이 필요하지 않다.

IRQ disable을 preemption 보호로 사용할 때의 위험

92-98

Interrupt disable 상태에 의존해 preemption을 막는 방식은 근본적으로 안전하지 않다. preempt_count가 0이면 cond_resched()나 cond_resched_lock()이 reschedule을 일으킬 수 있고, 단순한 printk()도 reschedule의 계기가 될 수 있다.

이 암묵적 성질은 해당 code path가 그런 동작을 하지 않는다고 확실히 아는 경우에만 사용한다. 직접 작성했고 복잡한 function을 호출하지 않는 짧은 atomic code로 범위를 제한하는 것이 좋다.

Critical variable의 모든 참조를 감싸기

100-122
cpucache_t *cc; /* this is per-CPU */
preempt_disable();
cc = cc_data(searchp);
if (cc && cc->avail) {
    __free_block(searchp, cc_entry(cc), cc->avail);
    cc->avail = 0;
}
preempt_enable();
return 0;

preempt_disable()과 preempt_enable()은 critical variable을 참조하는 모든 지점을 감싸야 한다.

int buf[NR_CPUS];
set_cpu_val(buf);
if (buf[smp_processor_id()] == -1)
    printf(KERN_INFO "wee!\n");
spin_lock(&buf_lock);
/* ... */

두 번째 code는 preempt-safe하지 않다. spin_lock()을 두 줄 위로 옮겨 per-CPU buffer 접근 전에 획득하면 쉽게 고칠 수 있다.

local_irq_disable()과 local_irq_save()

125-144

local_irq_disable()과 local_irq_save()로 preemption event를 막을 수도 있다. 하지만 need_resched를 설정하고 preemption check로 이어질 event를 일으키지 않도록 매우 주의해야 한다. 확신이 없다면 lock이나 명시적인 preemption disable을 사용한다.

Linux 2.5부터 interrupt disable은 CPU별 local 동작이다. 이 API로 preemption을 막았다가 빠져나올 때 preemption이 enable될 수 있다면 reschedule 필요 여부를 검사해야 한다.

spin_lock과 read/write lock macro가 내부에서 이 API를 호출할 때는 필요한 처리가 수행된다. Spinlock으로 보호된 구간 안에서도 사용할 수 있다. 그 밖의 곳에서 직접 호출한다면 preemption check가 필요하다. Interrupt context, bottom half, tasklet은 preemption lock으로 이미 보호되므로 preemption을 검사하지 않는 variant를 사용할 수 있다.