요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
Entry/exit handling for exceptions, interrupts, syscalls and KVM
================================================================
All transitions between execution domains require state updates which are
subject to strict ordering constraints. State updates are required for the
following:
* Lockdep
* RCU / Context tracking
* Preemption counter
* Tracing
* Time accounting
The update order depends on the transition type and is explained below in
the transition type sections: `Syscalls`_, `KVM`_, `Interrupts and regular
exceptions`_, `NMI and NMI-like exceptions`_.
Non-instrumentable code - noinstr
---------------------------------
Most instrumentation facilities depend on RCU, so instrumentation is prohibited
for entry code before RCU starts watching and exit code after RCU stops
watching. In addition, many architectures must save and restore register state,
which means that (for example) a breakpoint in the breakpoint entry code would
overwrite the debug registers of the initial breakpoint.
Such code must be marked with the 'noinstr' attribute, placing that code into a
special section inaccessible to instrumentation and debug facilities. Some
functions are partially instrumentable, which is handled by marking them
noinstr and using instrumentation_begin() and instrumentation_end() to flag the
instrumentable ranges of code:
.. code-block:: c
noinstr void entry(void)
{
handle_entry(); // <-- must be 'noinstr' or '__always_inline'
...
instrumentation_begin();
handle_context(); // <-- instrumentable code
instrumentation_end();
...
handle_exit(); // <-- must be 'noinstr' or '__always_inline'
}
This allows verification of the 'noinstr' restrictions via objtool on
supported architectures.
Invoking non-instrumentable functions from instrumentable context has no
restrictions and is useful to protect e.g. state switching which would
cause malfunction if instrumented.
All non-instrumentable entry/exit code sections before and after the RCU
state transitions must run with interrupts disabled.
Syscalls
--------
Syscall-entry code starts in assembly code and calls out into low-level C code
after establishing low-level architecture-specific state and stack frames. This
low-level C code must not be instrumented. A typical syscall handling function
invoked from low-level assembly code looks like this:
.. code-block:: c
noinstr void syscall(struct pt_regs *regs, int nr)
{
arch_syscall_enter(regs);
nr = syscall_enter_from_user_mode(regs, nr);
instrumentation_begin();
if (!invoke_syscall(regs, nr) && nr != -1)
result_reg(regs) = __sys_ni_syscall(regs);
instrumentation_end();
syscall_exit_to_user_mode(regs);
}
syscall_enter_from_user_mode() first invokes enter_from_user_mode() which
establishes state in the following order:
* Lockdep
* RCU / Context tracking
* Tracing
and then invokes the various entry work functions like ptrace, seccomp, audit,
syscall tracing, etc. After all that is done, the instrumentable invoke_syscall
function can be invoked. The instrumentable code section then ends, after which
syscall_exit_to_user_mode() is invoked.
syscall_exit_to_user_mode() handles all work which needs to be done before
returning to user space like tracing, audit, signals, task work etc. After
that it invokes exit_to_user_mode() which again handles the state
transition in the reverse order:
* Tracing
* RCU / Context tracking
* Lockdep
syscall_enter_from_user_mode() and syscall_exit_to_user_mode() are also
available as fine grained subfunctions in cases where the architecture code
has to do extra work between the various steps. In such cases it has to
ensure that enter_from_user_mode() is called first on entry and
exit_to_user_mode() is called last on exit.
Do not nest syscalls. Nested syscalls will cause RCU and/or context tracking
to print a warning.
KVM
---
Entering or exiting guest mode is very similar to syscalls. From the host
kernel point of view the CPU goes off into user space when entering the
guest and returns to the kernel on exit.
guest_state_enter_irqoff() is a KVM-specific variant of exit_to_user_mode()
and guest_state_exit_irqoff() is the KVM variant of enter_from_user_mode().
The state operations have the same ordering.
Task work handling is done separately for guest at the boundary of the
vcpu_run() loop via xfer_to_guest_mode_handle_work() which is a subset of
the work handled on return to user space.
Do not nest KVM entry/exit transitions because doing so is nonsensical.
Interrupts and regular exceptions
---------------------------------
Interrupts entry and exit handling is slightly more complex than syscalls
and KVM transitions.
If an interrupt is raised while the CPU executes in user space, the entry
and exit handling is exactly the same as for syscalls.
If the interrupt is raised while the CPU executes in kernel space the entry and
exit handling is slightly different. RCU state is only updated when the
interrupt is raised in the context of the CPU's idle task. Otherwise, RCU will
already be watching. Lockdep and tracing have to be updated unconditionally.
irqentry_enter() and irqentry_exit() provide the implementation for this.
The architecture-specific part looks similar to syscall handling:
.. code-block:: c
noinstr void interrupt(struct pt_regs *regs, int nr)
{
arch_interrupt_enter(regs);
state = irqentry_enter(regs);
instrumentation_begin();
irq_enter_rcu();
invoke_irq_handler(regs, nr);
irq_exit_rcu();
instrumentation_end();
irqentry_exit(regs, state);
}
Note that the invocation of the actual interrupt handler is within a
irq_enter_rcu() and irq_exit_rcu() pair.
irq_enter_rcu() updates the preemption count which makes in_hardirq()
return true, handles NOHZ tick state and interrupt time accounting. This
means that up to the point where irq_enter_rcu() is invoked in_hardirq()
returns false.
irq_exit_rcu() handles interrupt time accounting, undoes the preemption
count update and eventually handles soft interrupts and NOHZ tick state.
In theory, the preemption count could be updated in irqentry_enter(). In
practice, deferring this update to irq_enter_rcu() allows the preemption-count
code to be traced, while also maintaining symmetry with irq_exit_rcu() and
irqentry_exit(), which are described in the next paragraph. The only downside
is that the early entry code up to irq_enter_rcu() must be aware that the
preemption count has not yet been updated with the HARDIRQ_OFFSET state.
Note that irq_exit_rcu() must remove HARDIRQ_OFFSET from the preemption count
before it handles soft interrupts, whose handlers must run in BH context rather
than irq-disabled context. In addition, irqentry_exit() might schedule, which
also requires that HARDIRQ_OFFSET has been removed from the preemption count.
Even though interrupt handlers are expected to run with local interrupts
disabled, interrupt nesting is common from an entry/exit perspective. For
example, softirq handling happens within an irqentry_{enter,exit}() block with
local interrupts enabled. Also, although uncommon, nothing prevents an
interrupt handler from re-enabling interrupts.
Interrupt entry/exit code doesn't strictly need to handle reentrancy, since it
runs with local interrupts disabled. But NMIs can happen anytime, and a lot of
the entry code is shared between the two.
NMI and NMI-like exceptions
---------------------------
NMIs and NMI-like exceptions (machine checks, double faults, debug
interrupts, etc.) can hit any context and must be extra careful with
the state.
State changes for debug exceptions and machine-check exceptions depend on
whether these exceptions happened in user-space (breakpoints or watchpoints) or
in kernel mode (code patching). From user-space, they are treated like
interrupts, while from kernel mode they are treated like NMIs.
NMIs and other NMI-like exceptions handle state transitions without
distinguishing between user-mode and kernel-mode origin.
The state update on entry is handled in irqentry_nmi_enter() which updates
state in the following order:
* Preemption counter
* Lockdep
* RCU / Context tracking
* Tracing
The exit counterpart irqentry_nmi_exit() does the reverse operation in the
reverse order.
Note that the update of the preemption counter has to be the first
operation on enter and the last operation on exit. The reason is that both
lockdep and RCU rely on in_nmi() returning true in this case. The
preemption count modification in the NMI entry/exit case must not be
traced.
Architecture-specific code looks like this:
.. code-block:: c
noinstr void nmi(struct pt_regs *regs)
{
arch_nmi_enter(regs);
state = irqentry_nmi_enter(regs);
instrumentation_begin();
nmi_handler(regs);
instrumentation_end();
irqentry_nmi_exit(regs);
}
and for e.g. a debug exception it can look like this:
.. code-block:: c
noinstr void debug(struct pt_regs *regs)
{
arch_nmi_enter(regs);
debug_regs = save_debug_regs();
if (user_mode(regs)) {
state = irqentry_enter(regs);
instrumentation_begin();
user_mode_debug_handler(regs, debug_regs);
instrumentation_end();
irqentry_exit(regs, state);
} else {
state = irqentry_nmi_enter(regs);
instrumentation_begin();
kernel_mode_debug_handler(regs, debug_regs);
instrumentation_end();
irqentry_nmi_exit(regs, state);
}
}
There is no combined irqentry_nmi_if_kernel() function available as the
above cannot be handled in an exception-agnostic way.
NMIs can happen in any context. For example, an NMI-like exception triggered
while handling an NMI. So NMI entry code has to be reentrant and state updates
need to handle nesting.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
실행 도메인 전환과 상태 갱신
1-17Entry/exit handling for exceptions, interrupts, syscalls and KVM (예외, 인터럽트, 시스템 호출 및 KVM의 진입/이탈 처리)
실행 도메인 사이의 모든 전환에는 상태 갱신이 필요하며, 이 갱신에는 엄격한 순서 제약이 적용됩니다. 갱신해야 하는 상태는 다음과 같습니다.
- Lockdep
- RCU / Context tracking
- Preemption counter
- Tracing
- Time accounting
갱신 순서는 전환 유형에 따라 달라집니다. 아래의 `Syscalls`_, `KVM`_, `Interrupts and regular exceptions`_, `NMI and NMI-like exceptions`_ 절에서 각 순서를 설명합니다.
계측할 수 없는 코드와 noinstr
18-57계측할 수 없는 코드 - noinstr
대부분의 계측 기능은 RCU에 의존합니다. 따라서 RCU가 감시를 시작하기 전의 진입 코드와 RCU가 감시를 중단한 뒤의 이탈 코드는 계측할 수 없습니다. 또한 여러 아키텍처는 레지스터 상태를 저장하고 복원해야 합니다. 예를 들어 breakpoint 진입 코드 안에서 다시 breakpoint가 발생하면 최초 breakpoint의 debug register를 덮어쓸 수 있습니다.
이러한 코드는 `noinstr` 속성으로 표시해야 합니다. 그러면 코드는 계측 및 디버그 기능이 접근할 수 없는 특수 섹션에 배치됩니다. 일부 함수는 일부분만 계측할 수 있습니다. 이런 함수 전체를 `noinstr`로 표시한 뒤 `instrumentation_begin()`과 `instrumentation_end()`로 계측 가능한 코드 범위를 지정합니다.
noinstr void entry(void)
{
handle_entry(); // <-- must be 'noinstr' or '__always_inline'
...
instrumentation_begin();
handle_context(); // <-- instrumentable code
instrumentation_end();
...
handle_exit(); // <-- must be 'noinstr' or '__always_inline'
}
지원되는 아키텍처에서는 objtool을 사용해 `noinstr` 제약을 검증할 수 있습니다.
계측 가능한 문맥에서 계측 불가능한 함수를 호출하는 데에는 제한이 없습니다. 계측하면 오동작할 수 있는 상태 전환 등을 보호할 때 유용합니다.
RCU 상태 전환 전후의 계측 불가능한 모든 진입/이탈 코드 구간은 interrupts disabled 상태로 실행해야 합니다.
시스템 호출 진입과 이탈
58-110Syscalls
시스템 호출 진입 코드는 assembly code에서 시작합니다. 저수준 아키텍처별 상태와 stack frame을 확립한 다음 저수준 C code를 호출합니다. 이 저수준 C code는 계측하면 안 됩니다. 저수준 assembly code가 호출하는 일반적인 시스템 호출 처리 함수는 다음과 같습니다.
noinstr void syscall(struct pt_regs *regs, int nr)
{
arch_syscall_enter(regs);
nr = syscall_enter_from_user_mode(regs, nr);
instrumentation_begin();
if (!invoke_syscall(regs, nr) && nr != -1)
result_reg(regs) = __sys_ni_syscall(regs);
instrumentation_end();
syscall_exit_to_user_mode(regs);
}
`syscall_enter_from_user_mode()`는 먼저 `enter_from_user_mode()`를 호출하며 다음 순서로 상태를 확립합니다.
- Lockdep
- RCU / Context tracking
- Tracing
그다음 ptrace, seccomp, audit, syscall tracing 등 여러 진입 작업 함수를 호출합니다. 이 작업이 모두 끝나면 계측 가능한 `invoke_syscall` 함수를 호출할 수 있습니다. 계측 가능한 코드 구간이 끝난 뒤에는 `syscall_exit_to_user_mode()`를 호출합니다.
`syscall_exit_to_user_mode()`는 사용자 공간으로 돌아가기 전에 필요한 tracing, audit, signal, task work 등의 작업을 모두 처리합니다. 이어서 `exit_to_user_mode()`를 호출해 반대 순서로 상태를 전환합니다.
- Tracing
- RCU / Context tracking
- Lockdep
아키텍처 코드가 각 단계 사이에서 별도 작업을 해야 하는 경우를 위해 `syscall_enter_from_user_mode()`와 `syscall_exit_to_user_mode()`는 더 세분화된 하위 함수도 제공합니다. 이 경우 진입 시 `enter_from_user_mode()`를 가장 먼저 호출하고, 이탈 시 `exit_to_user_mode()`를 가장 마지막에 호출해야 합니다.
시스템 호출을 중첩하지 마십시오. 중첩된 시스템 호출은 RCU 및/또는 context tracking 경고를 발생시킵니다.
KVM 게스트 상태 전환
111-127KVM
게스트 모드 진입과 이탈은 시스템 호출과 매우 비슷합니다. 호스트 커널 관점에서 CPU는 게스트로 들어갈 때 사용자 공간으로 나가고, 게스트에서 나올 때 커널로 돌아옵니다.
`guest_state_enter_irqoff()`는 `exit_to_user_mode()`의 KVM 전용 변형이며, `guest_state_exit_irqoff()`는 `enter_from_user_mode()`의 KVM 변형입니다. 상태 연산의 순서는 동일합니다.
게스트의 task work 처리는 `vcpu_run()` 루프 경계에서 `xfer_to_guest_mode_handle_work()`를 통해 별도로 수행합니다. 이 함수는 사용자 공간으로 돌아갈 때 처리하는 작업의 부분집합입니다.
KVM 진입/이탈 전환을 중첩하지 마십시오. 이러한 중첩은 의미가 없습니다.
인터럽트와 일반 예외
128-196Interrupts and regular exceptions
인터럽트의 진입 및 이탈 처리는 시스템 호출과 KVM 전환보다 조금 더 복잡합니다.
CPU가 사용자 공간 코드를 실행하는 동안 인터럽트가 발생하면 진입 및 이탈 처리는 시스템 호출과 정확히 같습니다.
CPU가 커널 공간 코드를 실행하는 동안 인터럽트가 발생하면 처리가 조금 다릅니다. 인터럽트가 CPU의 idle task 문맥에서 발생한 경우에만 RCU 상태를 갱신합니다. 그 밖의 경우에는 RCU가 이미 감시 중입니다. Lockdep과 tracing은 조건 없이 갱신해야 합니다.
`irqentry_enter()`와 `irqentry_exit()`가 이 처리를 구현합니다. 아키텍처별 부분은 시스템 호출 처리와 유사합니다.
noinstr void interrupt(struct pt_regs *regs, int nr)
{
arch_interrupt_enter(regs);
state = irqentry_enter(regs);
instrumentation_begin();
irq_enter_rcu();
invoke_irq_handler(regs, nr);
irq_exit_rcu();
instrumentation_end();
irqentry_exit(regs, state);
}
실제 인터럽트 핸들러는 `irq_enter_rcu()`와 `irq_exit_rcu()` 쌍 사이에서 호출된다는 점에 유의하십시오.
`irq_enter_rcu()`는 preemption count를 갱신하여 `in_hardirq()`가 true를 반환하게 하고, NOHZ tick state와 interrupt time accounting을 처리합니다. 따라서 `irq_enter_rcu()`를 호출하기 전까지는 `in_hardirq()`가 false를 반환합니다.
`irq_exit_rcu()`는 interrupt time accounting을 처리하고 preemption count 갱신을 되돌리며, 마지막으로 soft interrupt와 NOHZ tick state를 처리합니다.
이론적으로 preemption count는 `irqentry_enter()`에서 갱신할 수 있습니다. 실제로는 이를 `irq_enter_rcu()`까지 미루면 preemption-count code를 추적할 수 있고, `irq_exit_rcu()` 및 `irqentry_exit()`와의 대칭도 유지됩니다. 단점은 `irq_enter_rcu()`까지의 초기 진입 코드가 preemption count에 아직 HARDIRQ_OFFSET 상태가 반영되지 않았음을 알아야 한다는 점뿐입니다.
`irq_exit_rcu()`는 soft interrupt를 처리하기 전에 preemption count에서 HARDIRQ_OFFSET을 제거해야 합니다. soft interrupt handler는 irq-disabled context가 아니라 BH context에서 실행되어야 하기 때문입니다. 또한 `irqentry_exit()`는 schedule할 수 있으므로 이때도 HARDIRQ_OFFSET이 제거되어 있어야 합니다.
인터럽트 핸들러는 local interrupt가 비활성화된 상태로 실행되는 것이 원칙이지만, 진입/이탈 관점에서 인터럽트 중첩은 흔합니다. 예를 들어 softirq 처리는 local interrupt를 활성화한 채 `irqentry_{enter,exit}()` 블록 안에서 일어납니다. 흔하지는 않지만 인터럽트 핸들러가 인터럽트를 다시 활성화하는 것도 막지 않습니다.
인터럽트 진입/이탈 코드는 local interrupt가 비활성화된 상태로 실행되므로 엄밀히는 재진입을 처리할 필요가 없습니다. 그러나 NMI는 언제든 발생할 수 있으며 진입 코드의 상당 부분을 인터럽트와 공유합니다.
NMI와 NMI 유사 예외
197-279NMI and NMI-like exceptions
NMI와 NMI 유사 예외(machine checks, double faults, debug interrupts 등)는 어떤 문맥에서도 발생할 수 있으므로 상태를 특별히 주의해서 다뤄야 합니다.
debug exception과 machine-check exception의 상태 변경은 예외가 사용자 공간(breakpoint 또는 watchpoint)에서 발생했는지, 커널 모드(code patching)에서 발생했는지에 따라 달라집니다. 사용자 공간에서 발생하면 인터럽트처럼 처리하고, 커널 모드에서 발생하면 NMI처럼 처리합니다.
NMI와 그 밖의 NMI 유사 예외는 발생 지점이 user mode인지 kernel mode인지 구분하지 않고 상태를 전환합니다.
진입 시 상태 갱신은 `irqentry_nmi_enter()`가 담당하며 순서는 다음과 같습니다.
- Preemption counter
- Lockdep
- RCU / Context tracking
- Tracing
이탈 측의 `irqentry_nmi_exit()`는 같은 연산을 반대 순서로 되돌립니다.
preemption counter 갱신은 진입 시 첫 연산이어야 하고 이탈 시 마지막 연산이어야 합니다. Lockdep과 RCU 모두 이 경우 `in_nmi()`가 true를 반환하는 데 의존하기 때문입니다. NMI 진입/이탈 시의 preemption count 변경은 추적해서는 안 됩니다.
아키텍처별 코드는 다음과 같습니다.
noinstr void nmi(struct pt_regs *regs)
{
arch_nmi_enter(regs);
state = irqentry_nmi_enter(regs);
instrumentation_begin();
nmi_handler(regs);
instrumentation_end();
irqentry_nmi_exit(regs);
}
예를 들어 debug exception은 다음처럼 처리할 수 있습니다.
noinstr void debug(struct pt_regs *regs)
{
arch_nmi_enter(regs);
debug_regs = save_debug_regs();
if (user_mode(regs)) {
state = irqentry_enter(regs);
instrumentation_begin();
user_mode_debug_handler(regs, debug_regs);
instrumentation_end();
irqentry_exit(regs, state);
} else {
state = irqentry_nmi_enter(regs);
instrumentation_begin();
kernel_mode_debug_handler(regs, debug_regs);
instrumentation_end();
irqentry_nmi_exit(regs, state);
}
}
위 처리는 예외 종류와 무관한 방식으로 수행할 수 없으므로 결합된 `irqentry_nmi_if_kernel()` 함수는 제공되지 않습니다.
NMI는 어떤 문맥에서도 발생할 수 있습니다. 예를 들어 NMI를 처리하는 중에도 NMI 유사 예외가 발생할 수 있습니다. 따라서 NMI 진입 코드는 재진입 가능해야 하며 상태 갱신은 중첩을 처리할 수 있어야 합니다.
요약과 해설
entry.rst:1-279진입/이탈 코드는 여러 커널 하위 시스템이 현재 실행 문맥을 정확히 인식하도록 상태를 정해진 순서로 바꿉니다. 순서를 어기면 RCU, Lockdep, 선점 및 추적기가 잘못된 문맥을 관찰할 수 있습니다.
`noinstr`는 RCU 감시가 안정되기 전후와 레지스터 보존 구간을 계측에서 제외합니다. 필요한 부분만 `instrumentation_begin()`과 `instrumentation_end()` 사이에 두어 objtool이 경계를 검증할 수 있게 합니다.
시스템 호출과 KVM은 사용자/게스트 경계 전환과 유사한 순서를 사용합니다. 일반 인터럽트는 idle 여부와 HARDIRQ_OFFSET 처리까지 고려하며, NMI는 어떤 문맥에도 끼어들 수 있으므로 preemption counter를 가장 먼저 올리고 가장 마지막에 내리며 중첩과 재진입을 허용해야 합니다.