NMI (Non-Maskable Interrupt) 심화
NMI는 CPU의 마스킹 메커니즘(cli/local_irq_disable())으로
비활성화할 수 없는 특수한 인터럽트입니다. 일반 인터럽트가 비활성 상태에서도 CPU에 전달되며,
시스템의 최후 방어선 역할을 합니다. 하드웨어 오류 감지, 커널 교착상태(hardlockup) 탐지,
성능 프로파일링, 디버거 진입 등 크리티컬한 용도에 사용됩니다.
본 문서에서는 NMI 발생 원인, x86 NMI 아키텍처, 핸들러 등록/처리 흐름,
hardlockup watchdog 메커니즘, NMI 컨텍스트 제약사항, IST 스택과 중첩 문제,
Unknown NMI 처리, NMI 기반 perf 프로파일링, CPU 디버깅, GHES/APEI 에러 보고,
핸들러 등록 전략, 지연 처리 설계, 운영 튜닝 체크리스트, 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()로 차단 가능 | 마스킹 불가 (하드웨어적 강제 전달) |
| 스택 | 현재 태스크의 커널 스택 | IST #2 전용 스택 (Per-CPU) |
| 중첩 | 가능 (우선순위에 따라) | CPU가 하드웨어적으로 차단 (IRET까지) |
| 핸들러 제약 | spinlock, kmalloc 등 사용 가능 | trylock만 가능, 메모리 할당 금지 |
| RCU | 자동으로 read-side critical section | nmi_enter()에서 별도 보장 필요 |
| panic 호출 | panic() 사용 가능 | nmi_panic()만 사용 가능 |
| printk | 직접 사용 가능 | NMI-safe 버퍼 경유, 지연 출력 |
| 주요 사용처 | 디바이스 I/O, 타이머, IPI | watchdog, PMU, 하드웨어 에러, 디버깅 |
NMI 발생 원인
NMI는 다양한 하드웨어 및 소프트웨어 소스에서 발생합니다. 각 소스는 커널에서 서로 다른 핸들러 체인으로 분류되어 처리됩니다.
| 소스 | NMI 타입 | 설명 | 커널 설정 |
|---|---|---|---|
| 하드웨어 오류 | NMI_SERR | 메모리 패리티 에러, PCI SERR# 시그널, 버스 에러 | BIOS/펌웨어 설정 |
| 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에 매핑됩니다.
/* 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 구조체로 등록되며, 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는 전달되므로, 교착 상태에 빠진 CPU를 감지할 수 있는 유일한 메커니즘입니다.
Softlockup vs Hardlockup 비교
커널의 lockup 감지 메커니즘은 두 계층으로 구성됩니다. softlockup은 커널 스레드가 긴 시간 동안 스케줄링되지 않는 상태를, hardlockup은 인터럽트 자체가 비활성화된 상태를 감지합니다.
| 구분 | Softlockup | Hardlockup |
|---|---|---|
| 감지 대상 | 커널 스레드가 watchdog_thresh초 이상 스케줄링 안 됨 | 인터럽트가 watchdog_thresh초 이상 비활성 상태 |
| 감지 메커니즘 | Per-CPU watchdog 커널 스레드 (watchdog/N) | PMU NMI 기반 NMI watchdog |
| 타이머 | hrtimer가 watchdog 스레드의 타임스탬프 확인 | 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를 선점하면 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)를 사용하여 전용 스택에서 실행됩니다. 이는 스택 오버플로 상태에서도 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
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 기반은 인터럽트가 비활성화된 크리티컬 섹션 내부까지 프로파일링할 수 있습니다.
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 컨텍스트에서는 페이지 테이블 조작이 불가능하므로, 부팅 시 미리 매핑해둔 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 경합과 지연 시간이 급증합니다. 실무에서는 NMI에서 최소 정보만 기록하고, 나머지는 IRQ/workqueue/thread로 넘기는 2단계 파이프라인을 사용합니다. 핵심은 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 빠른 격리 균형 |
| 관측 파이프라인 | 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: 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 코드 리뷰 체크리스트
NMI 코드 품질은 기능보다 실패 시나리오로 평가해야 합니다. 아래 체크리스트는 커널 모듈/플랫폼 코드 리뷰 시 자주 사용하는 항목입니다. 특히 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시간 안에 로그/카운터/덤프를 구조적으로 수집해야 재발 방지까지 연결됩니다.
| 시간대 | 핵심 목표 | 필수 액션 |
|---|---|---|
| 0~10분 | 증거 보존 | dmesg 고정 저장, /proc/interrupts 스냅샷, sysctl 값 수집, 가능하면 SysRq-l 수행 |
| 10~30분 | 원인 후보 축소 | Unknown NMI vs GHES 분리, 반복 주기 확인, 동일 커널/BIOS/하드웨어군 상관 분석 |
| 30~60분 | 운영 조치 확정 | panic 정책 조정, 문제 노드 격리, 펌웨어/메모리 교체 또는 커널 롤백 여부 결정 |
# [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 수집 연동
관련 문서
- 인터럽트 처리 -- 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