← Documents Documentation/RCU/NMI-RCU.rst GitHub 원문 ↗

Linux 6.18.37 · RCU

동적 NMI handler를 RCU로 보호하기

RCU로 동적 NMI 함수 포인터를 게시하고 기존 handler 실행이 끝난 뒤 데이터를 회수하는 protocol입니다.

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

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

1. 요약·해설

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

요약·해설

NMI-RCU.rst:1-123

RCU로 동적 NMI 함수 포인터를 게시하고 기존 handler 실행이 끝난 뒤 데이터를 회수하는 protocol입니다.

2. 영어 원문 전체

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

원문 전체 펼치기
1 .. _NMI_rcu_doc:
2
3 Using RCU to Protect Dynamic NMI Handlers
4 =========================================
5
6
7 Although RCU is usually used to protect read-mostly data structures,
8 it is possible to use RCU to provide dynamic non-maskable interrupt
9 handlers, as well as dynamic irq handlers. This document describes
10 how to do this, drawing loosely from Zwane Mwaikambo's NMI-timer
11 work in an old version of "arch/x86/kernel/traps.c".
12
13 The relevant pieces of code are listed below, each followed by a
14 brief explanation::
15
16 static int dummy_nmi_callback(struct pt_regs *regs, int cpu)
17 {
18 return 0;
19 }
20
21 The dummy_nmi_callback() function is a "dummy" NMI handler that does
22 nothing, but returns zero, thus saying that it did nothing, allowing
23 the NMI handler to take the default machine-specific action::
24
25 static nmi_callback_t nmi_callback = dummy_nmi_callback;
26
27 This nmi_callback variable is a global function pointer to the current
28 NMI handler::
29
30 void do_nmi(struct pt_regs * regs, long error_code)
31 {
32 int cpu;
33
34 nmi_enter();
35
36 cpu = smp_processor_id();
37 ++nmi_count(cpu);
38
39 if (!rcu_dereference_sched(nmi_callback)(regs, cpu))
40 default_do_nmi(regs);
41
42 nmi_exit();
43 }
44
45 The do_nmi() function processes each NMI. It first disables preemption
46 in the same way that a hardware irq would, then increments the per-CPU
47 count of NMIs. It then invokes the NMI handler stored in the nmi_callback
48 function pointer. If this handler returns zero, do_nmi() invokes the
49 default_do_nmi() function to handle a machine-specific NMI. Finally,
50 preemption is restored.
51
52 In theory, rcu_dereference_sched() is not needed, since this code runs
53 only on i386, which in theory does not need rcu_dereference_sched()
54 anyway. However, in practice it is a good documentation aid, particularly
55 for anyone attempting to do something similar on Alpha or on systems
56 with aggressive optimizing compilers.
57
58 Quick Quiz:
59 Why might the rcu_dereference_sched() be necessary on Alpha, given that the code referenced by the pointer is read-only?
60
61 :ref:`Answer to Quick Quiz <answer_quick_quiz_NMI>`
62
63 Back to the discussion of NMI and RCU::
64
65 void set_nmi_callback(nmi_callback_t callback)
66 {
67 rcu_assign_pointer(nmi_callback, callback);
68 }
69
70 The set_nmi_callback() function registers an NMI handler. Note that any
71 data that is to be used by the callback must be initialized up -before-
72 the call to set_nmi_callback(). On architectures that do not order
73 writes, the rcu_assign_pointer() ensures that the NMI handler sees the
74 initialized values::
75
76 void unset_nmi_callback(void)
77 {
78 rcu_assign_pointer(nmi_callback, dummy_nmi_callback);
79 }
80
81 This function unregisters an NMI handler, restoring the original
82 dummy_nmi_handler(). However, there may well be an NMI handler
83 currently executing on some other CPU. We therefore cannot free
84 up any data structures used by the old NMI handler until execution
85 of it completes on all other CPUs.
86
87 One way to accomplish this is via synchronize_rcu(), perhaps as
88 follows::
89
90 unset_nmi_callback();
91 synchronize_rcu();
92 kfree(my_nmi_data);
93
94 This works because (as of v4.20) synchronize_rcu() blocks until all
95 CPUs complete any preemption-disabled segments of code that they were
96 executing.
97 Since NMI handlers disable preemption, synchronize_rcu() is guaranteed
98 not to return until all ongoing NMI handlers exit. It is therefore safe
99 to free up the handler's data as soon as synchronize_rcu() returns.
100
101 Important note: for this to work, the architecture in question must
102 invoke nmi_enter() and nmi_exit() on NMI entry and exit, respectively.
103
104 .. _answer_quick_quiz_NMI:
105
106 Answer to Quick Quiz:
107 Why might the rcu_dereference_sched() be necessary on Alpha, given that the code referenced by the pointer is read-only?
108
109 The caller to set_nmi_callback() might well have
110 initialized some data that is to be used by the new NMI
111 handler. In this case, the rcu_dereference_sched() would
112 be needed, because otherwise a CPU that received an NMI
113 just after the new handler was set might see the pointer
114 to the new NMI handler, but the old pre-initialized
115 version of the handler's data.
116
117 This same sad story can happen on other CPUs when using
118 a compiler with aggressive pointer-value speculation
119 optimizations. (But please don't!)
120
121 More important, the rcu_dereference_sched() makes it
122 clear to someone reading the code that the pointer is
123 being protected by RCU-sched.
124

3. 한국어 전문 번역

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

NMI callback의 RCU 역참조

1-57

RCU는 read-mostly 자료구조뿐 아니라 동적으로 교체하는 NMI와 IRQ handler도 보호할 수 있다. 이 문서는 과거 `arch/x86/kernel/traps.c`의 NMI timer 작업을 바탕으로 함수 포인터 게시와 제거 protocol을 설명한다.

`dummy_nmi_callback()`은 아무 일도 하지 않고 0을 반환해 machine-specific 기본 처리를 계속하게 한다. 전역 `nmi_callback`은 현재 handler를 가리키며 처음에는 dummy 함수로 초기화된다.

`do_nmi()`는 `nmi_enter()` 뒤 CPU 번호와 NMI count를 갱신하고 `rcu_dereference_sched(nmi_callback)(regs, cpu)`로 handler를 호출한다. Handler가 0을 반환하면 `default_do_nmi()`를 실행하고 마지막에 `nmi_exit()`로 나간다.

i386에서는 하드웨어만 보면 accessor가 불필요해 보일 수 있지만, Alpha 같은 weakly ordered 아키텍처와 공격적인 compiler에서는 새 함수 포인터를 보면서 handler가 사용할 데이터의 오래된 초기화 상태를 볼 수 있다. `rcu_dereference_sched()`는 ordering을 제공하고 이 함수 포인터가 RCU-sched로 보호된다는 사실을 독자에게도 명시한다.

NMI dispatch
nmi_enter()per-CPU NMI count 증가rcu_dereference_sched(nmi_callback)동적 handler 호출0이면 default_do_nmi()nmi_exit()

함수 포인터를 안전하게 구독하고 0 반환 시 기본 handler로 이어진다.

.. _NMI_rcu_doc:

Using RCU to Protect Dynamic NMI Handlers
=========================================


Although RCU is usually used to protect read-mostly data structures,
it is possible to use RCU to provide dynamic non-maskable interrupt
handlers, as well as dynamic irq handlers.  This document describes
how to do this, drawing loosely from Zwane Mwaikambo's NMI-timer
work in an old version of "arch/x86/kernel/traps.c".

The relevant pieces of code are listed below, each followed by a
brief explanation::

        static int dummy_nmi_callback(struct pt_regs *regs, int cpu)
        {
                return 0;
        }

The dummy_nmi_callback() function is a "dummy" NMI handler that does
nothing, but returns zero, thus saying that it did nothing, allowing
the NMI handler to take the default machine-specific action::

        static nmi_callback_t nmi_callback = dummy_nmi_callback;

This nmi_callback variable is a global function pointer to the current
NMI handler::

        void do_nmi(struct pt_regs * regs, long error_code)
        {
                int cpu;

                nmi_enter();

                cpu = smp_processor_id();
                ++nmi_count(cpu);

                if (!rcu_dereference_sched(nmi_callback)(regs, cpu))
                        default_do_nmi(regs);

                nmi_exit();
        }

The do_nmi() function processes each NMI.  It first disables preemption
in the same way that a hardware irq would, then increments the per-CPU
count of NMIs.  It then invokes the NMI handler stored in the nmi_callback
function pointer.  If this handler returns zero, do_nmi() invokes the
default_do_nmi() function to handle a machine-specific NMI.  Finally,
preemption is restored.

In theory, rcu_dereference_sched() is not needed, since this code runs
only on i386, which in theory does not need rcu_dereference_sched()
anyway.  However, in practice it is a good documentation aid, particularly
for anyone attempting to do something similar on Alpha or on systems
with aggressive optimizing compilers.

새 NMI handler 게시

58-78

`set_nmi_callback()`은 `rcu_assign_pointer(nmi_callback, callback)`으로 새 handler를 등록한다. Callback이 사용할 모든 데이터는 이 호출 전에 완전히 초기화해야 한다.

Write ordering이 약한 CPU에서 `rcu_assign_pointer()`는 데이터 초기화가 handler pointer 공개보다 먼저 관찰되도록 한다. Reader 쪽 `rcu_dereference_sched()`와 짝을 이뤄 NMI가 새 handler를 보았다면 그 handler의 새 데이터도 보게 한다.

NMI handler publish
Handler 데이터 할당모든 필드 초기화rcu_assign_pointer(nmi_callback, callback)NMI가 새 포인터 관찰초기화된 데이터 사용

코드 주소와 handler 데이터가 하나의 준비된 상태로 공개된다.

Quick Quiz:
                Why might the rcu_dereference_sched() be necessary on Alpha, given that the code referenced by the pointer is read-only?

:ref:`Answer to Quick Quiz <answer_quick_quiz_NMI>`

Back to the discussion of NMI and RCU::

        void set_nmi_callback(nmi_callback_t callback)
        {
                rcu_assign_pointer(nmi_callback, callback);
        }

The set_nmi_callback() function registers an NMI handler.  Note that any
data that is to be used by the callback must be initialized up -before-
the call to set_nmi_callback().  On architectures that do not order
writes, the rcu_assign_pointer() ensures that the NMI handler sees the
initialized values::

        void unset_nmi_callback(void)
        {
                rcu_assign_pointer(nmi_callback, dummy_nmi_callback);

Handler 제거와 grace period

79-105

`unset_nmi_callback()`은 전역 pointer를 `dummy_nmi_callback`으로 되돌린다. 그러나 다른 CPU에서 old handler가 이미 실행 중일 수 있으므로 old handler의 데이터는 즉시 해제할 수 없다.

`unset_nmi_callback(); synchronize_rcu(); kfree(my_nmi_data);` 순서로 제거한다. v4.20 이후 `synchronize_rcu()`는 호출 전에 시작한 preemption-disabled 실행 구간이 모두 끝날 때까지 기다리며, NMI handler가 preemption을 disable하므로 진행 중이던 handler가 모두 빠져나간 뒤 반환한다.

이 보장은 해당 architecture가 NMI 진입과 이탈에서 반드시 `nmi_enter()`와 `nmi_exit()`를 호출할 때만 성립한다. Architecture의 context-tracking hook이 빠지면 RCU가 진행 중인 NMI를 알 수 없다.

동적 NMI handler 제거
rcu_assign_pointer(dummy handler)새 NMI는 dummy 호출synchronize_rcu()기존 NMI handler 모두 종료old handler 데이터 kfree()

새 진입을 dummy로 돌린 뒤 기존 실행을 기다리고 데이터를 회수한다.

        }

This function unregisters an NMI handler, restoring the original
dummy_nmi_handler().  However, there may well be an NMI handler
currently executing on some other CPU.  We therefore cannot free
up any data structures used by the old NMI handler until execution
of it completes on all other CPUs.

One way to accomplish this is via synchronize_rcu(), perhaps as
follows::

        unset_nmi_callback();
        synchronize_rcu();
        kfree(my_nmi_data);

This works because (as of v4.20) synchronize_rcu() blocks until all
CPUs complete any preemption-disabled segments of code that they were
executing.
Since NMI handlers disable preemption, synchronize_rcu() is guaranteed
not to return until all ongoing NMI handlers exit.  It is therefore safe
to free up the handler's data as soon as synchronize_rcu() returns.

Important note: for this to work, the architecture in question must
invoke nmi_enter() and nmi_exit() on NMI entry and exit, respectively.

.. _answer_quick_quiz_NMI:

Quick Quiz 답과 문서화 효과

106-123

새 handler 코드가 read-only여도 그 handler가 사용하는 데이터는 `set_nmi_callback()` 호출자가 방금 초기화했을 수 있다. Alpha에서는 subscription ordering이 없으면 새 함수 pointer와 old data 상태를 함께 보는 일이 가능하다. Pointer-value speculation이 강한 compiler에서도 유사한 문제가 생길 수 있다.

기능상 barrier가 필요 없는 특정 CPU에서도 `rcu_dereference_sched()`는 이 pointer의 보호 protocol을 분명하게 문서화한다. 코드를 다른 architecture로 옮기거나 최적화 규칙이 바뀌어도 의도가 보존된다.

NMI RCU publish/retire 계약
단계필수 요소
Publish데이터 초기화 후 rcu_assign_pointer
Dispatchnmi_enter/exit + rcu_dereference_sched
Retiredummy pointer 게시
Reclaimsynchronize_rcu 후 kfree

Accessor와 context tracking이 함께 수명을 보장한다.

Answer to Quick Quiz:
        Why might the rcu_dereference_sched() be necessary on Alpha, given that the code referenced by the pointer is read-only?

        The caller to set_nmi_callback() might well have
        initialized some data that is to be used by the new NMI
        handler.  In this case, the rcu_dereference_sched() would
        be needed, because otherwise a CPU that received an NMI
        just after the new handler was set might see the pointer
        to the new NMI handler, but the old pre-initialized
        version of the handler's data.

        This same sad story can happen on other CPUs when using
        a compiler with aggressive pointer-value speculation
        optimizations.  (But please don't!)

        More important, the rcu_dereference_sched() makes it
        clear to someone reading the code that the pointer is
        being protected by RCU-sched.