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

전제 조건: 인터럽트 처리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()로 차단 가능마스킹 불가 (하드웨어적 강제 전달)
스택현재 태스크(Task)의 커널 스택IST #2 전용 스택 (Per-CPU)
중첩가능 (우선순위(Priority)에 따라)CPU가 하드웨어적으로 차단 (IRET까지)
핸들러 제약spinlock, kmalloc 등 사용 가능trylock만 가능, 메모리 할당 금지
RCU자동으로 read-side critical sectionnmi_enter()에서 별도 보장 필요
panic 호출panic() 사용 가능nmi_panic()만 사용 가능
printk직접 사용 가능NMI-safe 버퍼(Buffer) 경유, 지연 출력
주요 사용처디바이스 I/O, 타이머(Timer), IPIwatchdog, PMU, 하드웨어 에러, 디버깅

NMI 발생 원인

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

소스NMI 타입설명커널 설정
하드웨어 오류NMI_SERR메모리 패리티 에러, PCI SERR# 시그널(Signal), 버스(Bus) 에러BIOS/펌웨어(Firmware) 설정
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의 콜스택 수집기본 내장
NMI 소스 분류 체계 NMI (벡터 #2) NMI_LOCAL (0) PMU Overflow perf/watchdog GHES/APEI HW 에러 보고 KGDB/Backtrace 디버깅 IPI NMI_UNKNOWN (1) 외부 NMI 버튼 IPMI/BMC NMI_SERR (2) PCI SERR# 패리티/버스 에러 메모리 패리티 ECC 치명 에러 NMI_IO_CHECK (3) I/O 채널 에러 ISA/PCI 디바이스 default_do_nmi() 처리 순서 1. NMI_LOCAL 2. NMI_SERR 3. NMI_IO_CHECK 4. NMI_UNKNOWN 모든 핸들러가 NMI_DONE 반환 시 → unknown_nmi_error() 호출 각 타입별 핸들러 리스트를 순회하며 NMI_HANDLED 또는 NMI_DONE을 반환
NMI 소스 분류 체계: 4가지 NMI 타입과 각 타입에 속하는 소스, default_do_nmi()의 처리 순서
/* 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에 매핑(Mapping)됩니다.

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 구조체(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를 감지할 수 있는 유일한 메커니즘입니다.

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은 커널 스레드(Kernel Thread)가 긴 시간 동안 스케줄링되지 않는 상태를, hardlockup은 인터럽트 자체가 비활성화된 상태를 감지합니다.

구분SoftlockupHardlockup
감지 대상커널 스레드가 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_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"
가상화(Virtualization) 환경 주의: VM에서는 vCPU가 호스트에 의해 스케줄링되므로 NMI watchdog이 false positive를 발생시킬 수 있습니다. KVM, VMware, Hyper-V 등의 가상화 환경에서는 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_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)를 사용하여 전용 스택에서 실행됩니다. 이는 스택 오버플로(Stack Overflow) 상태에서도 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
Unknown NMI 진단 의사결정 플로차트 Unknown NMI 수신 반복 발생인가? 단발 로그 기록 후 관찰 유지 반복 다수 노드에서 동시 발생? Yes 플랫폼/전원 이벤트 BMC/PDU 로그 확인 단일 노드 EDAC CE/UE 카운터 증가? Yes 메모리 하드웨어 결함 DIMM 교체 계획 No PCIe AER 에러 로그? Yes PCIe 디바이스 결함 카드/슬롯 점검 No BIOS/펌웨어 문제 가능 BMC 이벤트 로그 + BIOS 업데이트 검토 unknown_nmi_panic=1 + kdump 설정으로 다음 발생 시 자동 crash dump 확보
Unknown NMI 수신 시 원인 추적 의사결정 흐름: 반복/확산/하드웨어 에러 순서로 범위를 좁혀갑니다

Unknown NMI reason 코드 해석

Unknown NMI 메시지에 표시되는 reason 값은 포트(Port) 0x61(NMI Status and Control Register)에서 읽은 값입니다. 각 비트의 의미를 이해하면 원인을 더 빠르게 좁힐 수 있습니다.

비트이름의미
Bit 7SERR# NMI StatusPCI SERR# 시그널로 인한 NMI (1=발생)
Bit 6IOCHK# NMI StatusI/O 채널 체크 에러로 인한 NMI (1=발생)
Bit 5Timer 2 Output타이머 2 출력 상태
Bit 4Refresh Cycle ToggleDRAM 리프레시 토글 비트
Bit 3IOCHK# NMI EnableI/O 채널 체크 NMI 활성 (1=비활성화)
Bit 2SERR# NMI EnableSERR# NMI 활성 (1=비활성화)
Bit 1Speaker Data스피커(Speaker) 데이터 비트
Bit 0Timer 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
서버 환경에서의 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 기반은 인터럽트가 비활성화된 크리티컬 섹션 내부까지 프로파일링할 수 있습니다.
PMU NMI 샘플링 파이프라인 1. PMU 카운터 설정 perf_event_open() sample_period = N 2. HW 카운팅 CPU 명령어/사이클/ 캐시미스 카운트 3. 카운터 오버플로 LVT PMC → NMI 발생 EFLAGS.IF 무시 cli 구간도 샘플링! 4. NMI 핸들러 x86_pmu_handle_irq() → perf_event_overflow() 5. 샘플 기록 IP, callchain, timestamp → perf ring buffer 6. 카운터 재장전 sample_period 재설정 → 2단계로 복귀 사용자 공간 (perf) mmap ring buffer 읽기 NMI Callchain 수집 방법 1. Frame Pointer (FP) unwinding: RBP 체인 추적 (빠름, -fno-omit-frame-pointer 필요) 2. ORC unwinder: .orc_unwind 섹션 기반 (정확, CONFIG_UNWINDER_ORC) 3. DWARF unwinder: .debug_frame 기반 (가장 정확, 오버헤드 큼) NMI에서는 FP 또는 ORC 권장: DWARF는 메모리 할당이 필요할 수 있어 NMI-unsafe
PMU 카운터 오버플로 → NMI → 샘플 기록 → 카운터 재장전의 전체 파이프라인. NMI이므로 cli 구간도 관측 가능

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-missescpu-clock, task-clock
전달 메커니즘PMU 카운터 오버플로 → LVT PMC → NMIhrtimer → 일반 인터럽트
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를 사용하여 즉각적인 알림이 이루어집니다.

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 컨텍스트에서는 페이지 테이블(Page Table) 조작이 불가능하므로, 부팅 시 미리 매핑해둔 fixmap 슬롯을 활용합니다. 복구 가능한 에러는 NMI-safe 풀에 저장 후 워크큐에서 나중에 처리합니다.
MCE: Machine Check Exception(#MC)은 NMI와 밀접한 관계에 있으며 일부 corrected MCE는 CMCI를 통해 NMI와 유사한 경로로 전달됩니다. MCA 뱅크/레지스터(Register), Monarch 랑데부, APEI/GHES 연동, EDAC, memory_failure()/HWPoison 등 MCE 전반은 MCE (Machine Check Exception) 페이지(Page)를 참고하세요.

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 경합(Contention)과 지연 시간이 급증합니다. 실무에서는 NMI에서 최소 정보만 기록하고, 나머지는 IRQ/workqueue/thread로 넘기는 2단계 파이프라인(Pipeline)을 사용합니다. 핵심은 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 빠른 격리(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 컨텍스트 위반 사전 감지
크래시 시나리오 진단 요약: NMI 관련 크래시에서 가장 중요한 것은 dmesg의 Call Trace에서 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
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와 SMI 상호작용

SMI(System Management Interrupt)는 NMI보다 우선순위가 높은 유일한 인터럽트로, CPU를 SMM(System Management Mode)으로 전환시킵니다. SMI 처리 중에는 NMI를 포함한 모든 인터럽트가 차단되므로, SMI는 NMI watchdog의 가장 큰 교란 요인입니다.

인터럽트 우선순위 계층과 SMI/NMI 관계 SMI (System Management) 최고 우선순위 -- 모든 것을 차단 NMI (Non-Maskable) cli/IF=0으로 차단 불가 MCE (Machine Check) 치명적 하드웨어 오류 일반 인터럽트 (IRQ) cli/local_irq_disable()로 마스킹 가능 높음 낮음 SMI가 NMI Watchdog에 미치는 영향 1. BIOS가 SMI 핸들러에서 수백 ms~수 초 동안 CPU를 독점 (메모리 스크러빙, UEFI 런타임) 2. SMI 동안 NMI 전달 차단 → hrtimer 카운터 업데이트 안 됨 → 가짜 hardlockup 감지 3. 해결: watchdog_thresh 상향, SMI 소스 제거, 또는 msr-tools로 SMI 카운터 모니터링
인터럽트 우선순위 계층: SMI는 NMI보다 우선순위가 높아 NMI watchdog의 false positive를 유발할 수 있음

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 교체, 에러 소스 격리
SMI 감지의 한계: SMI는 운영체제에 완전히 투명하게 설계되었으므로, 커널이 SMI 발생을 직접 감지하는 것은 불가능합니다. IA32_SMI_COUNT MSR은 횟수만 제공하고 지속 시간은 알 수 없습니다. 정밀한 SMI 레이턴시 측정이 필요하면 hwlatdetect를 사용하되, 이것도 NMI에 의한 레이턴시와 구분하기 어려울 수 있습니다.
프로덕션 권장: 서버 환경에서 NMI watchdog false positive가 빈번하면 다음 순서로 조치하세요: (1) rdmsr 0x34로 SMI 빈도 확인, (2) BIOS에서 불필요한 SMI 소스(USB Legacy, 메모리 스크러빙 주기) 조정, (3) watchdog_thresh를 SMI 최대 지속 시간보다 크게 설정, (4) 그래도 해결 안 되면 HARDLOCKUP_DETECTOR_BUDDY 사용을 검토합니다.

NMI 코드 리뷰 체크리스트

NMI 코드 품질은 기능보다 실패 시나리오로 평가해야 합니다. 아래 체크리스트는 커널 모듈(Kernel Module)/플랫폼 코드 리뷰 시 자주 사용하는 항목입니다. 특히 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 패치(Patch) 리뷰에서는 정상 경로보다 실패 경로를 먼저 읽으세요. 특히 "핸들러가 어떤 조건에서 NMI_HANDLED를 반환하는지", "NMI에서 무엇을 하지 않도록 설계했는지"를 확인하면 결함을 빠르게 걸러낼 수 있습니다.

NMI 장애 대응 플레이북

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

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 정책 조정, 문제 노드 격리, 펌웨어/메모리 교체 또는 커널 롤백(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
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 수집 연동
보고서 품질 기준: "무엇이 일어났는가"보다 "왜 다시 일어날 수 있는가"를 명확히 쓰는 것이 핵심입니다. 근본 원인 섹션에 기술적 원인과 운영적 원인을 함께 분리 기록하세요.

NMI 진입 어셈블리(Assembly) 상세 분석

x86_64에서 NMI가 발생하면 CPU는 IDT#2를 통해 asm_exc_nmi로 진입합니다. 이 어셈블리 코드는 일반 인터럽트 진입과 달리, NMI 중첩 감지, IST 스택 관리, IRET 프레임 보호 등 복잡한 상태 머신을 구현해야 합니다. 이 섹션에서는 arch/x86/entry/entry_64.S의 NMI 진입 어셈블리를 단계별로 분석합니다.

NMI 어셈블리 진입 상태 머신 (asm_exc_nmi) 1. IST 진입 CPU→IST#2 스택 RIP,CS,RFLAGS,RSP,SS 저장 2. 중첩 확인 RSP가 NMI 스택 범위? nmi_executing 확인 first NMI nested 3. 스택 전환 IST→전용 NMI 스택 복사 IRET 프레임 보호 영역 설정 4. C 핸들러 호출 nmi_entering() exc_nmi() → default_do_nmi() 5. 복귀/재처리 nmi_executing 클리어 repeat_nmi 확인 → IRET repeat_nmi (중첩 감지 시) Nested NMI 처리 IRET 프레임 수정 → repeat_nmi 설정 IRET 프레임 레이아웃 (IST#2 스택) +0x28 SS +0x20 RSP +0x18 RFLAGS +0x10 CS +0x08 RIP +0x00 Error code (0) ← 보호 영역 ← 보호 영역 스택 전환 이유 • IRET 실행 중 NMI 발생 → IST 스택 덮어쓰기 • 해결: IRET 프레임을 별도 영역에 복사 • nmi_executing 변수로 중첩 상태 추적 • nested NMI는 원래 IRET 프레임을 수정하여 repeat_nmi로 점프하도록 설정
NMI 어셈블리 진입 상태 머신: IST 진입부터 중첩 감지, 스택 전환, C 핸들러 호출까지의 5단계

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 취약 구간: NMI 복귀를 위한 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_executing 변수: 이 Per-CPU 변수는 NMI 스택의 고정 오프셋(Offset)에 위치합니다. 값이 1이면 현재 CPU가 NMI를 처리 중임을 의미하며, 새로 도착한 NMI는 nested NMI 경로로 분기합니다. 이 변수는 반드시 IRET 직전에 0으로 클리어되어야 합니다.

NMI-safe printk 메커니즘

표준 printk()는 내부적으로 스핀락(Spinlock), console 드라이버 호출 등 NMI 컨텍스트에서 안전하지 않은 연산을 수행합니다. 커널은 NMI 내부에서도 로그를 안전하게 기록할 수 있도록 별도의 NMI-safe printk 경로를 제공합니다.

NMI-safe printk 경로 NMI 진입 printk_nmi_enter() this_cpu_or(printk_context, PRINTK_NMI_CONTEXT_MASK) printk() in_nmi() == true? YES nmi_print_seq 버퍼 Per-CPU seq_buf에 기록 (lockless) NO 일반 printk 경로 (console) NMI 종료 → irq_work flush → console 출력
NMI 내부의 printk는 lockless Per-CPU 버퍼에 기록된 후, NMI 종료 시점에 flush되어 console에 출력됨

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;
}
SAFE_LOG_BUF_LEN: NMI-safe printk 버퍼 크기는 기본 8KB입니다. 다량의 NMI 로그가 발생하는 환경(예: GHES 에러 폭풍)에서는 버퍼 오버플로(Buffer Overflow)로 메시지가 유실될 수 있습니다. 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가 갑자기 커널 코드를 실행하게 되었음을 인지해야 합니다.

RCU와 NMI 상태 전이 Idle (EQS 활성) dynticks_nesting == 0 NMI! rcu_nmi_enter() dynticks_nmi_nesting++ EQS → Active 전이 기록 NMI 핸들러 실행 RCU 읽기 안전하게 가능 rcu_nmi_exit() dynticks_nmi_nesting-- Active → EQS 복원 EQS 복원 Idle (EQS 복원) dynticks_nesting == 0 (변함없음) 중첩 NMI에서의 RCU 상태 • 첫 번째 NMI: EQS 해제 (dynticks_nmi_nesting: 0→1) • 중첩 NMI: nesting 증가만 (dynticks_nmi_nesting: 1→2), EQS 전이 없음 • 마지막 NMI 종료: nesting이 0이 되면 EQS 복원
NMI 발생 시 RCU EQS 상태 전이: idle CPU에서 NMI가 발생하면 일시적으로 EQS를 해제하고 NMI 종료 시 복원

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 dynticks 카운터는 짝수일 때 EQS(quiescent), 홀수일 때 Active 상태를 의미합니다. NMI 진입 시 이 카운터가 짝수이면 홀수로 전환하여 RCU가 이 CPU를 Active로 인식하게 합니다.
함수동작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 핸들러 레이턴시 측정 파이프라인 핸들러 시작 nmi_handler() rdtsc (시작) t_start = rdtsc() NMI 핸들러 체인 각 핸들러 순회 실행 (perf, watchdog, ...) rdtsc (종료) t_end = rdtsc() 임계값 비교 delta > 1ms ? 초과 WARN 출력 "NMI handler took too long" max_duration 추적 cmpxchg로 전역 최대값 atomic 업데이트 → /sys/kernel/debug/nmi_longest_ns perf stat -e 'nmi:*' 또는 perf record -e cycles:P 로 NMI 오버헤드 외부 측정 가능
NMI 핸들러 실행 시간을 rdtsc로 측정하고 임계값(1ms)을 초과하면 WARN을 출력하는 파이프라인

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() NMI-safe 여부: sched_clock()은 대부분의 x86 시스템에서 TSC를 직접 읽으므로 NMI-safe입니다. 그러나 TSC가 unstable로 표시된 시스템(일부 가상화 환경, 오래된 AMD CPU)에서는 jiffies 기반 폴백을 사용하여 정밀도가 떨어질 수 있습니다.
측정 방법도구측정 대상정밀도
커널 내부nmi_check_duration개별 NMI 핸들러나노초 (TSC 기반)
perf statperf stat -e cycles:PNMI PMI 오버헤드(Overhead)사이클 단위
ftracefunction_graph tracerNMI 핸들러 호출 트리마이크로초
debugfs/sys/kernel/debug/nmi_longest_ns부팅 후 최대 레이턴시나노초
BPFbpftrace nmi_handler probeNMI 핸들러 분포나노초

ARM64 Pseudo-NMI

ARM64 아키텍처에는 x86의 NMI에 대응하는 하드웨어 메커니즘이 전통적으로 없었습니다. 대신, GICv3(Generic Interrupt Controller v3)의 인터럽트 우선순위(priority) 기반 마스킹을 활용하여 "pseudo-NMI"를 구현합니다. ARMv8.8+에서는 FEAT_NMI를 통해 하드웨어 NMI 지원이 추가되었습니다.

ARM64 GICv3 Pseudo-NMI 아키텍처 GIC Distributor GICD_IPRIORITYn 설정 인터럽트 → CPU 라우팅 GIC Redistributor Per-CPU SGI/PPI 관리 GICR_IPRIORITYn CPU Interface (ICC) ICC_PMR_EL1 = Priority Mask priority < PMR → 전달됨 인터럽트 priority vs PMR 낮은 우선순위 (≥ PMR) 일반 IRQ local_irq_disable()로 마스킹 가능 높은 우선순위 (< PMR) Pseudo-NMI PMR 마스킹 우회! PMR 기반 마스킹 메커니즘 • local_irq_disable() → PMR을 낮은 우선순위로 설정 • pseudo-NMI는 PMR보다 높은 우선순위 → 마스킹 불가 • DAIF.I(PSR) 플래그는 건드리지 않음 (superpriority 인터럽트) ARMv8.8+ FEAT_NMI: 하드웨어 NMI 지원 PSTATE.ALLINT 비트, Superpriority 인터럽트, FIQ를 NMI로 사용 가능, GICv3 GICD_INMIR 레지스터
ARM64 GICv3에서 PMR(Priority Mask Register) 기반 pseudo-NMI 구현과 ARMv8.8+ 하드웨어 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 NMIARM64 pseudo-NMI
하드웨어 지원전용 NMI 핀 (IDT#2)GICv3 우선순위 기반 (소프트웨어)
마스킹 메커니즘CPU 내부 NMI blockingICC_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
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 처리를 표준화하고 있습니다.

RISC-V Smrnmi NMI 처리 흐름 플랫폼 NMI 하드웨어 오류, watchdog 전원 이상, 버스 에러 mncause CSR NMI 원인 코드 기록 mnepc = 중단된 PC rnmi 트랩 핸들러 mnstatus.NMIE=0 (자동) → NMI 중첩 차단 mnret NMIE=1 복원 mnepc로 복귀 Smrnmi 확장 CSR mncause NMI 원인 코드 (Interrupt bit + Exception Code) mnepc NMI 발생 시점의 PC (mnret 복귀 주소) mnstatus NMIE 비트 (0=NMI 차단, 1=NMI 허용), MNPP (이전 특권 모드) mnscratch NMI 핸들러용 임시 레지스터 저장 공간 mnret NMI 복귀 명령어 (mnepc → PC, mnstatus.NMIE=1)
RISC-V Smrnmi 확장의 NMI 처리 흐름: mncause에 원인을 기록하고, rnmi 핸들러에서 처리 후 mnret으로 복귀

x86/ARM64/RISC-V NMI 비교

항목x86ARM64RISC-V
NMI 벡터IDT#2 (고정)GICv3 priority 기반mtvec + rnmi 벡터
중첩 방지CPU NMI blocking + ISTPMR 마스킹mnstatus.NMIE=0
복귀 명령IRETERETmnret
원인 레지스터없음 (소프트웨어 판별)IAR (GIC)mncause CSR
전용 스택IST#2없음없음 (mnscratch 사용)
표준화완전 표준GICv3 사양Smrnmi 확장 (옵션)
Linux 지원완전 지원pseudo-NMI (v4.20+)제한적 (SiFive 등)
하드웨어 NMI있음FEAT_NMI (v8.8+)Smrnmi (옵션)
RISC-V NMI 현실: 2024년 기준 대부분의 RISC-V 프로세서는 Smrnmi를 구현하지 않습니다. SiFive의 일부 코어에서 vendor-specific NMI를 지원하지만, 표준 Linux NMI 프레임워크와의 통합은 아직 초기 단계입니다. 대부분의 RISC-V 플랫폼에서는 NMI 대신 최고 우선순위 인터럽트로 watchdog을 구현합니다.

NMI 가상화 (KVM/VMX)

가상화 환경에서 NMI 처리는 호스트와 게스트 사이의 전환(VM-exit/VM-entry)과 밀접하게 관련됩니다. KVM은 호스트 NMI를 적절히 핸들링하고, 게스트에 NMI를 주입(injection)하는 메커니즘을 관리합니다.

KVM NMI 가상화 흐름 (Intel VMX) Host NMI 발생 LAPIC / 외부 NMI VM-exit Exit reason: NMI Guest 상태 → VMCS 저장 KVM NMI 핸들링 vmx_handle_nmi() 호스트 NMI 핸들러 실행 Guest NMI 요청 KVM_NMI ioctl NMI Window NMI blocking 해제 대기 VMCS NMI-window exiting NMI Injection VM-entry interruption info = NMI, vector=2 VM-entry Guest IDT#2 실행 Guest NMI 핸들러 VMCS NMI 관련 필드 Pin-Based Controls NMI Exiting, Virtual NMIs Primary Proc Controls NMI-window Exiting Guest Interruptibility Blocking by NMI (virtual NMI blocking 상태) VM-entry Interruption Type=NMI(2), Vector=2, Valid=1 → 게스트에 NMI 주입
KVM에서 Host NMI의 VM-exit 처리와 Guest NMI injection 메커니즘 (Intel VMX)

AMD SVM vs Intel VMX NMI 가상화

항목Intel VMXAMD SVM
NMI exit 제어Pin-Based Controls의 NMI Exiting 비트VMCB Intercept의 NMI 비트
Virtual NMI지원 (Virtual NMIs 비트)제한적 (vNMI는 별도)
NMI windowNMI-window Exiting (Primary Proc)IRET intercept로 구현
NMI blocking 추적Guest Interruptibility StateVMCB의 V_NMI_MASK
NMI injectionVM-entry Interruption InfoVMCB EventInj
nested NMIIST 문제 동일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);
}
Nested NMI in VM: 게스트 내부에서 NMI 중첩이 발생하면, 게스트 커널의 NMI 트램폴린이 처리합니다. 그러나 게스트가 IRET을 실행하는 시점에 호스트 NMI가 발생하면 추가 VM-exit가 발생할 수 있어, KVM은 vnmi_blocked 상태를 세밀하게 추적해야 합니다.

NMI와 kdump 연동 상세

커널 패닉(Kernel Panic) 시 kdump는 NMI를 사용하여 모든 secondary CPU를 강제 정지시킵니다. 이 "NMI shootdown" 메커니즘은 교착 상태에 빠진 CPU에서도 동작하며, crash dump의 일관성을 보장하는 핵심 요소입니다.

NMI kdump 시퀀스 (커널 패닉 시) panic() panic CPU 결정 NMI Shootdown smp_send_nmi_allbutself() 모든 CPU에 NMI 전송 CPU1: crash_nmi_callback() CPU2: crash_nmi_callback() CPU3: crash_nmi_callback() Secondary CPU 정지 레지스터 저장 halt 진입 (무한 루프) all CPUs stopped crash_kexec() kdump 커널로 kexec kdump 커널 예약 메모리에서 부팅 vmcore 덤프 crash_nmi_callback() 상세 동작 1. cpu_emergency_stop_pt_regs에 현재 레지스터 상태 저장 (crash dump 분석용) 2. atomic_dec(&waiting_for_crash_ipi) → panic CPU에 정지 완료 알림 3. local_irq_disable() → halt 무한 루프 진입 (다시는 돌아오지 않음) 4. 타임아웃(1초): 응답 없는 CPU는 스킵하고 crash_kexec 진행
커널 패닉 시 NMI shootdown으로 모든 CPU를 정지시킨 후 kdump 커널을 실행하는 시퀀스
/* 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 + kdump 주의: 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에 쓸 수 있어야 합니다.

ftrace ring_buffer NMI-safe 쓰기 경로 NMI 발생 PMU overflow in_nmi() 확인 ftrace 재진입 방지 NMI-safe ring_buffer ring_buffer_lock_reserve() Per-CPU, cmpxchg 기반 Atomic Reserve cmpxchg(&cpu_buffer->commit, old, new) → 공간 예약 ring_buffer_unlock_commit() 재진입 방지 메커니즘 • ftrace_nmi_enter/exit • per_cpu(ftrace_in_nmi) 일반 컨텍스트: local_irq_disable() + 예약 → 쓰기 → commit | NMI: cmpxchg만으로 예약 → 쓰기 → cmpxchg commit (lockless)
NMI에서 ftrace ring_buffer에 이벤트를 기록하는 lockless 경로: cmpxchg 기반 공간 예약과 commit
/* 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선점 불가
NMIcmpxchg 기반 locklessatomic nest 카운터
NMI 중첩reserve 실패 (이벤트 유실)nest > 1이면 포기
function tracer 재진입 방지: NMI 내부에서 function tracer가 활성화되어 있으면, NMI 핸들러 자체의 함수 호출이 다시 tracer를 트리거할 수 있습니다. 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 핸들러에서 콜백이 실행됩니다.

irq_work: NMI에서 IRQ 컨텍스트로 작업 위임 NMI 컨텍스트 NMI 핸들러 irq_work_queue() llist_add(&work, &list) arch_irq_work_raise() → self-IPI IRQ 컨텍스트 self-IPI 수신 irq_work_run() llist_del_all → 콜백 실행 work->func(work) 실행 IPI llist (lockless linked list) 동작 원리 • llist_add(): cmpxchg(&head, old, new) → NMI-safe, 락 불필요 • llist_del_all(): xchg(&head, NULL) → 전체 리스트를 원자적으로 분리 • 여러 NMI에서 동시에 llist_add() 가능 → 모두 안전하게 연결됨
irq_work 파이프라인: NMI에서 llist_add로 작업을 큐잉하고, 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-safeseq_buf에 기록seq_buf → main log_buf flush
perf wakeupring_buffer에 샘플 기록task wakeup (schedule)
watchdoghardlockup 감지panic() 또는 경고 출력
RCU callbackgrace period 체크RCU callback 처리
trace outputtrace 이벤트 기록trace 출력 flush
IRQ_WORK_LAZY: IRQ_WORK_LAZY 플래그를 설정하면 self-IPI를 보내지 않고, 다음 timer tick에서 실행됩니다. NMI 오버헤드를 줄이고 싶을 때 유용하지만, 실행 지연이 발생합니다.

PMU NMI 내부 메커니즘

Performance Monitoring Unit(PMU)는 NMI의 가장 빈번한 소스입니다. 하드웨어 성능 카운터가 오버플로하면 LAPIC의 LVT Performance Counter 레지스터를 통해 NMI가 발생하며, perf_event 서브시스템이 샘플을 기록합니다.

PMU NMI 발생 경로: 카운터 오버플로 → perf 샘플 기록 프로그램 실행 명령어/캐시 미스/... PMC 카운터 이벤트마다 카운트 증가 → 오버플로 임계값 도달 overflow! LVT Performance Delivery Mode = NMI → IDT#2 트리거 perf_event_nmi_handler() x86_pmu.handle_irq() → intel_pmu_handle_irq() perf_event_output() ring_buffer에 샘플 기록 IP, callchain, timestamp PEBS (Intel) Precise Event Based Sampling 하드웨어가 직접 DS 영역에 기록 → NMI에서 DS 영역 drain x86_pmu 구조체 핵심 필드 .handle_irq = intel_pmu_handle_irq .enable = intel_pmu_enable_event .disable = intel_pmu_disable_event .drain_pebs = intel_pmu_drain_pebs_nhm .num_counters = 8 (Skylake+) .max_period = (1ULL << 47) - 1
PMU 카운터 오버플로 → LVT NMI 발생 → perf_event 핸들러 → 샘플 기록의 전체 경로

Intel vs AMD PMU NMI 차이점

항목IntelAMD
카운터 수 (범용)8개 (Skylake+), 4개 (이전)6개 (Zen3+), 4개 (이전)
정밀 샘플링PEBS (v1~v5)IBS (Instruction/Op)
NMI 전달LVT Performance CounterLVT Performance Counter
카운터 MSRIA32_PMCx (0x0C1+)MSR_F15H_PERF_CTR (0xC0010200+)
오버플로 감지IA32_PERF_GLOBAL_STATUS개별 카운터 MSB 확인
PEBS/IBS DSDS(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;
}
PMU NMI 폭풍: 너무 작은 샘플 주기(sample_period)를 설정하면 PMU NMI가 과도하게 발생하여 시스템 성능이 급격히 저하됩니다. 커널은 perf_event_max_sample_rate를 통해 자동으로 샘플 주기를 조절합니다. 기본값은 100,000 samples/sec이며, NMI 오버헤드가 1%를 초과하면 자동 감소합니다.

NMI 처리의 역사적 진화

Linux의 NMI 처리 메커니즘은 20년 이상에 걸쳐 단순한 핸들러에서 복잡한 트램폴린 시스템으로 진화했습니다. 각 세대는 이전 세대의 근본적 문제를 해결하면서 새로운 복잡성을 도입했습니다.

Linux NMI 처리 진화 타임라인 ~2003 1세대: i386 단순 NMI 핸들러 커널 스택 사용 중첩 보호 없음 문제: 스택 오버플로 2004 2세대: x86_64 IST IST#2 전용 스택 스택 오버플로 해결 Per-CPU IST 스택 문제: 중첩 NMI 스택 충돌 2012 3세대: 트램폴린 IRET 프레임 복사 nmi_executing 변수 repeat_nmi 메커니즘 문제: 복잡한 어셈블리 2024+ 4세대: FRED 별도 스택 레벨 IRET 문제 해결 ERETU/ERETS 명령 근본적 해결 세대별 핵심 문제와 해결 1세대 문제: NMI가 이미 깊은 커널 스택에서 발생하면 스택 오버플로 → 해결: IST 전용 스택 2세대 문제: 중첩 NMI가 동일한 IST 스택을 재사용 → IRET 프레임 덮어쓰기 → 해결: 트램폴린 3세대 문제: 500줄 이상의 복잡한 어셈블리, IRET 취약 구간 여전히 존재 → 해결: FRED 4세대 해결: FRED는 이벤트 유형별 독립 스택 레벨 제공, IRET 대신 ERETU/ERETS 사용 → NMI 중첩 문제가 하드웨어 레벨에서 근본적으로 해결됨 주요 기여자: Andy Lutomirski (트램폴린, 2012), H. Peter Anvin (FRED 초기 설계), Xin Li (FRED 커널 구현, 2024)
Linux NMI 처리 진화: i386 단순 핸들러 → IST 전용 스택 → 트램폴린 → FRED 하드웨어 지원
세대시기스택중첩 보호복귀 명령커널 코드 복잡도
1세대 (i386)~2003커널 스택없음IRET낮음
2세대 (IST)2004IST#2없음 (하드웨어 의존)IRET중간
3세대 (트램폴린)2012IST#2 + 복사소프트웨어 (nmi_executing)IRET매우 높음
4세대 (FRED)2024+스택 레벨 0하드웨어 자동ERETS낮음
FRED 채택 현황 (2024): Intel의 Sierra Forest, Grand Ridge 등 차세대 프로세서에서 FRED를 지원합니다. Linux 커널 6.9에서 FRED 지원이 메인라인에 병합되었으며, CONFIG_X86_FRED로 활성화합니다. FRED가 활성화된 시스템에서는 NMI 관련 어셈블리 코드가 대폭 간소화됩니다.

FRED 아키텍처와 NMI

FRED(Flexible Return and Event Delivery)는 Intel이 도입한 새로운 이벤트 전달 메커니즘으로, 기존 IDT 기반 인터럽트/예외 처리의 근본적 문제를 해결합니다. 특히 NMI 처리에서 IRET 취약 구간 문제를 하드웨어 레벨에서 제거합니다.

기존 IDT vs FRED: NMI 처리 비교 기존 IDT 방식 NMI 진입 IDT#2 → IST#2 스택으로 자동 전환 중첩 문제 (SW 해결 필요) IST 재사용 → IRET 프레임 복사 (트램폴린) NMI 핸들러 실행 (exc_nmi) IRET 취약 구간! IRET 실행 중 NMI 발생 가능 → nmi_executing + repeat_nmi로 해결 어셈블리 코드: ~500줄 (entry_64.S) FRED 방식 NMI 진입 FRED 스택 레벨 0 (NMI 전용) → 자동 전환 중첩 문제 없음 (HW 해결) NMI 중 NMI blocking 자동 유지, 스택 레벨 독립 NMI 핸들러 실행 (fred_exc_nmi) ERETS로 복귀 IRET 사용 안 함 → 취약 구간 없음 NMI blocking 해제는 ERETS 완료 후 원자적 어셈블리 코드: ~50줄 (fred.c) FRED는 NMI 중첩 문제를 하드웨어 레벨에서 근본적으로 해결: 코드 10배 감소
기존 IDT 방식과 FRED 방식의 NMI 처리 비교: FRED는 IRET 취약 구간을 제거하고 코드를 10배 간소화

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 blockingERETS 완료까지 NMI blocking 유지 → 소프트웨어 추적 불필요
ERETU/ERETS 명령IRET 대체 → 비원자적 복귀 문제 해결
이벤트 유형 필드NMI vs 예외 vs 인터럽트를 하드웨어가 구분 → 소프트웨어 판별 불필요
IST 불필요TSS의 IST 설정 불필요, IST 관련 코드 모두 제거 가능
ERETS vs IRET: ERETS(Event Return to Supervisor)는 커널 모드로의 복귀에 사용되며, ERETU(Event Return to User)는 사용자 모드로의 복귀에 사용됩니다. 두 명령어 모두 IRET과 달리 이벤트 유형에 따른 blocking 해제를 원자적으로 수행합니다. NMI의 경우, ERETS가 완료되기 전까지 NMI blocking이 유지되므로 IRET 취약 구간이 존재하지 않습니다.

NMI 관련 커널 설정 총정리

NMI 관련 커널 기능은 다수의 CONFIG 옵션에 의해 제어됩니다. 이 섹션에서는 NMI와 관련된 모든 Kconfig 옵션의 의존성 관계와 운영 환경별 권장 설정을 정리합니다.

NMI 관련 Kconfig 의존성 트리 HARDLOCKUP_DETECTOR HARDLOCKUP_DETECTOR_PERF PERF_EVENTS X86_LOCAL_APIC HARDLOCKUP_DETECTOR_BUDDY LOCKUP_DETECTOR SOFTLOCKUP_DETECTOR 추가 NMI 관련 CONFIG 옵션 X86_FRED NMI_LOG_BUF_SHIFT UNKNOWN_NMI_PANIC ARM64_PSEUDO_NMI KGDB_NMI DETECT_HUNG_TASK BOOTPARAM_HARDLOCKUP_PANIC
NMI 관련 Kconfig 의존성 트리: HARDLOCKUP_DETECTOR를 중심으로 한 옵션 체인

CONFIG 옵션 전체 맵

CONFIG 옵션설명의존성기본값
HARDLOCKUP_DETECTORNMI 기반 hardlockup 감지 활성화LOCKUP_DETECTORy (대부분 배포판)
HARDLOCKUP_DETECTOR_PERFPMU NMI 기반 hardlockup 감지PERF_EVENTS + HAVE_HARDLOCKUP_DETECTOR_PERFy (x86)
HARDLOCKUP_DETECTOR_BUDDYCPU buddy 기반 (PMU 없는 환경)SMPn
PERF_EVENTSperf 프레임워크 (PMU NMI 발생)HAVE_PERF_EVENTSy
X86_LOCAL_APICLocal APIC (NMI 전달)X86y
LOCKUP_DETECTORsoft/hard lockup 감지 프레임워크-y
SOFTLOCKUP_DETECTORsoftlockup watchdog (NMI 아님)LOCKUP_DETECTORy
BOOTPARAM_HARDLOCKUP_PANIChardlockup 시 자동 패닉HARDLOCKUP_DETECTORn
X86_FREDFRED NMI 처리 (새 하드웨어)X86_64, CPU_SUP_INTELy (지원 시)
ARM64_PSEUDO_NMIARM64 pseudo-NMI 지원ARM64 + GICv3y
KGDB_NMINMI로 KGDB 디버거 진입KGDB + SERIAL_CORE_CONSOLEn
DETECT_HUNG_TASKhung task 감지 (NMI 연관)-y

운영 환경별 권장 Kconfig 설정

옵션프로덕션 서버개발/디버깅실시간(RT)가상머신
HARDLOCKUP_DETECTORyyn (레이턴시)y
HARDLOCKUP_DETECTOR_PERFyynn (vPMU 없으면)
HARDLOCKUP_DETECTOR_BUDDYnny (대안)y (대안)
BOOTPARAM_HARDLOCKUP_PANICy (+ kdump)nnn
KGDB_NMInynn
X86_FREDy (지원 시)yyn (패스스루 필요)
PERF_EVENTSyy선택적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초 뒤 재부팅
가상머신 환경 주의: 대부분의 가상머신은 vPMU(Virtual PMU)를 제공하지 않으므로 HARDLOCKUP_DETECTOR_PERF가 동작하지 않습니다. KVM에서 -cpu host,pmu=on으로 vPMU를 활성화하거나, HARDLOCKUP_DETECTOR_BUDDY를 대안으로 사용하세요. buddy 감지기는 인접 CPU가 NMI 대신 hrtimer를 사용하여 교차 감시합니다.
RT(실시간) 커널과 NMI: PREEMPT_RT 커널에서 NMI watchdog는 추가 레이턴시 소스가 됩니다. RT 환경에서는 nmi_watchdog=0으로 비활성화하고, 대신 외부 하드웨어 watchdog이나 buddy 감지기를 사용하는 것이 권장됩니다.

참고자료

공식 커널 문서

LWN.net 기사

커널 소스 경로

파일 역할
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 구현 — 재진입 방지 버퍼링

하드웨어 사양 문서

기술 블로그 및 발표 자료

필수 관련 문서:
  • 인터럽트 처리 -- IDT, APIC, 인터럽트 벡터, 인터럽트 핸들링 전체 구조
  • SoftIRQ/HardIRQ -- 하드웨어 인터럽트(top-half)와 소프트 인터럽트(bottom-half) 처리 구조
참고 문서:
권장 학습 순서: 인터럽트 처리SoftIRQ/HardIRQWatchdog크래시 분석디버깅 & 트러블슈팅 순서로 보면 NMI 장애 대응 흐름을 빠르게 연결할 수 있습니다.