NMI (Non-Maskable Interrupt)
NMI는 CPU의 마스킹 메커니즘(cli/local_irq_disable())으로
비활성화할 수 없는 특수한 인터럽트(Interrupt)입니다. 일반 인터럽트가 비활성 상태에서도 CPU에 전달되며,
시스템의 최후 방어선 역할을 합니다. 하드웨어 오류 감지, 커널 교착상태(hardlockup) 탐지,
성능 프로파일링(Profiling), 디버거 진입 등 크리티컬한 용도에 사용됩니다.
본 문서에서는 NMI 발생 원인, x86 NMI 아키텍처, 핸들러(Handler) 등록/처리 흐름,
hardlockup watchdog 메커니즘, NMI 컨텍스트 제약사항, IST 스택과 중첩 문제,
Unknown NMI 처리, NMI 기반 perf 프로파일링, CPU 디버깅(Debugging), GHES/APEI 에러 보고,
핸들러 등록 전략, 지연(Latency) 처리 설계, 운영 튜닝 체크리스트, 1시간 장애 대응 플레이북,
장애 보고서 템플릿까지 전 영역을 상세히 다룹니다.
핵심 요약
- 마스킹 불가 --
cli/local_irq_disable()로 비활성화할 수 없음 - IDT 벡터 2번 -- x86에서 NMI는 고정 벡터로 항상 전달
- 주요 소스 -- PMU 오버플로, 하드웨어 오류, watchdog, 외부 NMI 버튼
- Hardlockup 감지 -- hrtimer 기반 카운터를 NMI에서 확인하여 교착 탐지
- 엄격한 제약 -- spinlock, 메모리 할당, 일반 panic() 사용 불가
단계별 이해
- NMI 개념 파악
일반 인터럽트와 NMI의 차이점, 마스킹 불가 특성을 먼저 이해합니다. - x86 하드웨어 경로 추적
LAPIC에서 IDT 벡터 2번을 거쳐exc_nmi()에 도달하는 전달 경로를 파악합니다. - 핸들러 등록 구조 이해
register_nmi_handler()와 NMI 타입(LOCAL, UNKNOWN, SERR, IO_CHECK)을 학습합니다. - Watchdog 메커니즘 이해
hrtimer + PMU NMI 조합으로 hardlockup을 감지하는 원리를 파악합니다. - NMI-safe 프로그래밍 패턴 익히기
trylock, per-CPU 변수, atomic 연산 등 NMI 컨텍스트에서 허용되는 패턴을 숙지합니다.
NMI vs 일반 인터럽트 비교
NMI와 일반 인터럽트(maskable interrupt)는 CPU에 도달하는 방식, 마스킹 가능 여부, 사용하는 스택, 핸들러 제약사항 등 거의 모든 측면에서 다릅니다. 아래 다이어그램과 비교 테이블에서 그 차이를 명확하게 확인할 수 있습니다.
| 구분 | 일반 인터럽트 (Maskable) | NMI (Non-Maskable) |
|---|---|---|
| IDT 벡터 | 32~255 (동적 할당) | 벡터 2 (고정) |
| 마스킹 | cli / local_irq_disable()로 차단 가능 | 마스킹 불가 (하드웨어적 강제 전달) |
| 스택 | 현재 태스크(Task)의 커널 스택 | IST #2 전용 스택 (Per-CPU) |
| 중첩 | 가능 (우선순위(Priority)에 따라) | CPU가 하드웨어적으로 차단 (IRET까지) |
| 핸들러 제약 | spinlock, kmalloc 등 사용 가능 | trylock만 가능, 메모리 할당 금지 |
| RCU | 자동으로 read-side critical section | nmi_enter()에서 별도 보장 필요 |
| panic 호출 | panic() 사용 가능 | nmi_panic()만 사용 가능 |
| printk | 직접 사용 가능 | NMI-safe 버퍼(Buffer) 경유, 지연 출력 |
| 주요 사용처 | 디바이스 I/O, 타이머(Timer), IPI | watchdog, PMU, 하드웨어 에러, 디버깅 |
NMI 발생 원인
NMI는 다양한 하드웨어 및 소프트웨어 소스에서 발생합니다. 각 소스는 커널에서 서로 다른 핸들러 체인으로 분류되어 처리됩니다.
| 소스 | NMI 타입 | 설명 | 커널 설정 |
|---|---|---|---|
| 하드웨어 오류 | NMI_SERR | 메모리 패리티 에러, PCI SERR# 시그널(Signal), 버스(Bus) 에러 | BIOS/펌웨어(Firmware) 설정 |
| I/O 채널 체크 | NMI_IO_CHECK | ISA/PCI 디바이스의 I/O 채널 에러 | BIOS/펌웨어 설정 |
| Watchdog (hardlockup) | NMI_LOCAL | PMU 카운터 오버플로로 발생. CPU가 인터럽트 비활성 상태에서 일정 시간 이상 멈추면 감지 | CONFIG_HARDLOCKUP_DETECTOR |
| Performance Monitor | NMI_LOCAL | perf 프로파일링에서 PMU 카운터 오버플로 시 NMI로 샘플 수집 | CONFIG_PERF_EVENTS |
| 외부 NMI 버튼 | NMI_UNKNOWN | 서버 하드웨어의 물리적 NMI 버튼 (디버깅 목적) | -- |
| IOAPIC Watchdog | NMI_LOCAL | I/O APIC를 통한 NMI 전달 (레거시 시스템) | nmi_watchdog=1 |
| GHES (ACPI) | NMI_LOCAL | ACPI의 Generic Hardware Error Source를 통한 하드웨어 에러 보고 | CONFIG_ACPI_APEI_GHES |
| MCE (Machine Check) | NMI_LOCAL | 심각한 하드웨어 오류 시 MCE를 NMI로 전달 (일부 아키텍처) | CONFIG_X86_MCE |
| KGDB/KDB | NMI_LOCAL | 커널 디버거 진입을 위한 NMI (SysRq + 디버거) | CONFIG_KGDB |
| CPU Backtrace IPI | NMI_LOCAL | SysRq-l 또는 trigger_all_cpu_backtrace()로 모든 CPU의 콜스택 수집 | 기본 내장 |
/* arch/x86/include/asm/nmi.h -- NMI 타입 정의 */
#define NMI_LOCAL 0 /* 이 CPU에만 해당 (PMU, watchdog, perf) */
#define NMI_UNKNOWN 1 /* 원인 불명 (외부 버튼 등) */
#define NMI_SERR 2 /* 시스템 에러 (PCI SERR#) */
#define NMI_IO_CHECK 3 /* I/O 채널 체크 에러 */
#define NMI_MAX 4
/* 각 타입별 핸들러 리스트 */
static struct nmi_desc nmi_desc[NMI_MAX] = {
[NMI_LOCAL] = { .head = LIST_HEAD_INIT(...), .lock = ... },
[NMI_UNKNOWN] = { ... },
[NMI_SERR] = { ... },
[NMI_IO_CHECK] = { ... },
};
x86 NMI 아키텍처
x86에서 NMI는 IDT(Interrupt Descriptor Table)의 벡터 2번에 고정되어 있습니다. CPU가 NMI를 수신하면 현재 실행 컨텍스트와 무관하게 즉시 NMI 핸들러로 진입하며, NMI 처리 중에는 동일 CPU에서 중첩 NMI가 차단됩니다(CPU 하드웨어 메커니즘).
LAPIC LVT NMI 전달 상세
Local APIC의 LVT(Local Vector Table)는 NMI 전달의 핵심 구성 요소입니다. LINT0과 LINT1 핀이 각각 다른 인터럽트 소스를 처리하며, 일반적으로 LINT1이 NMI에 매핑(Mapping)됩니다.
/* arch/x86/kernel/apic/apic.c -- LAPIC LVT NMI 설정 */
/* LINT1을 NMI로 설정 (부팅 시 BIOS가 설정, 커널이 검증) */
static void setup_local_APIC(void)
{
unsigned int value;
/* LINT1 → NMI delivery mode */
value = apic_read(APIC_LVT1);
value &= ~(APIC_MODE_MASK | APIC_INPUT_POLARITY);
value |= APIC_MODE_NMI; /* Delivery Mode = 100b (NMI) */
apic_write(APIC_LVT1, value);
/* Watchdog: LVT Performance Counter를 NMI로 설정 */
value = APIC_DM_NMI;
apic_write(APIC_LVTPC, value);
}
/* ICR을 통한 NMI IPI 전송 (CPU 간 NMI) */
void apic_send_nmi_to_cpu(unsigned int cpu)
{
/* ICR: Destination=target CPU, DelivMode=NMI, Level=Assert */
apic_icr_write(APIC_DM_NMI | APIC_DEST_PHYSICAL,
per_cpu(x86_cpu_to_apicid, cpu));
}
NMI 처리 코드 흐름
NMI 핸들러는 struct nmiaction 구조체(Struct)로 등록되며, nmi_handle()이 등록된 핸들러 체인을 순회합니다. 아래는 NMI 핸들러 등록 구조와 처리 코드 흐름입니다.
/* arch/x86/kernel/nmi.c -- NMI 핸들러 등록 */
struct nmiaction {
struct list_head list;
nmi_handler_t handler; /* 핸들러 콜백 */
u64 max_duration;/* 최대 실행 시간 (ns) */
unsigned long flags;
const char *name;
};
/* NMI 핸들러 등록/해제 */
int register_nmi_handler(unsigned int type,
nmi_handler_t handler, unsigned long flags,
const char *name);
void unregister_nmi_handler(unsigned int type,
const char *name);
/* NMI 타입 */
/* NMI_LOCAL - 이 CPU에만 해당하는 NMI (PMU, watchdog) */
/* NMI_UNKNOWN - 원인 불명 NMI */
/* NMI_SERR - 시스템 에러 (PCI SERR#) */
/* NMI_IO_CHECK - I/O 채널 체크 에러 */
/* NMI 진입점 (간략화) -- arch/x86/kernel/nmi.c */
void exc_nmi(struct pt_regs *regs)
{
/* IST(Interrupt Stack Table) 또는 전용 NMI 스택 사용 */
nmi_enter(); /* RCU, context tracking 업데이트 */
default_do_nmi(regs);
nmi_exit();
}
static void default_do_nmi(struct pt_regs *regs)
{
/* 1단계: CPU-local NMI 핸들러 체인 순회 */
if (nmi_handle(NMI_LOCAL, regs))
return;
/* 2단계: I/O 체크 에러 */
if (reason & NMI_REASON_IOCHK) {
io_check_error(reason, regs);
return;
}
/* 3단계: unknown NMI */
unknown_nmi_error(reason, regs);
/* unknown_nmi_panic=1 이면 여기서 panic() */
}
/* nmi_enter() 내부 -- NMI 컨텍스트 진입 시 수행하는 작업 */
static inline void nmi_enter(void)
{
instrumentation_begin();
rcu_nmi_enter(); /* RCU extended quiescent state 종료 */
lockdep_off(); /* lockdep 비활성화 (NMI 내에서 잠금 추적 불가) */
ftrace_nmi_enter(); /* ftrace NMI-safe 모드 진입 */
__this_cpu_write(nmi_state.cnt, 1);
}
NMI Watchdog (Hardlockup Detector)
NMI watchdog은 CPU가 인터럽트 비활성 상태에서 장시간 멈추는 hardlockup을 감지합니다. 일반 인터럽트가 차단된 상태에서도 NMI는 전달되므로, 교착 상태(Deadlock)에 빠진 CPU를 감지할 수 있는 유일한 메커니즘입니다.
Softlockup vs Hardlockup 비교
커널의 lockup 감지 메커니즘은 두 계층으로 구성됩니다. softlockup은 커널 스레드(Kernel Thread)가 긴 시간 동안 스케줄링되지 않는 상태를, hardlockup은 인터럽트 자체가 비활성화된 상태를 감지합니다.
| 구분 | Softlockup | Hardlockup |
|---|---|---|
| 감지 대상 | 커널 스레드가 watchdog_thresh초 이상 스케줄링 안 됨 | 인터럽트가 watchdog_thresh초 이상 비활성 상태 |
| 감지 메커니즘 | Per-CPU watchdog 커널 스레드 (watchdog/N) | PMU NMI 기반 NMI watchdog |
| 타이머 | hrtimer가 watchdog 스레드(Thread)의 타임스탬프 확인 | NMI에서 hrtimer_interrupts 변화 확인 |
| 인터럽트 상태 | 인터럽트 활성 (hrtimer 동작 가능) | 인터럽트 비활성 (hrtimer 동작 불가) |
| 심각도 | 경고 (BUG: soft lockup) | 치명적 (Watchdog detected hard LOCKUP) |
| 기본 동작 | backtrace 출력 후 계속 실행 | hardlockup_panic 설정에 따라 panic 가능 |
| 커널 설정 | CONFIG_SOFTLOCKUP_DETECTOR | CONFIG_HARDLOCKUP_DETECTOR |
/* kernel/watchdog.c -- hardlockup 감지 핵심 함수 */
/* Per-CPU 변수: hrtimer 인터럽트 카운터 */
static DEFINE_PER_CPU(unsigned long, hrtimer_interrupts);
static DEFINE_PER_CPU(unsigned long, hrtimer_interrupts_saved);
/* hrtimer 콜백: 일반 인터럽트로 주기적 실행 */
static enum hrtimer_restart watchdog_timer_fn(struct hrtimer *hrtimer)
{
/* hrtimer_interrupts 카운터 증가 */
__this_cpu_inc(hrtimer_interrupts);
/* softlockup 확인 (watchdog 스레드가 실행됐는지) */
if (is_softlockup(touch_ts))
pr_emerg("BUG: soft lockup - CPU#%d stuck for %us!\\n",
cpu, duration);
return HRTIMER_RESTART;
}
/* hardlockup 판정 (NMI 콜백에서 호출) */
static bool is_hardlockup(void)
{
unsigned long hrint = __this_cpu_read(hrtimer_interrupts);
/* 이전 NMI 이후로 hrtimer_interrupts가 변하지 않았으면 hardlockup */
if (__this_cpu_read(hrtimer_interrupts_saved) == hrint)
return true;
__this_cpu_write(hrtimer_interrupts_saved, hrint);
return false;
}
/* NMI watchdog 콜백: PMU 오버플로 NMI에서 호출 */
static void watchdog_overflow_callback(struct perf_event *event,
struct perf_sample_data *data, struct pt_regs *regs)
{
if (is_hardlockup()) {
pr_emerg("Watchdog detected hard LOCKUP on cpu %d\\n",
smp_processor_id());
show_regs(regs);
if (hardlockup_panic)
nmi_panic(regs, "Hard LOCKUP");
}
}
# NMI watchdog 설정
# 활성화/비활성화
echo 1 > /proc/sys/kernel/nmi_watchdog # 활성화
echo 0 > /proc/sys/kernel/nmi_watchdog # 비활성화 (가상화 환경에서 권장)
# watchdog 임계값 (기본 10초)
echo 30 > /proc/sys/kernel/watchdog_thresh
# hardlockup 시 panic 여부 (기본 0 = backtrace만)
echo 1 > /proc/sys/kernel/hardlockup_panic
# 커널 파라미터로 설정
# nmi_watchdog=0 NMI watchdog 비활성화
# nmi_watchdog=1 I/O APIC 기반 watchdog (레거시)
# nmi_watchdog=2 Local APIC 기반 (perf PMU)
# hardlockup_panic=1 hardlockup 시 panic
# watchdog 상태 확인
cat /proc/sys/kernel/nmi_watchdog
dmesg | grep -i "nmi watchdog"
nmi_watchdog=0으로 비활성화를 권장합니다. 또한 PMU 가상화가 지원되지 않는 환경에서는 자동으로 비활성화됩니다.
NMI 컨텍스트 제약사항
NMI 핸들러는 어떤 컨텍스트에서든 비동기적으로 진입할 수 있으므로, 일반 인터럽트 핸들러보다 훨씬 엄격한 제약이 적용됩니다:
| 동작 | NMI 컨텍스트 | 이유 |
|---|---|---|
spinlock (일반) | 사용 불가 | NMI가 lock holder를 선점(Preemption)하면 self-deadlock |
raw_spin_lock | 사용 불가 (동일) | NMI 중에도 동일 CPU의 lock holder를 선점 가능 |
nmi_spin_lock 패턴 | arch_spin_trylock | trylock으로만 시도, 실패 시 즉시 포기 |
| 메모리 할당 | 사용 불가 | 할당자 내부 lock 보유 중일 수 있음 |
printk() | 제한적 사용 가능 | NMI-safe 버퍼(nmi_print_seq)에 기록 후 나중에 출력 |
trace_* | NMI-safe 버전만 | perf_swevent_put 등 NMI-safe tracing API 사용 |
| RCU read | 제한적 가능 | nmi_enter()가 RCU watching 상태를 보장 |
panic() | nmi_panic() 사용 | 일반 panic()은 NMI 컨텍스트에서 교착 가능 |
/* NMI-safe 핸들러 작성 패턴 */
static int my_nmi_handler(unsigned int type,
struct pt_regs *regs)
{
/* !! 절대 금지: mutex, semaphore, kmalloc, schedule !! */
/* Per-CPU 변수는 안전 (NMI는 같은 CPU에서 중첩되지 않음) */
this_cpu_inc(nmi_counter);
/* atomic 연산은 안전 */
atomic_inc(&global_nmi_count);
/* trylock 패턴: 실패 시 포기 */
if (raw_spin_trylock(&my_lock)) {
/* ... critical section ... */
raw_spin_unlock(&my_lock);
}
/* NMI-safe 출력 */
nmi_panic(regs, "Fatal error detected");
return NMI_HANDLED; /* 또는 NMI_DONE */
}
/* 등록 예시 */
register_nmi_handler(NMI_LOCAL, my_nmi_handler,
0, "my_nmi");
NMI 중첩과 IST
x86_64에서 NMI는 IST(Interrupt Stack Table)를 사용하여 전용 스택에서 실행됩니다. 이는 스택 오버플로(Stack Overflow) 상태에서도 NMI가 안전하게 처리되도록 보장합니다. 그러나 IST 메커니즘과 NMI 중첩 사이에는 미묘하지만 치명적인 상호작용이 존재합니다.
/* arch/x86/entry/entry_64.S -- NMI 트램폴린 어셈블리 (간략화) */
/*
* NMI 진입 시 CPU가 자동으로 IST2 스택을 로드합니다.
* 문제: IRET 실행 시 NMI blocking이 해제되어
* 중첩 NMI가 같은 IST2 스택을 덮어쓸 수 있습니다.
*
* 해결: IST2를 트램폴린으로만 사용하고,
* 즉시 전용 Per-CPU NMI 스택으로 전환합니다.
*/
SYM_CODE_START(asm_exc_nmi)
/* 1. IST2 스택에서 시작 (CPU 자동 전환) */
/* 2. 현재 NMI 처리 중인지 확인 */
cmpq $1, PER_CPU_VAR(nmi_state)
je nested_nmi /* 이미 NMI 처리 중이면 중첩 핸들링 */
/* 3. nmi_state = 1 (NMI 처리 시작 표시) */
movq $1, PER_CPU_VAR(nmi_state)
/* 4. 전용 NMI 스택으로 전환 */
movq PER_CPU_VAR(nmi_stack_top), %rsp
/* 5. pt_regs 구조체 구성 후 C 핸들러 호출 */
call exc_nmi
/* 6. 복귀: nmi_state = 0, 원래 스택 복원, IRET */
movq $0, PER_CPU_VAR(nmi_state)
nested_nmi:
/* 중첩 NMI: 최소한의 처리만 수행 후 복귀 */
/* repeat_nmi 레이블로 분기하여 나중에 재처리 */
SYM_CODE_END(asm_exc_nmi)
NMI 기반 CPU 디버깅
NMI는 응답하지 않는 CPU를 디버깅하는 가장 강력한 도구입니다. SysRq-l 또는 trigger_all_cpu_backtrace()를 통해 모든 CPU의 콜스택을 NMI로 수집할 수 있으며, 이는 deadlock이나 livelock 진단에 핵심적입니다.
/* lib/nmi_backtrace.c -- NMI 기반 CPU backtrace 수집 */
/* 모든 CPU에 NMI를 보내어 backtrace 수집 */
void nmi_trigger_cpumask_backtrace(const struct cpumask *mask,
bool exclude_self,
void (*raise)(cpumask_t *mask))
{
/* backtrace_mask에 대상 CPU 설정 */
cpumask_copy(to_cpumask(backtrace_mask), mask);
/* 자기 자신의 backtrace 먼저 출력 */
if (!exclude_self && cpumask_test_cpu(this_cpu, mask)) {
printk("NMI backtrace for cpu %d\\n", this_cpu);
dump_stack();
}
/* 다른 CPU들에 NMI IPI 전송 */
raise(to_cpumask(backtrace_mask));
/* 최대 10초간 모든 CPU의 응답 대기 */
while (!cpumask_empty(to_cpumask(backtrace_mask))) {
if (timeout-- == 0)
break;
touch_nmi_watchdog();
mdelay(1);
}
}
/* NMI 핸들러에서 호출: 요청받은 CPU의 backtrace 출력 */
static int nmi_cpu_backtrace_handler(unsigned int cmd,
struct pt_regs *regs)
{
int cpu = smp_processor_id();
if (cpumask_test_cpu(cpu, to_cpumask(backtrace_mask))) {
pr_warn("NMI backtrace for cpu %d\\n", cpu);
show_regs(regs);
dump_stack();
cpumask_clear_cpu(cpu, to_cpumask(backtrace_mask));
return NMI_HANDLED;
}
return NMI_DONE;
}
# NMI 기반 CPU 디버깅 명령
# SysRq-l: 모든 CPU의 backtrace 출력
echo l > /proc/sysrq-trigger
# 특정 hung task 감지 시 backtrace
echo w > /proc/sysrq-trigger # blocked(D 상태) 태스크 표시
echo l > /proc/sysrq-trigger # 모든 CPU backtrace
# NMI를 이용한 원격 디버깅 (IPMI)
ipmitool chassis power diag # BMC를 통한 NMI 전송
# kdump + NMI: 응답하지 않는 시스템에서 crash dump 생성
# 1. unknown_nmi_panic=1 설정
# 2. kdump 서비스 활성화
# 3. NMI 버튼 또는 IPMI로 NMI 전송
# → panic → kdump가 vmcore 생성
# dmesg에서 NMI backtrace 확인
dmesg | grep -A 20 "NMI backtrace"
Unknown NMI 처리
등록된 핸들러가 처리하지 못한 NMI는 unknown_nmi_error()로 전달됩니다. 이는 하드웨어 문제를 나타낼 수 있습니다:
/* arch/x86/kernel/nmi.c -- Unknown NMI 처리 */
static void unknown_nmi_error(unsigned char reason,
struct pt_regs *regs)
{
/* unknown_nmi_panic이 설정되어 있으면 즉시 panic */
if (unknown_nmi_panic)
nmi_panic(regs, "NMI: Not continuing");
/* 유명한 에러 메시지 */
pr_emerg("Uhhuh. NMI received for unknown reason %02x on CPU %d.\\n",
reason, smp_processor_id());
pr_emerg("Dazed and confused, but trying to continue\\n");
}
# Unknown NMI 발생 시 panic 설정
echo 1 > /proc/sys/kernel/unknown_nmi_panic
# 또는 커널 파라미터
# unknown_nmi_panic=1
# dmesg에서 NMI 관련 로그 확인
dmesg | grep -i nmi
# [ 0.000000] NMI watchdog: Perf NMI watchdog permanently disabled
# [ 123.456789] Uhhuh. NMI received for unknown reason 2d on CPU 0.
# [ 123.456790] Dazed and confused, but trying to continue
# /proc/interrupts에서 NMI 카운트 확인
grep NMI /proc/interrupts
# NMI: 1234 5678 9012 3456 Non-maskable interrupts
# LOC: 123456 234567 345678 456789 Local timer interrupts
Unknown NMI reason 코드 해석
Unknown NMI 메시지에 표시되는 reason 값은 포트(Port) 0x61(NMI Status and Control Register)에서 읽은 값입니다. 각 비트의 의미를 이해하면 원인을 더 빠르게 좁힐 수 있습니다.
| 비트 | 이름 | 의미 |
|---|---|---|
| Bit 7 | SERR# NMI Status | PCI SERR# 시그널로 인한 NMI (1=발생) |
| Bit 6 | IOCHK# NMI Status | I/O 채널 체크 에러로 인한 NMI (1=발생) |
| Bit 5 | Timer 2 Output | 타이머 2 출력 상태 |
| Bit 4 | Refresh Cycle Toggle | DRAM 리프레시 토글 비트 |
| Bit 3 | IOCHK# NMI Enable | I/O 채널 체크 NMI 활성 (1=비활성화) |
| Bit 2 | SERR# NMI Enable | SERR# NMI 활성 (1=비활성화) |
| Bit 1 | Speaker Data | 스피커(Speaker) 데이터 비트 |
| Bit 0 | Timer 2 Gate | 타이머 2 게이트(Gate) 비트 |
/* arch/x86/kernel/nmi.c -- reason 코드 해석 */
#define NMI_REASON_SERR 0x80 /* Bit 7: SERR# NMI */
#define NMI_REASON_IOCHK 0x40 /* Bit 6: I/O Channel Check */
#define NMI_REASON_MASK (NMI_REASON_SERR | NMI_REASON_IOCHK)
/* default_do_nmi()에서의 reason 확인 흐름 */
static void default_do_nmi(struct pt_regs *regs)
{
unsigned char reason = x86_platform.get_nmi_reason();
/* SERR# 확인 */
if (reason & NMI_REASON_SERR)
pci_serr_error(reason, regs);
/* I/O Channel Check 확인 */
if (reason & NMI_REASON_IOCHK)
io_check_error(reason, regs);
/* 위 두 가지 모두 아니면 → unknown_nmi_error() */
unknown_nmi_error(reason, regs);
}
# Unknown NMI reason 코드 해석 예시
# reason=0x2d (0010 1101):
# Bit 7(SERR)=0, Bit 6(IOCHK)=0 → NMI_SERR/IO_CHECK 아님
# → 외부 NMI 버튼, 펌웨어, 또는 하드웨어 오류 가능
# reason=0xad (1010 1101):
# Bit 7(SERR)=1 → SERR# 관련, PCI 에러 확인 필요
# 포트 0x61 직접 읽기 (디버깅용)
# 커널 내부에서: inb(0x61)
# dmesg에서 reason 코드 찾기
dmesg | grep "unknown reason"
# [ 456.789012] Uhhuh. NMI received for unknown reason 2d on CPU 0.
# EDAC/MCE 상관 분석
dmesg | grep -iE "mce|edac|ghes|hardware.error" | tail -20
ipmitool chassis power diag)으로 NMI를 수동 발생시킬 수 있습니다. 시스템이 응답하지 않을 때 unknown_nmi_panic=1과 함께 사용하면 crash dump를 생성하여 kdump로 문제를 진단할 수 있습니다.
NMI 기반 성능 프로파일링
perf는 PMU 카운터 오버플로 시 NMI를 사용하여 CPU 샘플을 수집합니다. 인터럽트 비활성 구간에서도 샘플링이 가능하므로, 일반 인터럽트 기반 프로파일링보다 정확한 결과를 제공합니다.
# NMI 기반 perf 프로파일링 (cycles 이벤트 = PMU NMI)
perf record -e cycles -g -a sleep 10
# NMI를 사용하는지 확인 (/proc/interrupts의 NMI 카운트 증가)
watch -n 1 'grep NMI /proc/interrupts'
# perf의 NMI 사용량 확인
perf stat -e 'cycles:u,cycles:k' -a sleep 5
# NMI vs Timer 프로파일링 비교
perf record -e cycles -g -a sleep 5 # NMI 기반 (PMU)
perf record -e cpu-clock -g -a sleep 5 # Timer 기반
# NMI 기반은 local_irq_disable() 구간도 관측 가능
/* perf의 NMI 핸들러 등록 (간략화) */
/* arch/x86/events/core.c */
static int perf_event_nmi_handler(unsigned int cmd,
struct pt_regs *regs)
{
/* PMU 카운터 오버플로 확인 */
if (!x86_pmu_handle_irq(regs))
return NMI_DONE;
/* 샘플 기록: IP(Instruction Pointer), callchain 등 */
/* perf_event_overflow() → perf_event_output() */
return NMI_HANDLED;
}
/* NMI에서 안전하게 callchain을 수집하기 위해
* frame pointer 또는 ORC unwinder 사용 */
perf record -e cycles(NMI 기반)는 perf record -e cpu-clock(타이머 기반)보다 정밀합니다. 타이머 기반은 local_irq_disable() 구간을 관측할 수 없지만, NMI 기반은 인터럽트가 비활성화된 크리티컬 섹션 내부까지 프로파일링할 수 있습니다.
perf NMI 핸들러 상세 흐름
/* arch/x86/events/core.c -- PMU NMI 핸들러 상세 */
/* PMU NMI 핸들러: 카운터 오버플로 처리 */
static int x86_pmu_handle_irq(struct pt_regs *regs)
{
struct cpu_hw_events *cpuc = this_cpu_ptr(&cpu_hw_events);
int handled = 0;
/* 모든 활성 PMU 카운터 순회 */
for (idx = 0; idx < x86_pmu.num_counters; idx++) {
struct perf_event *event = cpuc->events[idx];
if (!event) continue;
val = x86_perf_event_update(event);
/* 카운터가 오버플로했는지 확인 */
if (val & (1ULL << (x86_pmu.cntval_bits - 1)))
continue; /* 오버플로 아님 */
handled++;
/* 샘플 데이터 수집 */
struct perf_sample_data data;
perf_sample_data_init(&data, 0, event->hw.last_period);
/* IP(Instruction Pointer) 기록 */
data.ip = instruction_pointer(regs);
/* Callchain 수집 (NMI-safe unwinder 사용) */
if (event->attr.sample_type & PERF_SAMPLE_CALLCHAIN)
data.callchain = perf_callchain(event, regs);
/* ring buffer에 샘플 기록 */
if (perf_event_overflow(event, &data, regs))
x86_pmu_stop(event, 0); /* 버퍼 풀 → 정지 */
/* 카운터 재장전: -sample_period로 설정 (오버플로까지) */
perf_event_update_userpage(event);
}
return handled;
}
/* NMI에서 안전한 callchain 수집 */
struct perf_callchain_entry *perf_callchain(
struct perf_event *event, struct pt_regs *regs)
{
struct perf_callchain_entry *entry;
/* Per-CPU 사전 할당된 버퍼 사용 (NMI-safe) */
entry = this_cpu_ptr(&callchain_entry);
/* 커널 콜체인: ORC 또는 frame pointer 기반 */
perf_callchain_kernel(entry, regs);
/* 사용자 콜체인: 페이지 폴트 가능 → NMI에서는 제한적 */
if (!in_nmi() && user_mode(regs))
perf_callchain_user(entry, regs);
return entry;
}
perf NMI vs Timer 프로파일링 정밀도 비교
| 항목 | NMI 기반 (PMU) | Timer 기반 (cpu-clock) |
|---|---|---|
| 이벤트 | cycles, instructions, cache-misses 등 | cpu-clock, task-clock |
| 전달 메커니즘 | PMU 카운터 오버플로 → LVT PMC → NMI | hrtimer → 일반 인터럽트 |
| cli 구간 관측 | 가능 (NMI는 마스킹 불가) | 불가 (일반 인터럽트 차단됨) |
| spinlock 내부 | 관측 가능 | 부분적 (irq-disabled 구간 누락) |
| 정밀도 | 하드웨어 카운터 기반 (높음) | 타이머 해상도 의존 (낮음) |
| 오버헤드 | NMI 핸들러 + PMU 레지스터 접근 | 낮음 (일반 인터럽트) |
| 커널 콜체인 | 정확 (NMI-safe unwinder) | 정확 |
| 유저 콜체인 | 제한적 (NMI에서 page fault 위험) | 완전 지원 |
| 가상화 환경 | vPMU 필요 | 항상 동작 |
| 적합한 분석 | lock contention, IRQ-off 레이턴시, 핫스팟 | 일반 CPU 사용률, 호출 빈도 |
# NMI 기반 프로파일링 고급 사용 예시
# 1. IRQ-disabled 구간 프로파일링 (NMI만 가능)
perf record -e cycles:k -g --call-graph dwarf -a sleep 10
perf report --sort=dso,symbol
# 2. Lock contention 핫스팟 찾기
perf record -e cycles -g -a -- sleep 5
perf report --sort=symbol | grep -E "spin_lock|mutex"
# 3. PMU 카운터별 NMI 발생 빈도 확인
perf stat -e cycles,instructions,cache-misses -a sleep 5
# 각 이벤트의 sample_period에 따라 NMI 빈도가 결정됨
# 4. NMI 오버헤드 측정
# perf 실행 전후 /proc/interrupts의 NMI 카운터 비교
grep NMI /proc/interrupts # before
perf record -e cycles -a sleep 10
grep NMI /proc/interrupts # after (증가량 = perf에 의한 NMI 수)
# 5. NMI 기반 perf로 hardlockup 원인 추적
# watchdog이 hardlockup 감지 시, 동시에 수집된 perf 데이터로
# 어떤 함수에서 CPU가 멈췄는지 확인
perf record -e cycles -g -a &
# ... hardlockup 재현 ...
kill %1
perf report
GHES/APEI NMI 에러 보고
ACPI의 APEI(ACPI Platform Error Interface) 프레임워크는 GHES(Generic Hardware Error Source)를 통해 펌웨어가 감지한 하드웨어 에러를 커널에 보고합니다. 심각한 에러의 경우 NMI를 사용하여 즉각적인 알림이 이루어집니다.
/* drivers/acpi/apei/ghes.c -- GHES NMI 핸들러 */
/* NMI 알림 핸들러 등록 */
static int ghes_notify_nmi(unsigned int cmd,
struct pt_regs *regs)
{
struct ghes *ghes;
int ret = NMI_DONE;
/* NMI 핸들러에서 안전하게 GHES 리스트 순회 */
/* rcu_read_lock()은 nmi_enter()에서 이미 보장 */
list_for_each_entry_rcu(ghes, &ghes_nmi, list) {
/* 공유 메모리에서 에러 상태 블록 읽기 */
if (ghes_read_estatus(ghes, 1)) { /* fixmap 사용 */
ghes_clear_estatus(ghes, buf_paddr, fixmap_idx);
continue;
}
/* 에러 심각도에 따라 처리 */
sev = ghes_severity(ghes->estatus->error_severity);
if (sev >= GHES_SEV_PANIC)
__ghes_panic(ghes, ghes->estatus, buf_paddr,
fixmap_idx);
/* 복구 가능한 에러: NMI-safe 큐에 넣고 나중에 처리 */
ghes_estatus_pool_shrink(ghes->estatus);
ret = NMI_HANDLED;
}
return ret;
}
/* GHES 초기화 시 NMI 핸들러 등록 */
register_nmi_handler(NMI_LOCAL, ghes_notify_nmi,
0, "ghes");
ioremap 대신 fixmap을 사용합니다. NMI 컨텍스트에서는 페이지 테이블(Page Table) 조작이 불가능하므로, 부팅 시 미리 매핑해둔 fixmap 슬롯을 활용합니다. 복구 가능한 에러는 NMI-safe 풀에 저장 후 워크큐에서 나중에 처리합니다.
NMI 핸들러 등록 전략
NMI 핸들러를 등록할 때는 type(LOCAL/UNKNOWN/SERR/IO_CHECK), flags, 반환값(NMI_HANDLED/NMI_DONE)의 조합이 동작을 좌우합니다. 핸들러가 체인 구조로 동작하기 때문에, 잘못된 반환값은 다른 핸들러 실행을 막거나 의미 없는 Unknown NMI 경고를 유발할 수 있습니다.
| 요소 | 선택지 | 실무 권장 |
|---|---|---|
| type | NMI_LOCAL, NMI_UNKNOWN, NMI_SERR, NMI_IO_CHECK | 대부분 NMI_LOCAL 사용, 원인 미상 분석 용도로 NMI_UNKNOWN 보조 등록 |
| flags | NMI_FLAG_FIRST, NMI_FLAG_LAST | watchdog/perf보다 우선이 필요하면 FIRST, 로깅 전용이면 LAST |
| 반환값 | NMI_HANDLED, NMI_DONE | 명확한 소스 확인 시 HANDLED, 불확실하면 DONE으로 체인 유지 |
| 해제 | unregister_nmi_handler() | 모듈 언로드 경로에서 반드시 해제, 실패 시 다음 로드에서 중복 등록 |
| 진단 | /proc/interrupts, tracepoint, dmesg | 배포 전 NMI 카운터 증가 패턴과 로그 폭주 여부를 함께 확인 |
/* 모듈에서 NMI 핸들러 등록/해제 시 권장 패턴 */
static int my_pmu_nmi_handler(unsigned int type,
struct pt_regs *regs)
{
if (!my_event_overflowed(regs))
return NMI_DONE;
this_cpu_inc(my_nmi_hits);
my_store_sample_nmi(regs); /* lockless ring/per-cpu 버퍼 */
return NMI_HANDLED;
}
static int __init my_nmi_init(void)
{
int ret;
ret = register_nmi_handler(NMI_LOCAL, my_pmu_nmi_handler,
NMI_FLAG_LAST, "my-nmi-profiler");
if (ret)
return ret;
return 0;
}
static void __exit my_nmi_exit(void)
{
unregister_nmi_handler(NMI_LOCAL, "my-nmi-profiler");
}
NMI 컨텍스트 지연 처리 설계
NMI 안에서 모든 처리를 끝내려 하면 lock 경합(Contention)과 지연 시간이 급증합니다. 실무에서는 NMI에서 최소 정보만 기록하고, 나머지는 IRQ/workqueue/thread로 넘기는 2단계 파이프라인(Pipeline)을 사용합니다. 핵심은 NMI 경로는 기록만, 해석/출력/메모리 작업은 지연 경로로 분리하는 것입니다.
/* NMI에서 고정 크기 샘플만 적재하고, 소비는 워커가 수행 */
struct nmi_sample {
u64 tsc;
u64 ip;
u32 cpu;
u32 reason;
};
struct nmi_ring {
struct nmi_sample buf[1024];
u32 head;
u32 tail;
u64 dropped;
};
static DEFINE_PER_CPU(struct nmi_ring, nmi_rings);
static __always_inline void push_nmi_sample(struct pt_regs *regs, u32 reason)
{
struct nmi_ring *r = this_cpu_ptr(&nmi_rings);
u32 head = READ_ONCE(r->head);
u32 next = (head + 1) & (1024 - 1);
if (next == READ_ONCE(r->tail)) {
WRITE_ONCE(r->dropped, r->dropped + 1);
return;
}
r->buf[head].tsc = rdtsc();
r->buf[head].ip = instruction_pointer(regs);
r->buf[head].cpu = smp_processor_id();
r->buf[head].reason = reason;
smp_wmb();
WRITE_ONCE(r->head, next);
}
NMI 운영 튜닝 체크리스트
운영 환경에서는 NMI를 무조건 강하게 켜는 것보다, 하드웨어/가상화/부하 특성에 맞춰 임계값과 정책을 조정해야 합니다. 아래 체크리스트를 순서대로 적용하면 오탐(false positive)과 미탐(false negative)을 균형 있게 줄일 수 있습니다.
| 점검 항목 | 권장 절차 | 목표 |
|---|---|---|
| PMU 가용성 | 부팅 로그에서 Perf NMI watchdog 활성 확인 | watchdog 자체 동작 보장 |
| 가상화 환경 | PMU 미가상화 VM은 nmi_watchdog=0 고려 | 오탐 억제 |
| 임계값 | watchdog_thresh를 워크로드 지연 특성에 맞게 상향/하향 | 민감도 조정 |
| panic 정책 | 운영군별로 hardlockup_panic 분리 적용 | 가용성 vs 빠른 격리(Isolation) 균형 |
| 관측 파이프라인 | NMI 발생 시 trace/perf/kdump 아티팩트 자동 수집 | 사후 분석 시간 단축 |
| 락 검증 | 스테이징에서 lockdep + stress로 deadlock 재현 | 사전 결함 제거 |
# 1) 현재 watchdog 관련 런타임 설정 확인
sysctl kernel.nmi_watchdog
sysctl kernel.watchdog_thresh
sysctl kernel.hardlockup_panic
sysctl kernel.unknown_nmi_panic
# 2) NMI/LOC 카운터 추이 관측
watch -n 1 'grep -E "NMI|LOC" /proc/interrupts'
# 3) 오탐이 많은 경우 임계값 완화
echo 20 > /proc/sys/kernel/watchdog_thresh
# 4) 치명 서비스군은 hardlockup 즉시 패닉 + kdump 연동
echo 1 > /proc/sys/kernel/hardlockup_panic
systemctl status kdump
# 5) NMI 이벤트와 스택 샘플 동시 수집
perf record -a -g -e cycles -- sleep 15
echo l > /proc/sysrq-trigger
hardlockup_panic=0으로 시작해 데이터 수집 중심으로 운영하고, 금융/제어 같이 정합성이 최우선인 노드는 hardlockup_panic=1 + 자동 재기동 + kdump 저장을 함께 구성하는 방식이 안정적입니다.
실제 NMI 관련 크래시 시나리오
운영 환경에서 자주 접하는 NMI 관련 크래시 메시지와 진단 방법을 정리합니다. dmesg 패턴을 이해하면 문제의 원인을 빠르게 파악할 수 있습니다.
시나리오 1: Hardlockup Panic
# dmesg 패턴: hardlockup 감지
[ 892.123456] NMI watchdog: Watchdog detected hard LOCKUP on cpu 3
[ 892.123457] Modules linked in: nvidia(PO) kvm_intel kvm irqbypass
[ 892.123460] CPU: 3 PID: 1234 Comm: kworker/3:1 Tainted: P O 6.1.0
[ 892.123462] Hardware name: Dell Inc. PowerEdge R740
[ 892.123463] RIP: 0010:_raw_spin_lock+0x18/0x30
[ 892.123465] Call Trace:
[ 892.123466] do_something_locked+0x42/0x100
[ 892.123467] worker_thread+0x1a3/0x3d0
[ 892.123468] kthread+0xd2/0x100
[ 892.123470] Kernel panic - not syncing: Hard LOCKUP
# 진단: 인터럽트 비활성 상태에서 spin_lock에서 무한 대기
# 원인: 다른 CPU가 같은 lock을 들고 있는 상태에서 deadlock
# 해결: lock ordering 검토, lockdep 활성화하여 원인 파악
시나리오 2: Unknown NMI
# dmesg 패턴: unknown NMI 수신
[ 456.789012] Uhhuh. NMI received for unknown reason 2d on CPU 0.
[ 456.789013] Do you have a strange power saving mode enabled?
[ 456.789014] Dazed and confused, but trying to continue
# 진단: 등록된 핸들러가 처리하지 못한 NMI
# 가능한 원인:
# - 하드웨어 문제 (메모리 에러, 전원 문제)
# - 외부 NMI 버튼 (의도적)
# - BIOS/펌웨어 버그
# - I/O 디바이스 에러
# 반복 발생 시 하드웨어 점검 필요
# EDAC 드라이버로 메모리 에러 확인:
cat /sys/devices/system/edac/mc/mc0/ce_count
cat /sys/devices/system/edac/mc/mc0/ue_count
시나리오 3: GHES NMI (메모리 에러)
# dmesg 패턴: GHES를 통한 메모리 에러 보고
[ 234.567890] GHES: SEV_CORRECTED, section type: Memory Error
[ 234.567891] {1} hardware error, severity: corrected
[ 234.567892] memory error: physical address: 0x123456000
[ 234.567893] node: 0 card: 0 module: 1 rank: 0 bank: 3 row: 1234 col: 567
[ 234.567894] DIMM location: CPU0_DIMM_A1
# Uncorrectable 에러 (더 심각):
[ 345.678901] GHES: SEV_FATAL, section type: Memory Error
[ 345.678902] {2} hardware error, severity: fatal
[ 345.678903] memory error: physical address: 0x789abc000
[ 345.678904] Kernel panic - not syncing: Fatal hardware error!
# 진단: 메모리 하드웨어 결함
# CE(Correctable): 모니터링, DIMM 교체 계획
# UE(Uncorrectable): 즉시 DIMM 교체, 영향받은 페이지 오프라인
# 메모리 에러 모니터링
rasdaemon --record # 에러 이벤트 기록
ras-mc-ctl --errors # 에러 요약 표시
mcelog --client # MCE 로그 확인
시나리오 4: SMI에 의한 NMI Watchdog False Positive
# dmesg 패턴: SMI 간섭으로 인한 false hardlockup
[ 567.890123] NMI watchdog: Watchdog detected hard LOCKUP on cpu 7
[ 567.890124] CPU: 7 PID: 0 Comm: swapper/7 Not tainted 6.1.0
[ 567.890126] Hardware name: HP ProLiant DL380 Gen10
[ 567.890127] RIP: 0010:native_safe_halt+0x5/0x10
[ 567.890129] Call Trace:
[ 567.890130] default_idle+0x1c/0x180
[ 567.890131] arch_cpu_idle+0x15/0x20
[ 567.890132] default_idle_call+0x30/0x90
# 진단: CPU가 idle 상태에서 hardlockup 감지
# 원인: SMI(System Management Interrupt)가 CPU를 장시간 빼앗음
# - BIOS/펌웨어가 SMI 핸들러에서 오래 머무름
# - SMI 동안 NMI watchdog 타이머도 정지하지 않음
# - 결과: 실제 lockup이 아닌데 watchdog 발동
# 확인 방법: SMI 카운터 확인
rdmsr -p 7 0x34 # MSR_SMI_COUNT (IA32_SMI_COUNT)
# 또는
cat /sys/devices/cpu/events/smi # perf SMI 이벤트 (일부 시스템)
# 해결:
# 1. watchdog_thresh 상향 (SMI 지속 시간보다 크게)
echo 30 > /proc/sys/kernel/watchdog_thresh
# 2. BIOS에서 불필요한 SMI 소스 비활성화
# 3. 펌웨어 업데이트로 SMI 핸들러 최적화
시나리오 5: perf와 watchdog 간 PMU 카운터 경합
# dmesg 패턴: perf 실행 시 NMI watchdog 비활성화
[ 2.345678] NMI watchdog: Perf NMI watchdog permanently disabled
[ 100.123456] perf: interrupt took too long (3250 > 3212), lowering kernel.perf_event_max_sample_rate to 50000
# 또는: perf 실행 시 watchdog PMU 이벤트 릴리즈
[ 200.567890] watchdog: BUG: soft lockup - CPU#4 stuck for 23s!
# → hardlockup이 아닌 softlockup만 감지됨 (PMU NMI 비활성)
# 원인: PMU 카운터는 CPU당 제한된 수 (Intel: 4~8개)
# - NMI watchdog이 1개 사용 (cycles 카운터)
# - perf가 모든 카운터를 요청하면 watchdog 양보
# - CONFIG_HARDLOCKUP_DETECTOR_PERF 비활성화됨
# 확인: PMU 카운터 가용성
cat /proc/sys/kernel/nmi_watchdog
perf list --no-pager | grep "Hardware event"
ls /sys/bus/event_source/devices/cpu/caps/
# 해결:
# 1. 필요한 perf 이벤트 수를 최소화
# 2. multiplexing 허용 (정밀도 약간 감소)
perf stat -e cycles,instructions,cache-misses,branches -a sleep 5
# 3. hardlockup_detector_buddy 사용 (PMU 불필요)
# CONFIG_HARDLOCKUP_DETECTOR_BUDDY=y
시나리오 6: NMI 핸들러 내부 Deadlock
# dmesg 패턴: NMI 핸들러에서 교착 (시스템 완전 정지)
# → dmesg 출력 자체가 불가능한 경우가 많음
# → serial console에 아무것도 나오지 않음
# → kdump도 트리거되지 않음 (NMI 핸들러가 리턴하지 않으므로)
# crash dump가 있는 경우 (IPMI NMI로 강제 panic 후):
crash> bt
PID: 0 TASK: ffff888100000000 CPU: 3 COMMAND: "swapper/3"
#0 _raw_spin_lock at ffffffff81a12345
#1 printk at ffffffff8110abcd # ← NMI에서 printk 호출
#2 my_buggy_nmi_handler at ffffffffa0001234
#3 nmi_handle at ffffffff8102ef01
#4 default_do_nmi at ffffffff8102f234
#5 exc_nmi at ffffffff81a56789
# 원인: NMI 핸들러에서 금지된 API 호출
# - printk()가 내부 logbuf_lock을 획득 시도
# - 해당 CPU가 이미 printk 중이었다면 self-deadlock
# - NMI 핸들러가 리턴하지 않으므로 시스템 완전 정지
# 예방:
# 1. NMI 핸들러에서 printk() 대신 nmi_panic() 또는 trace_printk()
# 2. 모든 lock은 trylock 패턴으로 대체
# 3. 메모리 할당 절대 금지
# 4. lockdep 활성화로 NMI 컨텍스트 위반 사전 감지
exc_nmi, nmi_handle)을 찾는 것입니다. NMI 핸들러 내부에서 금지된 API를 호출하면 시스템이 완전 정지하여 crash dump조차 남지 않을 수 있으므로, NMI 코드 리뷰에서 사전에 차단하는 것이 최선입니다.
시나리오 7: NMI Watchdog 비활성 (가상화 환경)
# dmesg 패턴: PMU 미지원으로 watchdog 자동 비활성
[ 0.000000] NMI watchdog: Perf NMI watchdog permanently disabled
[ 0.123456] NMI watchdog: Perf event create on CPU 0 failed with -2
# 원인: VM 환경에서 PMU 가상화가 비활성
# KVM에서 PMU 활성화:
# qemu-system-x86_64 -cpu host,pmu=on ...
# 또는 의도적으로 비활성화 (VM 권장):
# 커널 파라미터: nmi_watchdog=0
panic()을 호출하면 smp_send_stop() 과정에서 다른 CPU에 IPI를 보내는 중 deadlock이 발생할 수 있습니다. 반드시 nmi_panic()을 사용해야 하며, 이 함수는 NMI-safe 경로로 panic을 수행합니다. nmi_panic()은 내부적으로 panic_smp_self_stop()과 printk_nmi_flush()을 호출하여 NMI 컨텍스트에서도 안전하게 시스템을 정지시킵니다.
NMI와 SMI 상호작용
SMI(System Management Interrupt)는 NMI보다 우선순위가 높은 유일한 인터럽트로, CPU를 SMM(System Management Mode)으로 전환시킵니다. SMI 처리 중에는 NMI를 포함한 모든 인터럽트가 차단되므로, SMI는 NMI watchdog의 가장 큰 교란 요인입니다.
SMI 감지 및 모니터링
# SMI 카운터 확인 (Intel CPU)
# MSR 0x34 = IA32_SMI_COUNT (부팅 이후 SMI 누적 횟수)
rdmsr 0x34 # 현재 CPU
rdmsr -a 0x34 # 모든 CPU
# perf를 이용한 SMI 카운터 모니터링 (일부 시스템)
perf stat -e msr/smi/ -a sleep 10
# SMI 지속 시간 추정
# 1. TSC 기반: SMI 전후 TSC 차이 측정
# 2. hwlatdetect: SMI/NMI에 의한 레이턴시 감지
hwlatdetect --duration=60 --threshold=100
# cyclictest로 SMI 영향 확인 (RT 환경)
cyclictest -p 80 -t -m -D 5m
# Max latency가 수백 us 이상이면 SMI 의심
# /sys/kernel/debug/x86/tsc_khz와 비교하여
# TSC 점프가 SMI 지속 시간과 일치하는지 확인
/* SMI가 NMI watchdog에 미치는 영향 - 커널 내부 */
/* SMI 동안 발생하는 상황:
* 1. CPU가 SMM(System Management Mode)으로 전환
* 2. SMRAM의 SMI 핸들러 코드 실행
* 3. 모든 인터럽트(NMI 포함) 차단됨
* 4. TSC는 계속 카운트 (대부분의 현대 CPU)
* 5. PMU 카운터도 계속 카운트
* 6. 결과: hrtimer 콜백이 실행되지 않아
* hrtimer_interrupts 변수가 업데이트 안 됨
* 7. SMI 종료 후 다음 NMI에서 watchdog이
* hrtimer_interrupts 변화 없음을 감지 → false hardlockup!
*/
/* SMI 카운터 읽기 (커널 내부 코드) */
static u64 read_smi_count(void)
{
u64 count;
rdmsrl(MSR_SMI_COUNT, count); /* 0x34 */
return count & 0xFFFFFFFF;
}
/* SMI latency 측정을 위한 트레이서 (hwlat_detector) */
/* kernel/trace/trace_hwlat.c */
static void hwlat_detector_thread(void)
{
u64 start, now, delta;
local_irq_disable();
start = trace_clock_local();
do {
now = trace_clock_local();
delta = now - start;
/* delta가 threshold를 초과하면 SMI 의심 */
if (delta > threshold)
trace_hwlat_sample(delta);
start = now;
} while (!kthread_should_stop());
local_irq_enable();
}
| SMI 소스 | 지속 시간 | NMI watchdog 영향 | 완화 방법 |
|---|---|---|---|
| 메모리 스크러빙 | 100ms~수 초 | 높음 (false hardlockup) | BIOS에서 비활성화 또는 주기 연장 |
| UEFI 런타임 서비스 | 10~100ms | 중간 | efi=runtime 파라미터 비활성화 |
| BMC/IPMI 통신 | 1~50ms | 낮음~중간 | IPMI 폴링 주기 조정 |
| USB 레거시 에뮬레이션 | 1~10ms | 낮음 | BIOS에서 USB Legacy Support 비활성화 |
| 열 관리 | 가변적 | 중간 | BIOS 열 관리 정책 조정 |
| 하드웨어 에러 처리 | 가변적 | 높음 (CE 폭풍 시) | DIMM 교체, 에러 소스 격리 |
IA32_SMI_COUNT MSR은 횟수만 제공하고 지속 시간은 알 수 없습니다. 정밀한 SMI 레이턴시 측정이 필요하면 hwlatdetect를 사용하되, 이것도 NMI에 의한 레이턴시와 구분하기 어려울 수 있습니다.
rdmsr 0x34로 SMI 빈도 확인, (2) BIOS에서 불필요한 SMI 소스(USB Legacy, 메모리 스크러빙 주기) 조정, (3) watchdog_thresh를 SMI 최대 지속 시간보다 크게 설정, (4) 그래도 해결 안 되면 HARDLOCKUP_DETECTOR_BUDDY 사용을 검토합니다.
NMI 코드 리뷰 체크리스트
NMI 코드 품질은 기능보다 실패 시나리오로 평가해야 합니다. 아래 체크리스트는 커널 모듈(Kernel Module)/플랫폼 코드 리뷰 시 자주 사용하는 항목입니다. 특히 lock, 반환값, 지연 처리 경계, 로그 폭주 제어를 먼저 점검하면 운영 장애 가능성을 크게 줄일 수 있습니다.
| 체크 항목 | 질문 | 권장 기준 |
|---|---|---|
| 컨텍스트 안전성 | 핸들러 내부에 mutex, schedule, kmalloc이 있는가? | 모두 제거, per-CPU + atomic + trylock으로 대체 |
| 반환값 정확성 | 원인 미확정인데 NMI_HANDLED를 반환하는가? | 불확실하면 NMI_DONE, 오탐 핸들링 금지 |
| 지연 처리 분리 | 복잡한 파싱/출력을 NMI에서 수행하는가? | NMI는 기록만, 해석/출력은 workqueue로 이동 |
| 로그 제어 | 반복 NMI 시 로그 폭주 방어가 있는가? | rate limit 또는 카운터 누적 후 요약 출력 |
| 운영 정책 일치 | hardlockup_panic와 복구 정책이 충돌하는가? | 서비스군별 정책 문서화 + 부팅 파라미터 고정 |
| 언로드 안전성 | 모듈 종료 시 핸들러 해제가 보장되는가? | unregister_nmi_handler() 필수, 중복 등록 방지 |
/* 리뷰 중 자주 발견되는 NMI 안티패턴과 개선 예시 */
static int bad_nmi_handler(unsigned int type, struct pt_regs *regs)
{
mutex_lock(&global_lock); /* 금지: sleep 가능 */
void *p = kmalloc(256, GFP_KERNEL); /* 금지: allocator lock */
pr_info("NMI ip=%px\\n", (void *)regs->ip); /* 폭주 가능 */
mutex_unlock(&global_lock);
return NMI_HANDLED;
}
static int good_nmi_handler(unsigned int type, struct pt_regs *regs)
{
if (!my_reason_match(regs))
return NMI_DONE;
this_cpu_inc(my_stats.hits);
push_nmi_sample(regs, MY_REASON_CODE); /* lockless per-CPU 기록 */
irq_work_queue(&my_drain_work); /* 상세 처리는 지연 경로 */
return NMI_HANDLED;
}
NMI_HANDLED를 반환하는지", "NMI에서 무엇을 하지 않도록 설계했는지"를 확인하면 결함을 빠르게 걸러낼 수 있습니다.
NMI 장애 대응 플레이북
운영 중 NMI 경고가 발생하면 원인 파악 전에 재부팅만 수행하는 경우가 많습니다. 하지만 Unknown NMI와 GHES는 하드웨어 결함 신호일 수 있으므로, 초기 1시간 안에 로그/카운터/덤프(Dump)를 구조적으로 수집해야 재발 방지까지 연결됩니다.
| 시간대 | 핵심 목표 | 필수 액션 |
|---|---|---|
| 0~10분 | 증거 보존 | dmesg 고정 저장, /proc/interrupts 스냅샷, sysctl 값 수집, 가능하면 SysRq-l 수행 |
| 10~30분 | 원인 후보 축소 | Unknown NMI vs GHES 분리, 반복 주기 확인, 동일 커널/BIOS/하드웨어군 상관 분석 |
| 30~60분 | 운영 조치 확정 | panic 정책 조정, 문제 노드 격리, 펌웨어/메모리 교체 또는 커널 롤백(Rollback) 여부 결정 |
# [0~10분] 증거 보존 스크립트 예시
TS="$(date +%Y%m%d-%H%M%S)"
OUT="/var/tmp/nmi-incident-$TS"
mkdir -p "$OUT"
dmesg -T > "$OUT/dmesg.txt"
cat /proc/interrupts > "$OUT/interrupts.txt"
sysctl kernel.nmi_watchdog kernel.watchdog_thresh \
kernel.hardlockup_panic kernel.unknown_nmi_panic \
> "$OUT/sysctl-watchdog.txt"
grep -iE "nmi|ghes|apei|mce|watchdog" "$OUT/dmesg.txt" > "$OUT/dmesg-nmi-focus.txt"
# 가능하면 모든 CPU 스택 확보
echo l > /proc/sysrq-trigger
# [10~30분] Unknown NMI vs GHES 분류 보조
grep -i "unknown reason" "$OUT/dmesg.txt"
grep -iE "GHES|APEI|hardware error|SEV_" "$OUT/dmesg.txt"
grep -E "NMI|LOC" /proc/interrupts
# [30~60분] 하드웨어 신호 확인
ras-mc-ctl --errors
cat /sys/devices/system/edac/mc/mc*/ce_count 2>/dev/null
cat /sys/devices/system/edac/mc/mc*/ue_count 2>/dev/null
SEV_FATAL 또는 UE(수정 불가) 메모리 에러가 보이면 즉시 노드 격리 후 DIMM/플랫폼 점검을 진행하세요. CE만 누적될 때도 증가율이 급격하면 예방 교체 계획을 바로 세우는 것이 안전합니다.
NMI 장애 보고서 템플릿
NMI 장애는 하드웨어/펌웨어/커널 경계에 걸쳐 있어, 재현이 어려운 대신 재발 비용이 큽니다. 아래 템플릿으로 보고서를 표준화하면 원인 추적, 영향 평가, 재발 방지 액션까지 일관되게 관리할 수 있습니다.
| 섹션 | 작성 포인트 | 예시 키 |
|---|---|---|
| 사건 개요 | 언제/어디서/무엇이 발생했는지 3줄 요약 | 발생 시각, 노드, 서비스 |
| 증상 및 영향 | 사용자 영향과 내부 영향 분리 | 장애율, 지연 증가, 재기동 횟수 |
| 기술 분석 | Unknown NMI/GHES/hardlockup 중 분류와 근거 | dmesg 패턴, CE/UE 카운터, vmcore |
| 근본 원인 | 1차 원인 + 촉발 조건 + 탐지 실패 원인 | DIMM 불량, PMU 미가상화, lock 경합 |
| 조치 내역 | 즉시 조치와 영구 조치를 구분 | 노드 격리, BIOS 업데이트, 코드 수정 |
| 재발 방지 | 모니터링/테스트/정책 항목으로 분해 | 알람 추가, lockdep CI, 운영 기준 변경 |
제목: [NMI] INCIDENT-YYYYMMDD-서비스명
작성일: YYYY-MM-DD HH:MM KST
작성자: 팀/이름
1) 사건 개요
- 발생 시각:
- 감지 경로: (알람/사용자 신고/자동 watchdog)
- 영향 서비스:
- 장애 등급:
2) 증상 및 영향
- 사용자 영향:
- 내부 영향: (노드 다운, 재기동, 성능 저하)
- 영향 범위: (노드 수, 리전, AZ)
- 종료 시각 및 총 지속 시간:
3) 기술 분석
- NMI 유형: (Unknown NMI / GHES / Hardlockup)
- 핵심 로그:
- dmesg:
- /proc/interrupts:
- GHES/EDAC:
- 재현 여부: (가능/불가)
- 재현 조건:
4) 근본 원인
- 직접 원인:
- 촉발 조건:
- 탐지/격리 지연 원인:
5) 조치 내역
- 즉시 조치 (0~1시간):
- 단기 조치 (24시간 내):
- 영구 조치 (1~2주):
6) 재발 방지
- 모니터링 개선:
- 테스트 개선:
- 운영 정책 변경:
- 오너/기한:
7) 첨부 아티팩트
- nmi-incident 번들 경로:
- vmcore 경로:
- 관련 티켓/PR:
작성 예시 (요약)
제목: [NMI] INCIDENT-20260227-api-gateway
작성일: 2026-02-27 15:40 KST
1) 사건 개요
- 14:02 KST, api-gw 노드 3대에서 hardlockup NMI 발생
- 자동 감지: kernel watchdog alert + node_not_ready
2) 증상 및 영향
- 사용자: 502 응답률 3.2% (약 7분)
- 내부: 노드 3대 재기동, HPA 급격 확장
3) 기술 분석
- NMI 유형: Hardlockup
- 로그: "Watchdog detected hard LOCKUP on cpu"
- 공통점: 동일 커널 빌드 + 특정 드라이버 버전
4) 근본 원인
- 직접 원인: 드라이버 내부 spinlock 경합
- 촉발 조건: 고부하 + IRQ 비활성 구간 장기화
5) 조치 내역
- 즉시: 문제 노드 격리, 트래픽 우회
- 단기: 드라이버 롤백, watchdog_thresh 조정
- 영구: 락 순서 수정 패치 배포
6) 재발 방지
- lockdep 기반 스트레스 테스트를 릴리스 게이트에 추가
- hardlockup 알람에 자동 SysRq-l 수집 연동
NMI 진입 어셈블리(Assembly) 상세 분석
x86_64에서 NMI가 발생하면 CPU는 IDT#2를 통해 asm_exc_nmi로 진입합니다. 이 어셈블리 코드는 일반 인터럽트 진입과 달리, NMI 중첩 감지, IST 스택 관리, IRET 프레임 보호 등 복잡한 상태 머신을 구현해야 합니다. 이 섹션에서는 arch/x86/entry/entry_64.S의 NMI 진입 어셈블리를 단계별로 분석합니다.
asm_exc_nmi 핵심 코드 흐름
NMI 진입 어셈블리는 크게 세 가지 경우를 구분합니다: (1) 최초 NMI, (2) 중첩 NMI, (3) NMI 핸들러 복귀 중 발생한 NMI.
/* arch/x86/entry/entry_64.S - asm_exc_nmi 핵심 흐름 (간략화) */
SYM_CODE_START(asm_exc_nmi)
/* CPU가 IST#2 스택에 SS,RSP,RFLAGS,CS,RIP 푸시 완료 */
/* 1단계: 중첩 확인 - 현재 RSP가 NMI 스택 범위 내인지 */
lea -10*8(%rsp), %rdx
cmp %rdx, 4*8(%rsp) /* 중단된 RSP vs NMI 스택 하단 */
jb first_nmi
/* 이전 NMI 실행 중인지 확인 */
cmp $1, -8(%rsp) /* nmi_executing 변수 */
je nested_nmi
first_nmi:
/* 2단계: IRET 프레임을 보호 영역으로 복사 */
movq (%rsp), %rdx /* RIP */
movq %rdx, -5*8(%rsp) /* 복사본 RIP */
/* ... SS, RSP, RFLAGS, CS도 복사 ... */
/* 3단계: nmi_executing = 1 설정 */
movq $1, 5*8(%rsp)
/* 4단계: pt_regs 프레임 구성 후 C 핸들러 호출 */
pushq $-1 /* orig_rax = -1 (NMI는 시스템 콜 아님) */
PUSH_AND_CLEAR_REGS
movq %rsp, %rdi /* pt_regs 포인터 */
call exc_nmi
nmi_return:
POP_REGS
/* 5단계: repeat_nmi 확인 */
testl %ebx, %ebx
jnz repeat_nmi
/* nmi_executing = 0 클리어 */
movq $0, 5*8(%rsp)
/* 보호된 IRET 프레임으로 복귀 */
iretq
nested_nmi:
/* 복귀 주소를 repeat_nmi로 수정 */
leaq repeat_nmi(%rip), %rdx
movq %rdx, 4*8+0(%rsp) /* 첫 번째 NMI의 IRET RIP 수정 */
jmp nmi_restore /* 즉시 IRET → repeat_nmi로 돌아감 */
repeat_nmi:
/* 전체 NMI 핸들링을 처음부터 재시작 */
jmp first_nmi
SYM_CODE_END(asm_exc_nmi)
pt_regs 프레임 레이아웃
NMI 핸들러에 전달되는 pt_regs 구조체는 다음과 같은 스택 프레임(Stack Frame)에서 구성됩니다:
/* arch/x86/include/asm/ptrace.h */
struct pt_regs {
/* PUSH_AND_CLEAR_REGS가 저장하는 범용 레지스터 */
unsigned long r15, r14, r13, r12;
unsigned long bp, bx;
unsigned long r11, r10, r9, r8;
unsigned long ax, cx, dx, si, di;
unsigned long orig_ax; /* NMI: -1 */
/* CPU가 자동 저장하는 IRET 프레임 */
unsigned long ip; /* 중단된 RIP */
unsigned long cs;
unsigned long flags; /* RFLAGS */
unsigned long sp; /* 중단된 RSP */
unsigned long ss;
};
IRET 명령어는 원자적(atomic)이 아닙니다. IRET이 스택에서 RIP를 팝하고 CS를 팝하는 사이에 또 다른 NMI가 발생하면, CPU는 IST#2 스택을 다시 사용하면서 진행 중인 IRET 프레임을 덮어씁니다. 이것이 NMI 트램폴린(스택 복사)이 필요한 근본적인 이유입니다.
| 단계 | 동작 | 핵심 목적 |
|---|---|---|
| 1. IST 진입 | CPU 하드웨어가 IST#2 스택으로 자동 전환 | 현재 스택 상태와 무관하게 안전한 스택 확보 |
| 2. 중첩 확인 | RSP 범위 + nmi_executing 변수 검사 | NMI-in-NMI 상황 감지 |
| 3. 스택 전환 | IRET 프레임을 보호 영역에 복사 | IRET 중 NMI 재진입 시 프레임 보호 |
| 4. C 핸들러 | pt_regs 구성 후 exc_nmi() 호출 | 등록된 NMI 핸들러 체인 실행 |
| 5. 복귀/재처리 | repeat_nmi 확인 후 IRET 또는 재시작(Reboot) | 중첩 NMI 누락 방지 |
NMI-safe printk 메커니즘
표준 printk()는 내부적으로 스핀락(Spinlock), console 드라이버 호출 등 NMI 컨텍스트에서 안전하지 않은 연산을 수행합니다. 커널은 NMI 내부에서도 로그를 안전하게 기록할 수 있도록 별도의 NMI-safe printk 경로를 제공합니다.
printk NMI 내부 구현
/* kernel/printk/printk_safe.c */
static DEFINE_PER_CPU(struct printk_safe_seq_buf, nmi_print_seq);
struct printk_safe_seq_buf {
atomic_t len; /* 현재 버퍼 사용량 */
atomic_t message_lost; /* 오버플로 시 유실 카운트 */
struct irq_work work; /* flush용 irq_work */
unsigned char buffer[SAFE_LOG_BUF_LEN]; /* 8KB 기본 */
};
/* NMI 진입 시 호출 */
void printk_nmi_enter(void)
{
this_cpu_or(printk_context, PRINTK_NMI_CONTEXT_MASK);
}
/* NMI 종료 시 호출 */
void printk_nmi_exit(void)
{
this_cpu_and(printk_context, ~PRINTK_NMI_CONTEXT_MASK);
}
/* NMI-safe 버퍼에 쓰기 */
static int vprintk_nmi(const char *fmt, va_list args)
{
struct printk_safe_seq_buf *s = this_cpu_ptr(&nmi_print_seq);
int add = 0;
size_t len;
/* atomic하게 버퍼 공간 예약 */
len = atomic_read(&s->len);
if (len >= sizeof(s->buffer)) {
atomic_inc(&s->message_lost);
return 0;
}
add = vsnprintf(s->buffer + len,
sizeof(s->buffer) - len, fmt, args);
atomic_add(add, &s->len);
/* irq_work를 통해 NMI 종료 후 flush 예약 */
irq_work_queue(&s->work);
return add;
}
message_lost 카운터가 증가하면 "printk_safe_seq_buf: N message(s) dropped" 경고가 출력됩니다.
| 함수 | 호출 시점 | 동작 |
|---|---|---|
printk_nmi_enter() | nmi_enter() 내부 | printk_context에 NMI 플래그 설정 |
printk_nmi_exit() | nmi_exit() 내부 | NMI 플래그 클리어 |
vprintk_nmi() | NMI 내 printk 호출 시 | Per-CPU seq_buf에 lockless 기록 |
printk_safe_flush() | irq_work 콜백(Callback) | seq_buf → 메인 log_buf 전송 |
printk_safe_flush_on_panic() | panic() 경로 | 모든 CPU의 NMI 버퍼 강제 flush |
NMI와 RCU 상호작용
NMI는 CPU가 어떤 상태에 있든 발생할 수 있으므로, RCU(Read-Copy-Update)의 상태 추적에 특별한 주의가 필요합니다. 특히 CPU가 Extended Quiescent State(EQS)에 있을 때(idle, user-mode, nohz_full) NMI가 발생하면, RCU는 이 CPU가 갑자기 커널 코드를 실행하게 되었음을 인지해야 합니다.
nmi_enter() 내부의 RCU 호출 순서
/* include/linux/hardirq.h */
#define nmi_enter() \
do { \
lockdep_off(); \
arch_nmi_enter(); \
printk_nmi_enter(); \
BUG_ON(in_nmi() == 1); \
__preempt_count_add(NMI_OFFSET + HARDIRQ_OFFSET); \
rcu_nmi_enter(); /* ← EQS 해제 */ \
ftrace_nmi_enter(); \
instrumentation_begin(); \
} while (0)
/* kernel/rcu/tree.c */
void rcu_nmi_enter(void)
{
struct rcu_data *rdp = this_cpu_ptr(&rcu_data);
long incby = 2; /* 짝수 = EQS, 홀수 = Active */
/* 이미 NMI 내부인 경우 nesting만 증가 */
if (rdp->dynticks_nmi_nesting) {
rdp->dynticks_nmi_nesting++;
return;
}
rdp->dynticks_nmi_nesting = 1;
/* EQS 상태에서 NMI 발생: dynticks 카운터를 홀수로 전이 */
if (rcu_dynticks_curr_cpu_in_eqs()) {
rcu_dynticks_eqs_exit();
incby = 1;
}
}
| 함수 | 동작 | dynticks 변화 |
|---|---|---|
rcu_nmi_enter() | EQS 해제 (첫 번째 NMI) | 짝수 → 홀수 |
rcu_nmi_enter() | nesting 증가 (중첩 NMI) | 변화 없음 |
rcu_nmi_exit() | EQS 복원 (마지막 NMI) | 홀수 → 짝수 |
rcu_nmi_exit() | nesting 감소 (중첩 NMI 종료) | 변화 없음 |
NMI 핸들러 레이턴시 측정
NMI 핸들러는 최대한 빠르게 실행되어야 합니다. 핸들러가 과도하게 긴 시간을 소비하면 시스템 전체의 응답성에 영향을 미칩니다. 커널은 nmi_check_duration()을 통해 핸들러 실행 시간을 모니터링하고, 임계값 초과 시 경고를 출력합니다.
nmi_check_duration 구현
/* arch/x86/kernel/nmi.c */
static u64 nmi_longest_ns = 1000000; /* 1ms, 초기 임계값 */
static int nmi_handle(unsigned int type, struct pt_regs *regs)
{
struct nmi_desc *desc = nmi_to_desc(type);
struct nmiaction *a;
int handled = 0;
rcu_read_lock();
list_for_each_entry_rcu(a, &desc->head, list) {
u64 before, delta, whole_msecs;
int decimal_msecs;
before = sched_clock();
handled |= a->handler(type, regs);
delta = sched_clock() - before;
/* 레이턴시 검사 */
if (delta < nmi_longest_ns)
continue;
/* cmpxchg로 atomic 최대값 업데이트 */
if (cmpxchg(&nmi_longest_ns, nmi_longest_ns, delta)
!= nmi_longest_ns)
continue;
whole_msecs = delta;
decimal_msecs = do_div(whole_msecs, (1000 * 1000));
printk_ratelimited(KERN_INFO
"INFO: NMI handler (%ps) took too long to run: "
"%lld.%03d msecs\n",
a->handler, whole_msecs, decimal_msecs / 1000);
}
rcu_read_unlock();
return handled;
}
sched_clock()은 대부분의 x86 시스템에서 TSC를 직접 읽으므로 NMI-safe입니다. 그러나 TSC가 unstable로 표시된 시스템(일부 가상화 환경, 오래된 AMD CPU)에서는 jiffies 기반 폴백을 사용하여 정밀도가 떨어질 수 있습니다.
| 측정 방법 | 도구 | 측정 대상 | 정밀도 |
|---|---|---|---|
| 커널 내부 | nmi_check_duration | 개별 NMI 핸들러 | 나노초 (TSC 기반) |
| perf stat | perf stat -e cycles:P | NMI PMI 오버헤드(Overhead) | 사이클 단위 |
| ftrace | function_graph tracer | NMI 핸들러 호출 트리 | 마이크로초 |
| debugfs | /sys/kernel/debug/nmi_longest_ns | 부팅 후 최대 레이턴시 | 나노초 |
| BPF | bpftrace nmi_handler probe | NMI 핸들러 분포 | 나노초 |
ARM64 Pseudo-NMI
ARM64 아키텍처에는 x86의 NMI에 대응하는 하드웨어 메커니즘이 전통적으로 없었습니다. 대신, GICv3(Generic Interrupt Controller v3)의 인터럽트 우선순위(priority) 기반 마스킹을 활용하여 "pseudo-NMI"를 구현합니다. ARMv8.8+에서는 FEAT_NMI를 통해 하드웨어 NMI 지원이 추가되었습니다.
alloc_nmi() / enable_nmi() API
/* arch/arm64/include/asm/irq.h */
/* pseudo-NMI로 인터럽트를 등록 */
static inline void enable_percpu_nmi(unsigned int irq,
unsigned long flags)
{
enable_percpu_irq(irq, irq_calc_nmi_flags(flags));
}
/* drivers/irqchip/irq-gic-v3.c */
static int gic_irq_nmi_setup(struct irq_data *d)
{
struct irq_desc *desc = irq_to_desc(d->irq);
/* NMI는 percpu devid 핸들러만 지원 */
if (!test_bit(IRQD_PER_CPU, &d->state_use_accessors))
return -EINVAL;
/* priority를 superpriority 그룹으로 설정 */
gic_set_nmi_priority(d);
desc->istate |= IRQS_NMI;
return 0;
}
x86 NMI vs ARM64 pseudo-NMI 비교
| 항목 | x86 NMI | ARM64 pseudo-NMI |
|---|---|---|
| 하드웨어 지원 | 전용 NMI 핀 (IDT#2) | GICv3 우선순위 기반 (소프트웨어) |
| 마스킹 메커니즘 | CPU 내부 NMI blocking | ICC_PMR_EL1 레지스터 |
| 전용 스택 | IST#2 | 없음 (커널 스택 공유) |
| 중첩 문제 | IRET 취약 구간 존재 | PMR 기반으로 중첩 제어 용이 |
| 용도 | PMU, watchdog, MCE, 디버거 | PMU, watchdog |
| 비활성화 | 불가 (하드웨어 레벨) | DAIF.I로 완전 차단 가능 |
| FIQ 관계 | 해당 없음 | ARMv8.8+ FEAT_NMI에서 FIQ=NMI |
| CONFIG 옵션 | 기본 활성 | CONFIG_ARM64_PSEUDO_NMI |
irqchip.gicv3_pseudo_nmi=1 커널 파라미터로 활성화합니다. 기본값은 비활성이며, 활성화하면 local_irq_disable()이 DAIF.I 대신 PMR을 조작하는 방식으로 변경됩니다.
RISC-V NMI 아키텍처
RISC-V에서 NMI는 기본 ISA 사양에서 최소한으로만 정의되어 있으며, 구현은 플랫폼에 의존합니다. Smrnmi(Resumable Non-Maskable Interrupt) 확장이 제안되어 NMI 처리를 표준화하고 있습니다.
x86/ARM64/RISC-V NMI 비교
| 항목 | x86 | ARM64 | RISC-V |
|---|---|---|---|
| NMI 벡터 | IDT#2 (고정) | GICv3 priority 기반 | mtvec + rnmi 벡터 |
| 중첩 방지 | CPU NMI blocking + IST | PMR 마스킹 | mnstatus.NMIE=0 |
| 복귀 명령 | IRET | ERET | mnret |
| 원인 레지스터 | 없음 (소프트웨어 판별) | IAR (GIC) | mncause CSR |
| 전용 스택 | IST#2 | 없음 | 없음 (mnscratch 사용) |
| 표준화 | 완전 표준 | GICv3 사양 | Smrnmi 확장 (옵션) |
| Linux 지원 | 완전 지원 | pseudo-NMI (v4.20+) | 제한적 (SiFive 등) |
| 하드웨어 NMI | 있음 | FEAT_NMI (v8.8+) | Smrnmi (옵션) |
NMI 가상화 (KVM/VMX)
가상화 환경에서 NMI 처리는 호스트와 게스트 사이의 전환(VM-exit/VM-entry)과 밀접하게 관련됩니다. KVM은 호스트 NMI를 적절히 핸들링하고, 게스트에 NMI를 주입(injection)하는 메커니즘을 관리합니다.
AMD SVM vs Intel VMX NMI 가상화
| 항목 | Intel VMX | AMD SVM |
|---|---|---|
| NMI exit 제어 | Pin-Based Controls의 NMI Exiting 비트 | VMCB Intercept의 NMI 비트 |
| Virtual NMI | 지원 (Virtual NMIs 비트) | 제한적 (vNMI는 별도) |
| NMI window | NMI-window Exiting (Primary Proc) | IRET intercept로 구현 |
| NMI blocking 추적 | Guest Interruptibility State | VMCB의 V_NMI_MASK |
| NMI injection | VM-entry Interruption Info | VMCB EventInj |
| nested NMI | IST 문제 동일 | IST 문제 동일 |
/* arch/x86/kvm/vmx/vmx.c - NMI injection */
static void vmx_inject_nmi(struct kvm_vcpu *vcpu)
{
struct vcpu_vmx *vmx = to_vmx(vcpu);
/* VM-entry interruption info: Type=NMI, Vector=2 */
vmcs_write32(VM_ENTRY_INTR_INFO_FIELD,
INTR_TYPE_NMI_INTR | INTR_INFO_VALID_MASK |
NMI_VECTOR);
vmx->loaded_vmcs->nmi_known_unmasked = false;
/* Virtual NMI가 활성이면 IRET 시까지 NMI blocking */
if (vmcs_read32(PIN_BASED_VM_EXEC_CONTROL) &
PIN_BASED_VIRTUAL_NMIS)
vmx->loaded_vmcs->vnmi_blocked = true;
}
/* NMI window exiting 설정 */
static void vmx_enable_nmi_window(struct kvm_vcpu *vcpu)
{
/* 게스트가 NMI blocking을 해제하면 VM-exit 발생 */
vmcs_set_bits(CPU_BASED_VM_EXEC_CONTROL,
CPU_BASED_NMI_WINDOW_EXITING);
}
vnmi_blocked 상태를 세밀하게 추적해야 합니다.
NMI와 kdump 연동 상세
커널 패닉(Kernel Panic) 시 kdump는 NMI를 사용하여 모든 secondary CPU를 강제 정지시킵니다. 이 "NMI shootdown" 메커니즘은 교착 상태에 빠진 CPU에서도 동작하며, crash dump의 일관성을 보장하는 핵심 요소입니다.
/* arch/x86/kernel/crash.c */
static int crash_nmi_callback(unsigned int val,
struct pt_regs *regs)
{
int cpu = raw_smp_processor_id();
/* 현재 CPU 레지스터 상태를 crash 영역에 저장 */
if (cpu_emergency_stop_pt_regs[cpu])
crash_save_cpu(regs, cpu);
/* panic CPU에 정지 완료 알림 */
atomic_dec(&waiting_for_crash_ipi);
/* 이 CPU는 여기서 영원히 멈춤 */
local_irq_disable();
while (1)
cpu_relax(); /* or halt */
return NMI_HANDLED;
}
/* NMI shootdown 시작 */
void nmi_shootdown_cpus(nmi_shootdown_cb callback)
{
unsigned long msecs;
atomic_set(&waiting_for_crash_ipi,
num_online_cpus() - 1);
register_nmi_handler(NMI_LOCAL, crash_nmi_callback,
NMI_FLAG_FIRST, "crash");
/* 모든 다른 CPU에 NMI 전송 */
apic_send_IPI_allbutself(NMI_VECTOR);
/* 최대 1초 대기 */
msecs = 1000;
while (atomic_read(&waiting_for_crash_ipi) > 0
&& msecs) {
mdelay(1);
msecs--;
}
}
unknown_nmi_panic=1 설정 시, IPMI BMC의 NMI 진단 인터럽트가 의도치 않은 패닉과 kdump를 유발할 수 있습니다. 운영 환경에서는 반드시 crashkernel= 부팅 파라미터와 함께 kdump 서비스가 올바르게 설정되었는지 확인하세요.
| 단계 | 함수 | 동작 | 타임아웃 |
|---|---|---|---|
| 1. 패닉 시작 | panic() | panic_cpu 설정, 콘솔 flush | - |
| 2. NMI 브로드캐스트 | nmi_shootdown_cpus() | 모든 다른 CPU에 NMI 전송 | - |
| 3. CPU 정지 | crash_nmi_callback() | 레지스터 저장, halt 진입 | 1초 |
| 4. kexec 실행 | crash_kexec() | kdump 커널로 전환 | - |
| 5. 덤프 수집 | kdump 커널 | /proc/vmcore → 파일 시스템 | 설정에 따름 |
NMI와 ftrace 상호작용
ftrace의 ring_buffer는 NMI 컨텍스트에서도 이벤트를 안전하게 기록할 수 있도록 설계되었습니다. NMI에서 function tracer가 재진입되는 것을 방지하면서도, perf 이벤트 등은 NMI에서 ring_buffer에 쓸 수 있어야 합니다.
/* kernel/trace/ring_buffer.c - NMI 안전 공간 예약 */
static struct ring_buffer_event *
rb_reserve_next_event(struct trace_buffer *buffer,
struct ring_buffer_per_cpu *cpu_buffer,
unsigned long length)
{
struct ring_buffer_event *event;
u64 ts;
/* NMI 컨텍스트: 이미 reserve 중이면 중첩 → 포기 */
if (unlikely(in_nmi())) {
if (atomic_read(&cpu_buffer->record_disabled))
return NULL;
if (atomic_inc_return(&cpu_buffer->nest) > 1) {
atomic_dec(&cpu_buffer->nest);
return NULL; /* 중첩 reserve 방지 */
}
}
/* cmpxchg 기반 lockless 공간 예약 */
event = __rb_reserve_next(cpu_buffer, length);
return event;
}
/* perf의 NMI ring_buffer 쓰기 */
void perf_output_sample(struct perf_output_handle *handle,
struct perf_event_header *header,
struct perf_sample_data *data)
{
/* NMI-safe: Per-CPU ring_buffer에 lockless 쓰기 */
perf_output_put(handle, *header);
/* ... sample data ... */
}
| 컨텍스트 | ring_buffer 예약 방식 | 보호 메커니즘 |
|---|---|---|
| 일반 (process) | local_irq_disable + reserve | 인터럽트 비활성 |
| IRQ (hardirq) | 이미 비활성 + reserve | 선점 불가 |
| NMI | cmpxchg 기반 lockless | atomic nest 카운터 |
| NMI 중첩 | reserve 실패 (이벤트 유실) | nest > 1이면 포기 |
ftrace_nmi_enter()는 Per-CPU 변수로 이를 감지하고, NMI 동안은 function tracing을 일시 중단합니다.
irq_work 메커니즘
irq_work는 NMI 컨텍스트에서 안전하게 수행할 수 없는 작업을 IRQ 컨텍스트로 위임하는 핵심 메커니즘입니다. NMI 핸들러에서 irq_work_queue()를 호출하면, lockless linked list(llist)에 작업을 추가하고, self-IPI를 통해 IRQ 핸들러에서 콜백이 실행됩니다.
/* kernel/irq_work.c */
struct irq_work {
struct __call_single_node node;
void (*func)(struct irq_work *);
atomic_t flags; /* IRQ_WORK_PENDING, IRQ_WORK_BUSY, etc. */
};
/* NMI-safe 큐 추가 */
bool irq_work_queue(struct irq_work *work)
{
/* 이미 pending이면 중복 큐잉 방지 */
if (!irq_work_claim(work))
return false;
/* Per-CPU llist에 lockless 추가 */
__irq_work_queue_local(work);
/* self-IPI로 IRQ 컨텍스트에서 처리 예약 */
if (!(work->flags & IRQ_WORK_LAZY))
arch_irq_work_raise(); /* APIC self-IPI 전송 */
return true;
}
/* IRQ 컨텍스트에서 실행 */
void irq_work_run(void)
{
struct llist_node *llnode;
llnode = llist_del_all(this_cpu_ptr(&raised_list));
struct irq_work *work;
llist_for_each_entry_safe(work, llnode) {
work->func(work);
atomic_and(~IRQ_WORK_BUSY, &work->flags);
}
}
irq_work 사용 패턴
| 사용처 | NMI에서 하는 일 | irq_work에서 하는 일 |
|---|---|---|
| printk NMI-safe | seq_buf에 기록 | seq_buf → main log_buf flush |
| perf wakeup | ring_buffer에 샘플 기록 | task wakeup (schedule) |
| watchdog | hardlockup 감지 | panic() 또는 경고 출력 |
| RCU callback | grace period 체크 | RCU callback 처리 |
| trace output | trace 이벤트 기록 | trace 출력 flush |
IRQ_WORK_LAZY 플래그를 설정하면 self-IPI를 보내지 않고, 다음 timer tick에서 실행됩니다. NMI 오버헤드를 줄이고 싶을 때 유용하지만, 실행 지연이 발생합니다.
PMU NMI 내부 메커니즘
Performance Monitoring Unit(PMU)는 NMI의 가장 빈번한 소스입니다. 하드웨어 성능 카운터가 오버플로하면 LAPIC의 LVT Performance Counter 레지스터를 통해 NMI가 발생하며, perf_event 서브시스템이 샘플을 기록합니다.
Intel vs AMD PMU NMI 차이점
| 항목 | Intel | AMD |
|---|---|---|
| 카운터 수 (범용) | 8개 (Skylake+), 4개 (이전) | 6개 (Zen3+), 4개 (이전) |
| 정밀 샘플링 | PEBS (v1~v5) | IBS (Instruction/Op) |
| NMI 전달 | LVT Performance Counter | LVT Performance Counter |
| 카운터 MSR | IA32_PMCx (0x0C1+) | MSR_F15H_PERF_CTR (0xC0010200+) |
| 오버플로 감지 | IA32_PERF_GLOBAL_STATUS | 개별 카운터 MSB 확인 |
| PEBS/IBS DS | DS(Debug Store) 영역 | IbsFetchCtl/IbsOpCtl MSR |
| Large PEBS | 지원 (GPRs, XMM 등 자동 기록) | 해당 없음 |
| 핸들러 함수 | intel_pmu_handle_irq() | amd_pmu_handle_irq() |
/* arch/x86/events/intel/core.c */
static int intel_pmu_handle_irq(struct pt_regs *regs)
{
struct cpu_hw_events *cpuc = this_cpu_ptr(&cpu_hw_events);
int handled = 0;
u64 status;
/* PEBS 드레인 (정밀 샘플) */
if (x86_pmu.drain_pebs)
x86_pmu.drain_pebs(regs, &data);
/* GLOBAL_STATUS에서 오버플로 비트 확인 */
status = intel_pmu_get_status();
if (!status)
return 0; /* 이 NMI는 PMU가 아님 */
/* 오버플로된 각 카운터 처리 */
while (status) {
int idx = __ffs(status);
struct perf_event *event = cpuc->events[idx];
if (event) {
perf_event_overflow(event, &data, regs);
handled++;
}
status &= ~(1ULL << idx);
}
/* 카운터 재프로그래밍 (다음 NMI를 위해) */
intel_pmu_enable_all(0);
return handled;
}
perf_event_max_sample_rate를 통해 자동으로 샘플 주기를 조절합니다. 기본값은 100,000 samples/sec이며, NMI 오버헤드가 1%를 초과하면 자동 감소합니다.
NMI 처리의 역사적 진화
Linux의 NMI 처리 메커니즘은 20년 이상에 걸쳐 단순한 핸들러에서 복잡한 트램폴린 시스템으로 진화했습니다. 각 세대는 이전 세대의 근본적 문제를 해결하면서 새로운 복잡성을 도입했습니다.
| 세대 | 시기 | 스택 | 중첩 보호 | 복귀 명령 | 커널 코드 복잡도 |
|---|---|---|---|---|---|
| 1세대 (i386) | ~2003 | 커널 스택 | 없음 | IRET | 낮음 |
| 2세대 (IST) | 2004 | IST#2 | 없음 (하드웨어 의존) | IRET | 중간 |
| 3세대 (트램폴린) | 2012 | IST#2 + 복사 | 소프트웨어 (nmi_executing) | IRET | 매우 높음 |
| 4세대 (FRED) | 2024+ | 스택 레벨 0 | 하드웨어 자동 | ERETS | 낮음 |
CONFIG_X86_FRED로 활성화합니다. FRED가 활성화된 시스템에서는 NMI 관련 어셈블리 코드가 대폭 간소화됩니다.
FRED 아키텍처와 NMI
FRED(Flexible Return and Event Delivery)는 Intel이 도입한 새로운 이벤트 전달 메커니즘으로, 기존 IDT 기반 인터럽트/예외 처리의 근본적 문제를 해결합니다. 특히 NMI 처리에서 IRET 취약 구간 문제를 하드웨어 레벨에서 제거합니다.
FRED 핵심 개념
/* arch/x86/entry/entry_fred.c */
/*
* FRED 스택 레벨 (Stack Level):
* Level 0: NMI, Machine Check, Double Fault (최고 우선순위)
* Level 1: 일반 인터럽트 (#DB 등)
* Level 2: 예약
* Level 3: 시스템 콜, 일반 예외
*/
/* FRED NMI 핸들러 - IST 트램폴린 불필요! */
DEFINE_FRED_HANDLER(exc_nmi)
{
/* FRED가 자동으로:
* 1) 스택 레벨 0으로 전환 (NMI 전용)
* 2) NMI blocking 유지 (ERETS까지)
* 3) 확장 IRET 프레임 저장 (이벤트 유형 포함)
*/
nmi_enter();
default_do_nmi(regs);
nmi_exit();
/* ERETS로 복귀:
* - NMI blocking 해제가 원자적
* - IRET 취약 구간 없음
*/
}
/* FRED 확장 IRET 프레임 */
struct fred_frame {
/* 기존 IRET 프레임 */
u64 ip, cs, flags, sp, ss;
/* FRED 추가 필드 */
u64 event_type; /* NMI=3, 외부인터럽트=0, ... */
u64 event_data; /* 벡터 번호, 에러 코드 등 */
u64 entrypoint; /* 이벤트 핸들러 진입점 */
};
| FRED 특성 | NMI에 미치는 영향 |
|---|---|
| 스택 레벨 0 전용 | NMI는 항상 독립된 스택에서 실행 → 중첩 시 스택 충돌 없음 |
| 자동 NMI blocking | ERETS 완료까지 NMI blocking 유지 → 소프트웨어 추적 불필요 |
| ERETU/ERETS 명령 | IRET 대체 → 비원자적 복귀 문제 해결 |
| 이벤트 유형 필드 | NMI vs 예외 vs 인터럽트를 하드웨어가 구분 → 소프트웨어 판별 불필요 |
| IST 불필요 | TSS의 IST 설정 불필요, IST 관련 코드 모두 제거 가능 |
ERETS(Event Return to Supervisor)는 커널 모드로의 복귀에 사용되며, ERETU(Event Return to User)는 사용자 모드로의 복귀에 사용됩니다. 두 명령어 모두 IRET과 달리 이벤트 유형에 따른 blocking 해제를 원자적으로 수행합니다. NMI의 경우, ERETS가 완료되기 전까지 NMI blocking이 유지되므로 IRET 취약 구간이 존재하지 않습니다.
NMI 관련 커널 설정 총정리
NMI 관련 커널 기능은 다수의 CONFIG 옵션에 의해 제어됩니다. 이 섹션에서는 NMI와 관련된 모든 Kconfig 옵션의 의존성 관계와 운영 환경별 권장 설정을 정리합니다.
CONFIG 옵션 전체 맵
| CONFIG 옵션 | 설명 | 의존성 | 기본값 |
|---|---|---|---|
HARDLOCKUP_DETECTOR | NMI 기반 hardlockup 감지 활성화 | LOCKUP_DETECTOR | y (대부분 배포판) |
HARDLOCKUP_DETECTOR_PERF | PMU NMI 기반 hardlockup 감지 | PERF_EVENTS + HAVE_HARDLOCKUP_DETECTOR_PERF | y (x86) |
HARDLOCKUP_DETECTOR_BUDDY | CPU buddy 기반 (PMU 없는 환경) | SMP | n |
PERF_EVENTS | perf 프레임워크 (PMU NMI 발생) | HAVE_PERF_EVENTS | y |
X86_LOCAL_APIC | Local APIC (NMI 전달) | X86 | y |
LOCKUP_DETECTOR | soft/hard lockup 감지 프레임워크 | - | y |
SOFTLOCKUP_DETECTOR | softlockup watchdog (NMI 아님) | LOCKUP_DETECTOR | y |
BOOTPARAM_HARDLOCKUP_PANIC | hardlockup 시 자동 패닉 | HARDLOCKUP_DETECTOR | n |
X86_FRED | FRED NMI 처리 (새 하드웨어) | X86_64, CPU_SUP_INTEL | y (지원 시) |
ARM64_PSEUDO_NMI | ARM64 pseudo-NMI 지원 | ARM64 + GICv3 | y |
KGDB_NMI | NMI로 KGDB 디버거 진입 | KGDB + SERIAL_CORE_CONSOLE | n |
DETECT_HUNG_TASK | hung task 감지 (NMI 연관) | - | y |
운영 환경별 권장 Kconfig 설정
| 옵션 | 프로덕션 서버 | 개발/디버깅 | 실시간(RT) | 가상머신 |
|---|---|---|---|---|
HARDLOCKUP_DETECTOR | y | y | n (레이턴시) | y |
HARDLOCKUP_DETECTOR_PERF | y | y | n | n (vPMU 없으면) |
HARDLOCKUP_DETECTOR_BUDDY | n | n | y (대안) | y (대안) |
BOOTPARAM_HARDLOCKUP_PANIC | y (+ kdump) | n | n | n |
KGDB_NMI | n | y | n | n |
X86_FRED | y (지원 시) | y | y | n (패스스루 필요) |
PERF_EVENTS | y | y | 선택적 | y |
주요 부팅 파라미터
# NMI 관련 부팅 파라미터 (커널 커맨드라인)
nmi_watchdog=0|1 # hardlockup watchdog 활성/비활성
unknown_nmi_panic=0|1 # 미식별 NMI 시 패닉 여부
hardlockup_all_cpu_backtrace=1 # hardlockup 시 모든 CPU 백트레이스
watchdog_thresh=10 # watchdog 임계값 (초, 기본 10)
watchdog_cpumask=0xff # watchdog 활성 CPU 마스크
# ARM64 pseudo-NMI
irqchip.gicv3_pseudo_nmi=1 # pseudo-NMI 활성화
# FRED (Intel)
fred=on|off # FRED 활성/비활성 (기본: on)
# kdump 연동
crashkernel=256M # kdump용 예약 메모리
panic=10 # 패닉 후 10초 뒤 재부팅
HARDLOCKUP_DETECTOR_PERF가 동작하지 않습니다. KVM에서 -cpu host,pmu=on으로 vPMU를 활성화하거나, HARDLOCKUP_DETECTOR_BUDDY를 대안으로 사용하세요. buddy 감지기는 인접 CPU가 NMI 대신 hrtimer를 사용하여 교차 감시합니다.
nmi_watchdog=0으로 비활성화하고, 대신 외부 하드웨어 watchdog이나 buddy 감지기를 사용하는 것이 권장됩니다.
참고자료
공식 커널 문서
- Softlockup and Hardlockup Detectors — Linux Kernel Documentation — NMI watchdog, softlockup/hardlockup 감지기의 공식 관리자 가이드입니다.
- Kernel Parameters — Linux Kernel Documentation —
nmi_watchdog=,hardlockup_all_cpu_backtrace=등 NMI 관련 부트 파라미터를 확인할 수 있습니다. - NMI Backtrace — Linux Kernel Documentation —
trigger_all_cpu_backtrace()등 NMI 기반 전체 CPU 백트레이스 API를 설명합니다. - x86 NMI — Linux Kernel Documentation — x86 아키텍처 NMI 처리 구조에 대한 공식 문서입니다.
- Ftrace — Linux Kernel Documentation —
function_graph트레이서를 이용한 NMI 핸들러 진입·탈출 추적 방법을 포함합니다. - Perf Events and Tool Security — Linux Kernel Documentation — perf NMI 이벤트의 보안 모델과
perf_event_paranoid설정을 다룹니다.
LWN.net 기사
- Detecting kernel stack overflows (LWN, 2015) — NMI 핸들러에서의 스택 오버플로우 감지와 별도 NMI 스택의 필요성을 설명합니다.
- Reinventing the watchdog (LWN, 2015) — hardlockup 감지기의 perf NMI 기반 구현에서 buddy 방식으로의 전환 논의를 다룹니다.
- Lockup detection with the buddy watchdog (LWN, 2016) — 인접 CPU 기반 buddy hardlockup 감지기의 설계와 장단점을 비교합니다.
- NMI-safe printk and console handling (LWN, 2017) — NMI 컨텍스트에서 안전한 printk 출력과 콘솔 잠금 문제를 다룹니다.
- Safely debugging with NMI (LWN, 2018) — NMI를 활용한 커널 디버깅 기법과 NMI 재진입 안전성 문제를 분석합니다.
- Printk and NMI: getting the messages out (LWN, 2022) — printk 서브시스템의 NMI-safe 재설계와 threaded printk의 관계를 설명합니다.
커널 소스 경로
| 파일 | 역할 |
|---|---|
arch/x86/kernel/nmi.c |
x86 NMI 핸들러 메인 — default_do_nmi(), NMI 타입 분류, 재진입 처리 |
arch/x86/entry/entry_64.S |
x86_64 NMI 진입점 — asm_exc_nmi, IST 스택 전환, IRET 프레임 보호 |
kernel/watchdog.c |
softlockup/hardlockup 감지기 공통 코드 — watchdog_overflow_callback() |
kernel/watchdog_hld.c |
perf NMI 기반 hardlockup 감지기 — hardlockup_detector_perf_init() |
kernel/watchdog_buddy.c |
buddy hardlockup 감지기 — 인접 CPU hrtimer 기반 교차 감시 |
include/linux/nmi.h |
NMI watchdog API 헤더 — touch_nmi_watchdog(), trigger_all_cpu_backtrace() |
arch/x86/include/asm/nmi.h |
x86 NMI 타입 정의 — NMI_LOCAL, NMI_UNKNOWN, NMI_IO_CHECK |
arch/x86/kernel/cpu/perfctr-watchdog.c |
PMC(성능 카운터) 기반 NMI watchdog 구현 |
lib/nmi_backtrace.c |
NMI 기반 전체 CPU 백트레이스 구현 — sysrq-l 연동 |
kernel/printk/printk_safe.c |
NMI-safe printk 구현 — 재진입 방지 버퍼링 |
하드웨어 사양 문서
- Intel 64 and IA-32 Architectures Software Developer's Manual — Volume 3A, Chapter 6 "Interrupt and Exception Handling" 섹션에서 NMI 벡터(Vector 2), NMI 블로킹/언블로킹 규칙, IST(Interrupt Stack Table) 메커니즘을 상세히 설명합니다.
- AMD64 Architecture Programmer's Manual — Volume 2, Chapter 8 "Exceptions and Interrupts"에서 AMD 플랫폼의 NMI 처리 사양을 확인할 수 있습니다.
- ACPI Specification — FADT(Fixed ACPI Description Table)의 NMI 소스 플래그와 SCI(System Control Interrupt) 설정을 포함합니다.
기술 블로그 및 발표 자료
- Don Zickus — NMI Watchdog: Past, Present, and Future (OLS 2009) — NMI watchdog의 역사적 발전 과정과 perf 기반 감지기로의 전환을 발표한 자료입니다.
- Brendan Gregg — perf Examples —
perf record -g의 NMI 기반 샘플링과 PMU 오버플로우 인터럽트 활용 사례를 포함합니다. - Terence Li — NMI in x86: Entry, Reentrance, and IST — x86 NMI 재진입 문제와 IRET unblocking race를 상세히 분석합니다.
- Cloudflare — Hardlockup Debugging in Production — 실제 프로덕션 환경에서의 hardlockup 발생 사례와 NMI watchdog을 통한 진단 과정을 설명합니다.
관련 문서
- 인터럽트 처리 -- IDT, APIC, 인터럽트 벡터, 인터럽트 핸들링 전체 구조
- SoftIRQ/HardIRQ -- 하드웨어 인터럽트(top-half)와 소프트 인터럽트(bottom-half) 처리 구조
- IPI (Inter-Processor Interrupt) -- CPU 간 인터럽트 전달, SMP 동기화
- Kdump & Crash -- 커널 패닉 시 crash dump 생성 및 분석
- 크래시 분석 -- vmcore, call trace, 레지스터 상태 기반 원인 추적 절차
- 디버깅 & 트러블슈팅 -- hung task, lockup, 재현 실험, 로그 수집 표준 절차
- Watchdog -- soft/hard watchdog 동작 차이, 운영 임계값 정책 수립
- 타이머 -- hrtimer 기반 watchdog 타이머의 구현 기반
- ktime/Clock -- NMI-safe 시간 측정 API
- MCE (Machine Check Exception) -- MCA 뱅크, Monarch 프로토콜, EDAC, HWPoison, NMI 연동