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시간 장애 대응 플레이북, 장애 보고서 템플릿까지 전 영역을 상세히 다룹니다.

전제 조건: 인터럽트 처리SoftIRQ/HardIRQ 문서를 먼저 읽으세요. NMI는 일반 인터럽트 메커니즘 위에 동작하는 특수 계층이므로, IDT, APIC, 인터럽트 컨텍스트에 대한 기본 이해가 필요합니다.
일상 비유: NMI는 비상 경보 시스템과 비슷합니다. 일반 알림(일반 인터럽트)은 '방해 금지 모드'로 끌 수 있지만, 화재 경보(NMI)는 어떤 설정으로도 무시할 수 없는 최우선 알림입니다.

핵심 요약

  • 마스킹 불가 -- cli/local_irq_disable()로 비활성화할 수 없음
  • IDT 벡터 2번 -- x86에서 NMI는 고정 벡터로 항상 전달
  • 주요 소스 -- PMU 오버플로, 하드웨어 오류, watchdog, 외부 NMI 버튼
  • Hardlockup 감지 -- hrtimer 기반 카운터를 NMI에서 확인하여 교착 탐지
  • 엄격한 제약 -- spinlock, 메모리 할당, 일반 panic() 사용 불가

단계별 이해

  1. NMI 개념 파악
    일반 인터럽트와 NMI의 차이점, 마스킹 불가 특성을 먼저 이해합니다.
  2. x86 하드웨어 경로 추적
    LAPIC에서 IDT 벡터 2번을 거쳐 exc_nmi()에 도달하는 전달 경로를 파악합니다.
  3. 핸들러 등록 구조 이해
    register_nmi_handler()와 NMI 타입(LOCAL, UNKNOWN, SERR, IO_CHECK)을 학습합니다.
  4. Watchdog 메커니즘 이해
    hrtimer + PMU NMI 조합으로 hardlockup을 감지하는 원리를 파악합니다.
  5. NMI-safe 프로그래밍 패턴 익히기
    trylock, per-CPU 변수, atomic 연산 등 NMI 컨텍스트에서 허용되는 패턴을 숙지합니다.
관련 표준: Intel SDM Vol.3 Ch.6 (Interrupt and Exception Handling), AMD64 Architecture Programmer's Manual Vol.2 Ch.8 (Exceptions and Interrupts), ACPI Specification (GHES, APEI) -- NMI 전달 및 하드웨어 에러 보고와 관련된 규격입니다. 종합 목록은 참고자료 -- 표준 & 규격 섹션을 참고하세요.

NMI vs 일반 인터럽트 비교

NMI와 일반 인터럽트(maskable interrupt)는 CPU에 도달하는 방식, 마스킹 가능 여부, 사용하는 스택, 핸들러 제약사항 등 거의 모든 측면에서 다릅니다. 아래 다이어그램과 비교 테이블에서 그 차이를 명확하게 확인할 수 있습니다.

NMI vs 일반 인터럽트 처리 흐름 비교 일반 인터럽트 (Maskable) I/O APIC / Local APIC (벡터 32~255) EFLAGS.IF == 1 ? IF=0: 차단 인터럽트 보류 (Pending) IF=1 IDT 벡터 N 진입 커널 스택 (현재 태스크) do_IRQ() / handle_irq() NMI (Non-Maskable) LAPIC NMI pin / LINT1 (벡터 2) EFLAGS.IF 무시 (항상 전달) IDT 벡터 #2 (고정) IST #2 전용 스택 (Per-CPU) exc_nmi() / default_do_nmi() 일반 인터럽트는 cli/IF=0으로 차단 가능하지만, NMI는 어떤 상태에서도 무조건 전달
NMI와 일반 인터럽트의 전달 경로 비교: NMI는 EFLAGS.IF 플래그를 완전히 무시
구분일반 인터럽트 (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 sectionnmi_enter()에서 별도 보장 필요
panic 호출panic() 사용 가능nmi_panic()만 사용 가능
printk직접 사용 가능NMI-safe 버퍼 경유, 지연 출력
주요 사용처디바이스 I/O, 타이머, IPIwatchdog, PMU, 하드웨어 에러, 디버깅

NMI 발생 원인

NMI는 다양한 하드웨어 및 소프트웨어 소스에서 발생합니다. 각 소스는 커널에서 서로 다른 핸들러 체인으로 분류되어 처리됩니다.

소스NMI 타입설명커널 설정
하드웨어 오류NMI_SERR메모리 패리티 에러, PCI SERR# 시그널, 버스 에러BIOS/펌웨어 설정
I/O 채널 체크NMI_IO_CHECKISA/PCI 디바이스의 I/O 채널 에러BIOS/펌웨어 설정
Watchdog (hardlockup)NMI_LOCALPMU 카운터 오버플로로 발생. CPU가 인터럽트 비활성 상태에서 일정 시간 이상 멈추면 감지CONFIG_HARDLOCKUP_DETECTOR
Performance MonitorNMI_LOCALperf 프로파일링에서 PMU 카운터 오버플로 시 NMI로 샘플 수집CONFIG_PERF_EVENTS
외부 NMI 버튼NMI_UNKNOWN서버 하드웨어의 물리적 NMI 버튼 (디버깅 목적)--
IOAPIC WatchdogNMI_LOCALI/O APIC를 통한 NMI 전달 (레거시 시스템)nmi_watchdog=1
GHES (ACPI)NMI_LOCALACPI의 Generic Hardware Error Source를 통한 하드웨어 에러 보고CONFIG_ACPI_APEI_GHES
MCE (Machine Check)NMI_LOCAL심각한 하드웨어 오류 시 MCE를 NMI로 전달 (일부 아키텍처)CONFIG_X86_MCE
KGDB/KDBNMI_LOCAL커널 디버거 진입을 위한 NMI (SysRq + 디버거)CONFIG_KGDB
CPU Backtrace IPINMI_LOCALSysRq-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 처리 흐름 PMU Overflow HW Error (MCE) External NMI Local APIC NMI pin / LVT IDT Vector #2 asm_exc_nmi NMI# exc_nmi() default_do_nmi() nmi_handle() perf_event_nmi watchdog_nmi kgdb / panic NMI는 EFLAGS.IF=0 상태에서도 CPU에 전달됨 (마스킹 불가)
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에 매핑됩니다.

NMI Delivery Path: LAPIC LVT 상세 MCE / HW Error PMU Overflow IPI (NMI mode) External NMI Pin Local APIC (Per-CPU) LVT LINT0 (ExtINT) LVT LINT1 (NMI) LVT PMC (PMI) LVT Error LVT Thermal / CMCI / Timer ICR (IPI 전송용) CPU Core IDT[2] = asm_exc_nmi IST #2 스택으로 전환 NMI blocking 활성화 NMI# LVT LINT1 레지스터 구조 (32비트) Bit 16 Mask Bit 15 Trigger Bit 13 Polarity Bit 12 Status Bit 10:8 DelivMode=100(NMI) Bit 7:0 Vector (무시됨) LINT1의 Delivery Mode가 100b(NMI)로 설정되면 벡터 필드는 무시되고 IDT #2가 사용됨 PMU는 LVT PMC를 NMI 모드로 설정하여 perf/watchdog NMI를 발생시킴
LAPIC 내부의 LVT 구조와 NMI 전달 경로: LINT1과 LVT PMC가 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를 감지할 수 있는 유일한 메커니즘입니다.

Hardlockup Detection 메커니즘 hrtimer (Per-CPU) watchdog_timer_fn() hrtimer_interrupts++ 매 watchdog_thresh 초 PMU Counter NMI perf event overflow watchdog_overflow hrtimer_interrupts 변화 확인 카운터 변화? (비교) 정상 Yes HARDLOCKUP! panic / backtrace No hrtimer는 일반 인터럽트로 동작 -- hardlockup 시 카운터 증가 불가 -- NMI에서 감지
NMI watchdog의 hardlockup 탐지 원리: hrtimer(일반 인터럽트)가 멈추면 NMI에서 이를 감지

Softlockup vs Hardlockup 비교

커널의 lockup 감지 메커니즘은 두 계층으로 구성됩니다. softlockup은 커널 스레드가 긴 시간 동안 스케줄링되지 않는 상태를, hardlockup은 인터럽트 자체가 비활성화된 상태를 감지합니다.

구분SoftlockupHardlockup
감지 대상커널 스레드가 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_DETECTORCONFIG_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"
가상화 환경 주의: VM에서는 vCPU가 호스트에 의해 스케줄링되므로 NMI watchdog이 false positive를 발생시킬 수 있습니다. KVM, VMware, Hyper-V 등의 가상화 환경에서는 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_trylocktrylock으로만 시도, 실패 시 즉시 포기
메모리 할당사용 불가할당자 내부 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 함수 카테고리 안전 (NMI-Safe) this_cpu_read/write() atomic_read/set/inc() raw_spin_trylock() nmi_panic() smp_processor_id() rdtsc() / ktime_get() READ_ONCE/WRITE_ONCE perf_event_output() 조건부 (주의 필요) printk() [NMI 버퍼] rcu_read_lock() [nmi_enter] trace_*() [NMI-safe만] show_regs() [trylock] ring_buffer_write [NMI] copy_from_user [fault] 금지 (NMI-Unsafe) spin_lock() / mutex kmalloc() / vmalloc() schedule() / sleep panic() [use nmi_panic] down() / up() (semaphore) copy_to_user() wake_up_process() request_irq()
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 중첩 사이에는 미묘하지만 치명적인 상호작용이 존재합니다.

IST 스택 레이아웃 + NMI 중첩 방지 트램폴린 TSS (Per-CPU) IST[1]: #DF (Double Fault) IST[2]: NMI Stack IST[3]: #DB (Debug) IST[4]: #MC (Machine Check) 각 Per-CPU, 크기 4~8KB 중첩 문제 시나리오 1. NMI 진입 (IST2 스택 사용) 2. breakpoint / page fault 발생 3. IRET으로 복귀 (NMI 차단 해제!) 4. 새 NMI 도착 (IST2 재사용) 5. 이전 NMI 스택 데이터 파괴! Linux 해결: 트램폴린 1. NMI 진입 (IST2 스택) 2. 즉시 전용 NMI 스택으로 전환 3. IST2는 트램폴린으로만 사용 4. 중첩 NMI 감지 변수 설정 5. IRET 후 중첩 NMI 처리 NMI 스택 메모리 레이아웃 (Per-CPU) IST2 스택 (트램폴린) NMI 전용 실행 스택 exc_nmi() 실행 영역 진입 / RSP 저장 스택 전환 후 사용 핸들러 체인 호출 Per-CPU nmi_state: {on_nmi_stack, swapped_stack, nmi_cr2} -- 중첩 상태 추적 asm_exc_nmi 진입부에서 nmi_state 확인 후 중첩 NMI면 repeat_nmi로 분기 IST2는 "착륙장"일 뿐이고, 실제 NMI 처리는 전환된 전용 스택에서 수행됨
IST 스택 구조와 NMI 중첩 방지 트램폴린 메커니즘: IST2는 착륙장, 실제 처리는 전용 스택에서
/* 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 중첩이 실제로 발생하는 경우: NMI 핸들러 내에서 page fault가 발생하면 (예: 유저 공간 스택을 unwinding 하는 중), page fault 핸들러의 IRET이 NMI blocking을 해제합니다. 이 시점에서 pending NMI가 있으면 중첩이 발생합니다. Linux 커널의 트램폴린 메커니즘은 이 시나리오를 안전하게 처리합니다.

NMI 기반 CPU 디버깅

NMI는 응답하지 않는 CPU를 디버깅하는 가장 강력한 도구입니다. SysRq-l 또는 trigger_all_cpu_backtrace()를 통해 모든 CPU의 콜스택을 NMI로 수집할 수 있으며, 이는 deadlock이나 livelock 진단에 핵심적입니다.

NMI Backtrace: CPU 간 디버깅 흐름 SysRq-l (사용자 입력) trigger_all_cpu_backtrace() hardlockup 감지 CPU 0 (요청 CPU) nmi_trigger_cpumask_backtrace() apic->send_IPI(cpu, NMI_VECTOR) NMI IPI CPU 1 NMI 수신 CPU 2 NMI 수신 nmi_cpu_backtrace_handler() show_regs(regs) + dump_stack() dmesg 출력 (Per-CPU Backtrace) NMI backtrace for cpu 1: Call Trace: schedule+0x35/0x80 schedule_timeout+0x1c5/0x260 NMI backtrace for cpu 2: Call Trace: _raw_spin_lock+0x18/0x30 do_something+0x42/0x100 NMI IPI로 모든 CPU의 콜스택을 수집하여 deadlock/livelock 원인을 진단
NMI backtrace 메커니즘: NMI IPI를 통해 모든 CPU의 콜스택을 원격 수집
/* 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
서버 환경에서의 NMI: 서버 BIOS/BMC에서 NMI 버튼 또는 IPMI 명령(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 사용 */
NMI vs. Timer 프로파일링: 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를 사용하여 즉각적인 알림이 이루어집니다.

GHES/APEI NMI 에러 보고 흐름 메모리 에러 (CE/UE) PCIe AER 에러 프로세서 에러 UEFI Firmware / SMM 에러 감지 + 기록 HEST (Error Source Table) NMI 시그널 생성 ACPI HEST (Hardware Error Source Table) Type: GHES (Generic HW Error Source) Notification: NMI / SCI / GPIO / SEA GHES 드라이버 (NMI) ghes_notify_nmi() ghes_read_estatus() NMI CE: 로깅 + 계속 UE: 페이지 오프라인 Fatal: panic MCE 에스컬레이션 GHES Error Status Block (공유 메모리) | Block Status | Raw Data Offset | Raw Data Length | Error Severity | Generic Error Data Entry[] | | Section Type (GUID) | Error Severity | Data Length | FRU Text | Section Data (memory/pcie/proc) | 읽기 펌웨어가 공유 메모리에 에러를 기록하고 NMI로 커널에 알림 -- 커널은 NMI 핸들러에서 읽음
GHES/APEI 에러 보고: 펌웨어가 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");
GHES NMI 제약사항: GHES NMI 핸들러는 공유 메모리에서 에러 데이터를 읽는데, 이때 ioremap 대신 fixmap을 사용합니다. NMI 컨텍스트에서는 페이지 테이블 조작이 불가능하므로, 부팅 시 미리 매핑해둔 fixmap 슬롯을 활용합니다. 복구 가능한 에러는 NMI-safe 풀에 저장 후 워크큐에서 나중에 처리합니다.

NMI 핸들러 등록 전략

NMI 핸들러를 등록할 때는 type(LOCAL/UNKNOWN/SERR/IO_CHECK), flags, 반환값(NMI_HANDLED/NMI_DONE)의 조합이 동작을 좌우합니다. 핸들러가 체인 구조로 동작하기 때문에, 잘못된 반환값은 다른 핸들러 실행을 막거나 의미 없는 Unknown NMI 경고를 유발할 수 있습니다.

요소선택지실무 권장
typeNMI_LOCAL, NMI_UNKNOWN, NMI_SERR, NMI_IO_CHECK대부분 NMI_LOCAL 사용, 원인 미상 분석 용도로 NMI_UNKNOWN 보조 등록
flagsNMI_FLAG_FIRST, NMI_FLAG_LASTwatchdog/perf보다 우선이 필요하면 FIRST, 로깅 전용이면 LAST
반환값NMI_HANDLED, NMI_DONE명확한 소스 확인 시 HANDLED, 불확실하면 DONE으로 체인 유지
해제unregister_nmi_handler()모듈 언로드 경로에서 반드시 해제, 실패 시 다음 로드에서 중복 등록
진단/proc/interrupts, tracepoint, dmesg배포 전 NMI 카운터 증가 패턴과 로그 폭주 여부를 함께 확인
NMI 핸들러 체인과 반환값 분기 exc_nmi() nmi_handle(type) handler A NMI_FLAG_FIRST handler B 기본 체인 handler C NMI_FLAG_LAST 반환값 == NMI_HANDLED 현재 체인 처리 종료, unknown 경로 진입 차단 모두 NMI_DONE unknown_nmi_error()로 이동 가능 핸들러는 원인을 확실히 소비했을 때만 NMI_HANDLED 반환
반환값 설계가 NMI 체인 전체 동작과 Unknown 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 최소 경로와 지연 처리 파이프라인 PMU/GHES/외부 NMI 인터럽트 발생 NMI 핸들러 타임스탬프 + 레지스터 최소 저장 Per-CPU lockless ring buffer WRITE_ONCE(head), 샘플 적재 softirq/workqueue 소비자 상세 해석, 문자열 변환, 로그 출력 사용자 공간 관측 tracefs/perf/ringbuffer 읽기 NMI 경로에서 금지 또는 최소화 - kmalloc/vmalloc, mutex, schedule, copy_to_user - printk 폭주, 복잡한 포맷팅, 긴 루프 - 허용: this_cpu, atomic, trylock, READ_ONCE/WRITE_ONCE - 목표: 고정 시간 처리(상한) 유지 - 실패 시 샘플 드롭 카운터만 증가 핵심: NMI는 빠르게 기록하고 빠져나온 뒤, 나머지는 지연 경로에서 처리
NMI 경로 최소화 패턴: lockless 기록과 지연 소비를 분리하면 안정성과 관측성을 함께 확보할 수 있습니다.
/* 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
NMI 핸들러 내부 panic 주의: NMI 컨텍스트에서 일반 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, 반환값, 지연 처리 경계, 로그 폭주 제어를 먼저 점검하면 운영 장애 가능성을 크게 줄일 수 있습니다.

NMI 코드 리뷰 우선순위 금지 API 확인 sleep/alloc/lock 반환값/체인 규칙 HANDLED vs DONE 지연 처리 경계 NMI 최소화 관측/복구 정책 rate limit + kdump 실패 패턴 사전 차단 - NMI에서 mutex/kmalloc 호출: 즉시 수정 - NMI_DONE 남발로 unknown NMI 로그 폭주: 반환 조건 재정의 - NMI에서 문자열 포맷/복잡한 순회: 지연 워커로 이전 - hardlockup_panic 정책과 kdump/재기동 정책 일치 여부 검증 리뷰는 기능 확인보다 "NMI 컨텍스트 위반" 제거를 우선
NMI 코드 리뷰는 금지 API 제거와 체인 반환값 검증을 최우선으로 진행합니다.
체크 항목질문권장 기준
컨텍스트 안전성핸들러 내부에 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 패치 리뷰에서는 정상 경로보다 실패 경로를 먼저 읽으세요. 특히 "핸들러가 어떤 조건에서 NMI_HANDLED를 반환하는지", "NMI에서 무엇을 하지 않도록 설계했는지"를 확인하면 결함을 빠르게 걸러낼 수 있습니다.

NMI 장애 대응 플레이북

운영 중 NMI 경고가 발생하면 원인 파악 전에 재부팅만 수행하는 경우가 많습니다. 하지만 Unknown NMI와 GHES는 하드웨어 결함 신호일 수 있으므로, 초기 1시간 안에 로그/카운터/덤프를 구조적으로 수집해야 재발 방지까지 연결됩니다.

NMI 장애 대응 의사결정 흐름 NMI 이벤트 감지 hardlockup / unknown / GHES 분류: 재현성/확산성/치명도 단발 / 반복 / 다수 노드 초기 대응 정책 격리/유지/즉시 패닉 10분: 증거 보존 dmesg/interrupts/perf/sysctl 30분: 범위 축소 노드/커널/펌웨어 공통점 1시간: 조치 확정 커널/BIOS/하드웨어 액션 출력물(Incident Artifact) - NMI 로그 번들, /proc/interrupts 추이, perf 샘플, vmcore, EDAC/GHES 카운터 - 영향 범위 보고서(노드/커널 버전/펌웨어 버전), 재발 방지 액션 목록 초기 1시간 수집 품질이 재발 분석 속도를 결정
NMI 장애 대응은 10분 증거 보존, 30분 범위 축소, 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
Unknown NMI 우선 기준: 다수 노드에서 같은 시점에 발생하면 전원/펌웨어/플랫폼 이벤트 가능성이 높습니다. 단일 노드 반복이면 해당 노드의 BIOS 설정, BMC 이벤트 로그, 디바이스 상태를 먼저 점검하세요.
GHES 우선 기준: 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) 처리 구조
참고 문서:
권장 학습 순서: 인터럽트 처리SoftIRQ/HardIRQWatchdog크래시 분석디버깅 & 트러블슈팅 순서로 보면 NMI 장애 대응 흐름을 빠르게 연결할 수 있습니다.