preempt_count

Linux 커널의 preempt_count는 32비트 정수 하나로 현재 CPU의 실행 컨텍스트(hardirq·softirq·NMI)와 선점 비활성화 깊이를 동시에 인코딩합니다. 이 문서는 비트 필드 구조, 컨텍스트 판별 매크로, preempt_disable/enable 메커니즘, 선점 모델(NONE/VOLUNTARY/FULL/RT), PREEMPT_DYNAMIC 런타임 전환, PREEMPT_LAZY(6.12+), Per-CPU 저장 구조, might_sleep/in_atomic 심화, DEBUG_PREEMPT 진단까지 코드 흐름 중심으로 통합 정리합니다.

전제 조건: 인터럽트, softirq/hardirq, 프로세스 스케줄러, 동기화 기법 문서를 먼저 읽으세요. preempt_count는 "현재 CPU가 어떤 문맥에 있는지", "지금 당장 스케줄러에 CPU를 넘겨도 되는지", "인터럽트 하위 반이 막혀 있는지"를 한 번에 담는 값이라서, 선점과 인터럽트와 락의 기초 개념이 같이 잡혀 있어야 전체 흐름이 보입니다.
일상 비유: preempt_count는 공용 정비 구역 입구에 붙은 작업 상태판과 비슷합니다. "정비공이 작업 중인지", "긴급 호출이 들어왔는지", "하위 작업반 출입을 막아 두었는지", "일반 정비가 아니라 비상 정비인지"를 색깔 칸 하나에 같이 적어 둔 셈입니다. 스케줄러는 이 상태판을 보고 "지금 다른 사람을 들여보내도 되는가"를 결정합니다.

핵심 요약

  • preempt_count — 현재 실행 문맥과 선점 금지 깊이를 함께 담는 32비트 상태 값입니다.
  • PREEMPT 필드preempt_disable(), 일반적인 spin_lock() 같은 "선점 금지" 중첩 횟수를 나타냅니다.
  • SOFTIRQ/HARDIRQ/NMI 필드 — 단순히 "락을 잡았다"가 아니라 인터럽트 문맥 자체인지까지 구분해 줍니다.
  • need_resched 결합 — 이 문서에서 설명하는 빠른 경로는 "선점 가능"과 "재스케줄 필요"를 하나의 워드 비교로 확인하도록 설계되어 있습니다.
  • 균형 유지 — 증가시킨 필드는 반드시 같은 경로에서 정확히 감소시켜야 하며, 한 번이라도 어긋나면 선점이 영구히 막히거나 디버그 경고가 발생합니다.

단계별 이해

  1. 평상시 상태
    프로세스 컨텍스트에서 락도 없고 인터럽트 문맥도 아니면 preempt_count의 주요 필드는 0입니다. 이때가 가장 자유로운 상태입니다.
  2. 임계 구역 진입
    preempt_disable() 또는 일반적인 spin_lock()이 PREEMPT 필드를 올려 "잠깐만, 지금은 중간에 끊으면 안 됨"을 표시합니다.
  3. 인터럽트 진입
    IRQ가 들어오면 HARDIRQ 필드가 올라가고, softirq 처리나 NMI 진입이면 각각 자기 필드가 설정됩니다. 이제 현재 코드는 특정 태스크의 sleep 가능한 문맥이 아닙니다.
  4. 재스케줄 요청 누적
    다른 태스크가 깨어나 우선순위가 더 높아져도, 당장 스케줄하지 못하면 "필요함" 표시만 남겨 둡니다. 이것이 선점 지연(deferred preemption)입니다.
  5. 마지막 금지 요인 해제
    irq_exit(), local_bh_enable(), preempt_enable(), spin_unlock() 같은 경로가 마지막 카운터를 0으로 만들면 그때 비로소 스케줄러 진입 여부를 판단합니다.

개요: 선점의 개념

커널 선점(Kernel Preemption)이란 커널 모드에서 실행 중인 태스크를 더 높은 우선순위의 태스크가 강제로 중단시키고 CPU를 빼앗는 메커니즘입니다. 유저 모드 선점은 syscall 반환이나 인터럽트 반환 시점에서 자연스럽게 발생하지만, 커널 모드 선점은 추가적인 안전 장치가 필요합니다.

왜 커널 모드에서 무조건 선점하면 안 될까요? 커널 코드는 공유 자료 구조(예: 런큐, 페이지 테이블, 파일시스템 캐시)를 조작하는 도중에 중단되면 일관성이 깨질 수 있기 때문입니다. 유저 공간에서는 각 프로세스가 독립적인 주소 공간을 갖지만, 커널은 모든 프로세스가 동일한 커널 주소 공간을 공유합니다. 따라서 커널은 "지금 선점해도 안전한가?"를 매 시점에 판단해야 하며, 이 판단의 핵심이 바로 preempt_count입니다.

커널 선점 판단 흐름 (인터럽트 반환 시) 인터럽트 발생 (IRQ) IRQ 핸들러 실행 (hardirq 컨텍스트) 이전 모드가 유저 모드? Yes 무조건 선점 검사 need_resched → schedule() (유저 모드 선점: 항상 안전) No (커널 모드) preempt_count == 0 ? No (≠0) 선점 보류 중단된 커널 코드로 복귀 (iret) Yes (==0) need_resched 설정됨? Yes preempt_schedule_irq() 커널 선점 실행! No 커널 코드로 복귀 핵심: preempt_count == 0 검사가 커널 선점의 게이트키퍼 — 이 값이 0이 아니면 커널 모드에서 절대로 선점하지 않음

preempt_count는 이 안전 장치의 핵심으로, 다음 세 가지 질문에 동시에 답합니다:

  1. 현재 어떤 컨텍스트인가? — hardirq, softirq, NMI, 또는 프로세스 컨텍스트
  2. 선점이 안전한가? — 선점 비활성화 중첩 깊이, 인터럽트 컨텍스트 여부
  3. 선점이 필요한가? — bit 31의 NEED_RESCHED 플래그(반전 저장)

이 32비트 카운터가 0이면 "프로세스 컨텍스트이며 선점 가능하고 리스케줄이 필요"를 의미합니다. 0이 아닌 값은 현재 CPU에서 선점이 불가함을 나타내며, 각 비트 영역이 그 이유를 인코딩합니다.

/* 커널 선점 결정의 핵심 흐름 */
if (preempt_count() == 0 && need_resched()) {
    /* 선점 가능: preempt_count가 0이고 리스케줄 요청이 있음 */
    __schedule(SM_PREEMPT);
}
/* preempt_count != 0 이면 → 현재 컨텍스트가 안전하지 않으므로 선점 보류 */

preempt_count가 0이 아닌 대표적인 상황을 정리하면 다음과 같습니다:

상황preempt_count 영향선점 차단 이유
spin_lock() 보유 중PREEMPT 필드 +1공유 자료 구조 보호 (선점되면 다른 태스크가 동일 락 대기 → 교착)
local_bh_disable() 구간SOFTIRQ 필드 +1softirq 핸들러 실행 억제 (하위 반 처리 방지)
hardirq 핸들러 내부HARDIRQ 필드 +1인터럽트 핸들러는 특정 태스크에 속하지 않음 (스케줄 불가)
NMI 핸들러 내부NMI 비트 =1마스크 불가 인터럽트 — 가장 제한적 컨텍스트
유저 모드 선점 vs 커널 모드 선점: 유저 모드로 복귀할 때(syscall 반환, irq_exit)는 preempt_count 검사 없이 need_resched만 확인하면 됩니다. 유저 모드 코드는 커널 자료 구조를 조작하지 않으므로 항상 안전하게 선점할 수 있습니다. preempt_count가 중요한 것은 오직 커널 모드에서 커널 모드로 돌아가야 할 때뿐입니다.

핵심 불변식과 빠른 판독 규칙

preempt_count를 단순히 "atomic context 여부를 나타내는 카운터" 정도로 이해하면 실전에서 자주 막힙니다. 실제로는 서로 다른 종류의 진입 금지 이유를 한 워드에 압축한 상태 벡터에 가깝습니다. 선점 금지, 하위 반 금지, hardirq 진입, NMI 진입은 서로 다른 사건이므로 각기 독립적으로 중첩되어야 하고, 인터럽트 반환이나 preempt_enable() 같은 빠른 경로에서는 이를 거의 분기 없이 판독해야 합니다.

이 설계가 중요한 이유는 스케줄러와 인터럽트 반환 경로가 커널에서 가장 뜨거운 경로이기 때문입니다. 불리언 플래그 여러 개를 따로 읽는 대신 한 번 로드하고 몇 개의 마스크 연산만 수행하면, "지금은 절대 스케줄하면 안 되는가", "아직 softirq가 남아 있는가", "하드 IRQ 문맥인가", "마지막 금지 요인이 막 해제되었는가"를 즉시 판단할 수 있습니다.

  1. 필드는 서로 독립적입니다. PREEMPT, SOFTIRQ, HARDIRQ, NMI는 덮어쓰는 값이 아니라 동시에 살아 있을 수 있는 중첩 축입니다. 예를 들어 spin_lock()을 잡은 코드가 IRQ에 의해 끊기면 PREEMPT와 HARDIRQ가 동시에 비영이 됩니다.
  2. 더 높은 문맥이 더 강한 제약을 뜻합니다. HARDIRQ나 NMI 비트가 살아 있으면 PREEMPT 필드가 0이더라도 스케줄할 수 없습니다. "선점 금지 카운터가 0인가"만 보면 부족하고, 어떤 종류의 문맥인지까지 같이 봐야 합니다.
  3. 전체 워드가 0이 되는 순간이 가장 중요합니다. 이 문서에서 설명하는 빠른 경로는 "모든 금지 이유가 해제되었고 지금 재스케줄이 필요함"을 전체 워드 0으로 표현합니다. 그래서 마지막 preempt_enable() 또는 인터럽트 반환 직후의 한 번 비교가 결정적입니다.
  4. 불균형은 즉시가 아니라 나중에 터집니다. spin_unlock() 하나를 빼먹어도 당장 패닉이 안 날 수 있습니다. 대신 해당 CPU 또는 해당 태스크의 선점이 영구히 늦춰져 수 초 뒤 완전히 다른 위치에서 지연, 락업, BUG: scheduling while atomic로 드러납니다.
  5. IRQ 비활성화와 preempt_count는 별개 축입니다. local_irq_disable()는 CPU 플래그를 바꾸지만 보통 PREEMPT/SOFTIRQ/HARDIRQ 필드를 직접 증가시키지 않습니다. 따라서 preempt_count만 읽고 안전 여부를 완전히 판단할 수는 없고, preemptible()irqs_disabled()를 함께 보는 이유가 여기에 있습니다.
  6. 이 값은 "현재 CPU의 지금 시점"을 설명합니다. 다른 CPU에서 더 높은 우선순위 태스크가 깨어났더라도, 현재 CPU가 락을 쥐고 있거나 IRQ 문맥이면 재스케줄은 연기됩니다. 즉, 깨어난 사실과 실제 문맥 전환 시점은 분리되어 있습니다.
preempt_count 판독은 "상태판 한 장 읽기"와 같다 현재 CPU의 preempt_count 한 번 읽기 한 워드에 PREEMPT / SOFTIRQ / HARDIRQ / NMI / resched 상태가 함께 들어 있음 NMI/HARDIRQ 확인 비영이면 즉시 "인터럽트 문맥" SOFTIRQ 확인 비영이면 softirq 실행 중 또는 BH 비활성화 PREEMPT 확인 비영이면 명시적 선점 금지 또는 락 중첩 상태 IRQ 플래그 확인 irqs_disabled()는 별도 축에서 판단 빠른 경로의 실제 질문 순서 1. NMI/IRQ인가? 그렇다면 스케줄 금지 2. SOFTIRQ/BH 금지인가? 그렇다면 하위 반 처리 우선 3. PREEMPT 중첩이 남았는가? 마지막 unlock/enable을 기다림 4. 모두 해제되었는가? 그때만 선점 검사 핵심은 "어떤 이유로 아직 못 멈추는가"를 한 워드에서 즉시 복원하는 것이다

실전에서 특히 자주 보는 API를 기준으로 어떤 축이 변하는지 정리하면 다음과 같습니다. 아래 표는 이해를 돕기 위한 일반 PREEMPT 커널 기준의 대표 경로이며, PREEMPT_RT처럼 락의 의미가 바뀌는 구성에서는 일부 세부 동작이 달라질 수 있습니다.

이벤트 / API바뀌는 축즉시 의미실전에서 헷갈리는 점
preempt_disable()PREEMPT +1현재 태스크를 커널 내부에서 강제로 빼앗기지 않도록 함IRQ는 여전히 들어올 수 있으므로 "완전히 원자적" 상태와 동일하지는 않습니다.
spin_lock()보통 PREEMPT +1공유 자료 구조 보호와 함께 현재 CPU에서의 선점 방지RT 구성에서는 일반 spinlock이 sleep 가능한 락으로 치환될 수 있어 해석이 달라질 수 있습니다.
spin_lock_irqsave()PREEMPT +1, 로컬 IRQ off현재 CPU에서 선점과 하드 IRQ 진입을 함께 막음preempt_count만 보고는 IRQ off 여부를 복원할 수 없고 저장된 flags가 필요합니다.
local_bh_disable()SOFTIRQ +1softirq와 네트워크 하위 반 실행을 지연시킴in_softirq()가 true가 되어 실제 softirq 핸들러와 혼동되기 쉽습니다.
irq_enter()HARDIRQ +1이후 코드는 hardirq 문맥으로 해석되어 sleep이 금지됨중단된 원래 태스크의 PREEMPT 값이 있었다면 HARDIRQ 위에 겹쳐 보입니다.
irq_exit()HARDIRQ -1하드 IRQ 문맥에서 빠져나오며 softirq 또는 선점 검사를 수행리턴 직후 바로 스케줄되지 않을 수 있으며, 아직 PREEMPT 필드가 남아 있으면 계속 지연됩니다.
nmi_enter()NMI 비트 설정가장 강한 제한 문맥으로 진입NMI는 일반 IRQ 마스크와 별개이므로 "IRQ를 껐으니 안전"이라는 직관이 통하지 않습니다.
local_irq_disable()preempt_count 변화 없음CPU 플래그로 하드 IRQ만 잠시 차단in_interrupt()는 false일 수 있습니다. 즉, 인터럽트 문맥과 IRQ-off 구간은 다른 개념입니다.
헷갈리기 쉬운 점: local_irq_disable()는 인터럽트를 끌 뿐 preempt_count에 hardirq 문맥이 들어왔다는 표시를 남기지 않습니다. 반대로 irq_enter()는 실제 인터럽트 핸들러 진입이므로 HARDIRQ 필드가 올라갑니다. "IRQ를 못 받는 상태"와 "이미 IRQ 문맥 안에 있는 상태"는 비슷해 보여도 커널은 분명히 구분합니다.

preempt_count 비트 필드 구조

preempt_count는 32비트를 여러 영역으로 나누어 각각 독립적인 의미를 부여합니다. 아래 다이어그램은 x86_64 기준 비트 레이아웃입니다.

preempt_count 32비트 필드 레이아웃 bit 31 RESCHED bits 22-30 Reserved bit 21 LAZY bit 20 NMI bits 16-19 HARDIRQ (4 bits) bits 8-15 SOFTIRQ (8 bits) bits 0-7 PREEMPT (8 bits) TIF_NEED_RESCHED 비트 반전 저장 PREEMPT_LAZY용 (커널 6.12+) NMI 컨텍스트 중첩 불가 하드 IRQ 중첩 횟수 irq_enter/exit 조작 local_bh_disable 횟수 softirq 처리 중 증가 preempt_disable 횟수 spin_lock 등으로 증가 비트 마스크 상수 (include/linux/preempt.h) PREEMPT_MASK 0x000000ff 비트 0-7 : 선점 비활성화 깊이 (최대 255 중첩) SOFTIRQ_MASK 0x0000ff00 비트 8-15 : softirq/BH 비활성화 깊이 HARDIRQ_MASK 0x000f0000 비트 16-19 : hardirq 중첩 카운트 (최대 15) NMI_MASK 0x00100000 비트 20 : NMI 컨텍스트 (0 또는 1) PREEMPT_NEED_RESCHED 0x80000000 비트 31 : 리스케줄 필요 (반전 저장: 0 = 필요)
/* include/linux/preempt.h — 비트 필드 정의 */
#define PREEMPT_BITS      8
#define SOFTIRQ_BITS      8
#define HARDIRQ_BITS      4
#define NMI_BITS          1

#define PREEMPT_SHIFT     0
#define SOFTIRQ_SHIFT     (PREEMPT_SHIFT + PREEMPT_BITS)    /* 8 */
#define HARDIRQ_SHIFT     (SOFTIRQ_SHIFT + SOFTIRQ_BITS)    /* 16 */
#define NMI_SHIFT         (HARDIRQ_SHIFT + HARDIRQ_BITS)    /* 20 */

#define PREEMPT_MASK      ((1UL << PREEMPT_BITS) - 1)       /* 0x000000ff */
#define SOFTIRQ_MASK      ((1UL << SOFTIRQ_BITS) - 1) << SOFTIRQ_SHIFT
#define HARDIRQ_MASK      ((1UL << HARDIRQ_BITS) - 1) << HARDIRQ_SHIFT
#define NMI_MASK          (1UL << NMI_SHIFT)

/* 비트 31: PREEMPT_NEED_RESCHED — 반전 저장 최적화 */
/*
 * preempt_count 테스트와 need_resched 테스트를 단일 비교로 합침:
 *   preempt_count() == 0 이면
 *     → 모든 필드가 0이고 bit 31도 0(=need_resched 설정됨)
 *     → 하나의 "test %eax, %eax" 명령으로 선점 가능 여부 판단
 */
#define PREEMPT_NEED_RESCHED  0x80000000
bit 31 반전 저장 최적화: preempt_count 전체가 정확히 0인지만 검사하면 "선점 가능 + 리스케줄 필요"를 한 번에 판단할 수 있습니다. bit 31이 0이면 TIF_NEED_RESCHED가 설정된 상태이므로, test %reg, %reg; jz preempt_schedule 단 두 명령으로 최적 경로가 완성됩니다.

실제 preempt_count 값 예시

아래 다이어그램은 다양한 실행 컨텍스트에서 preempt_count의 실제 비트 값과 16진수 표현을 보여줍니다. 각 상황에서 어떤 비트 영역이 활성화되는지 직관적으로 확인할 수 있습니다.

컨텍스트별 preempt_count 실제 값 컨텍스트 RESCHED LAZY NMI HARDIRQ SOFTIRQ PREEMPT 16진수 선점 가능 + resched 0 0 0 0000 00000000 00000000 0x00000000 프로세스 (resched 없음) 1 0 0 0000 00000000 00000000 0x80000000 spin_lock ×1 1 0 0 0000 00000000 00000001 0x80000001 spin_lock ×2 + bh_disable 1 0 0 0000 00000011 00000010 0x80000302 hardirq 핸들러 (1단계) 1 0 0 0001 00000000 00000000 0x80010000 hardirq + spin_lock 1 0 0 0001 00000000 00000001 0x80010001 softirq 핸들러 실행 중 1 0 0 0000 00000001 00000000 0x80000100 NMI 핸들러 1 0 1 0000 00000000 00000000 0x80100000 값 해석 가이드 0x00000000 = 모든 비트 0 → 선점 가능 + 리스케줄 필요 (bit 31=0은 NEED_RESCHED 설정을 의미) 0x80000000 = bit 31만 1 → 선점 가능하나 리스케줄 불필요 (NEED_RESCHED 미설정) 0x80000001 = PREEMPT 필드=1 → preempt_disable() 또는 spin_lock() 1회 호출 상태 0x80000302 = SOFTIRQ=0x03(local_bh_disable ×1 + 추가), PREEMPT=0x02(spin_lock ×2) 0x80010000 = HARDIRQ=0x1 → irq_enter()로 진입한 hardirq 핸들러 1단계 0x80100000 = NMI 비트=1 → NMI 핸들러 내부 (가장 제한적 컨텍스트)

컨텍스트 판별 매크로

커널 코드는 현재 실행 컨텍스트를 확인하기 위해 preempt_count의 비트 필드를 검사하는 매크로를 제공합니다. 올바른 매크로 선택은 동기화 전략과 메모리 할당 플래그 결정에 직접적인 영향을 미칩니다.

/* include/linux/preempt.h — 컨텍스트 판별 매크로 */

/* 하위 카운터 추출 */
#define preempt_count()   (current_thread_info()->preempt_count)
#define hardirq_count()   (preempt_count() & HARDIRQ_MASK)
#define softirq_count()   (preempt_count() & SOFTIRQ_MASK)
#define irq_count()       (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK))

/* 컨텍스트 판별 */
#define in_irq()          (hardirq_count())          /* 하드 IRQ 핸들러 내부 */
#define in_hardirq()      (hardirq_count())          /* in_irq()와 동일 */
#define in_softirq()      (softirq_count())          /* softirq 또는 BH disabled */
#define in_interrupt()    (irq_count())              /* hardirq | softirq | NMI */
#define in_nmi()          (preempt_count() & NMI_MASK)  /* NMI 핸들러 내부 */

/* 선점 가능 여부 */
#define preemptible()     (preempt_count() == 0 && !irqs_disabled())

/* in_serving_softirq() — 실제 softirq 핸들러 내부만 true */
#define in_serving_softirq()  (softirq_count() & SOFTIRQ_OFFSET)

/* in_atomic() — 모든 atomic 컨텍스트 (주의: CONFIG_PREEMPT_COUNT 필요) */
#define in_atomic()       (preempt_count() != 0)
in_softirq() vs in_serving_softirq()

in_softirq()local_bh_disable() 구간에서도 true를 반환합니다. 실제 softirq 핸들러 내부인지 확인하려면 in_serving_softirq()를 사용해야 합니다. 네트워크 드라이버에서 이 차이를 간과하면 동기화 로직에서 미묘한 버그가 발생합니다.

아래 표는 각 컨텍스트에서 매크로 반환값과 허용되는 동작을 정리합니다:

컨텍스트in_irqin_softirqin_interruptin_nmiin_atomicsleep 가능
프로세스 (선점 가능)00000O
preempt_disable 구간00001X
local_bh_disable 구간01101X
softirq 핸들러01101X
hardirq 핸들러10101X
NMI 핸들러00111X
in_atomic()의 함정

in_atomic()CONFIG_PREEMPT_COUNT가 활성화된 커널에서만 정확합니다. CONFIG_PREEMPT_NONE 커널에서는 preempt_count가 IRQ/NMI 필드만 추적하므로, spin_lock() 구간에서도 in_atomic()이 false를 반환할 수 있습니다. 드라이버에서 in_atomic()을 기반으로 할당 플래그를 선택하는 것은 권장되지 않으며, 설계 시점에서 컨텍스트를 명확히 아는 것이 올바른 접근입니다.

컨텍스트 판별 매크로 결정 트리: "현재 어떤 컨텍스트인가?" preempt_count() 읽기 NMI_MASK & 0x00100000 ? ≠ 0 NMI 컨텍스트 in_nmi() → true == 0 HARDIRQ_MASK & 0x000f0000 ? ≠ 0 Hardirq 컨텍스트 in_irq() → true == 0 SOFTIRQ_MASK & 0x0000ff00 ? ≠ 0 Softirq / BH off in_softirq() → true == 0 PREEMPT_MASK & 0x000000ff ? ≠ 0 선점 비활성화 in_atomic() → true == 0 프로세스 컨텍스트 preemptible() → true (IRQ 활성 시) in_interrupt() → true NMI, Hardirq, Softirq 모두 포함 (= irq_count() ≠ 0) sleep 절대 금지 mutex_lock 금지 schedule() 금지 GFP_ATOMIC 사용 spin_lock 사용 가능 per-cpu 접근 안전 (단, hardirq에서 spin_lock_bh 금지) in_interrupt()가 true이면 sleep 가능 함수(kmalloc GFP_KERNEL, mutex, copy_from_user 등) 호출 금지

매크로 사용 시 주의사항과 실전 패턴

컨텍스트 판별 매크로를 올바르게 사용하려면 몇 가지 미묘한 차이를 이해해야 합니다:

/* 패턴 1: 메모리 할당 플래그 선택 (권장되지 않지만 현실적) */
void *device_alloc_buffer(size_t size)
{
    /* in_interrupt()를 사용 — NMI/hardirq/softirq 모두 커버 */
    if (in_interrupt())
        return kmalloc(size, GFP_ATOMIC);
    return kmalloc(size, GFP_KERNEL);
}

/* 패턴 2: 네트워크 드라이버에서 BH 컨텍스트 구분 */
void netdev_tx_handler(struct sk_buff *skb)
{
    if (in_serving_softirq()) {
        /* 실제 softirq 핸들러에서 호출됨 — NAPI poll 등 */
        process_in_napi_context(skb);
    } else if (in_softirq()) {
        /* local_bh_disable 구간 — 프로세스 컨텍스트에서 BH 비활성화 */
        process_with_bh_disabled(skb);
    } else {
        /* 일반 프로세스 컨텍스트 */
        process_in_process_context(skb);
    }
}

/* 패턴 3: RCU 콜백에서 컨텍스트 확인 */
void rcu_callback(struct rcu_head *head)
{
    /* RCU 콜백은 softirq 컨텍스트에서 실행됨 */
    WARN_ON_ONCE(!in_serving_softirq());
    /* ... */
}

preempt_disable/enable 메커니즘

preempt_disable()preempt_enable()preempt_count의 PREEMPT 필드(비트 0-7)를 조작하여 현재 CPU에서 커널 선점을 임시로 금지합니다. 중첩 호출이 가능하며, 카운터가 정확히 0으로 돌아올 때만 선점이 재개됩니다.

/* include/linux/preempt.h — preempt_disable/enable 구현 */
#define preempt_disable() \
    do { \
        preempt_count_inc(); \
        barrier(); \
    } while (0)

#define preempt_enable() \
    do { \
        barrier(); \
        if (unlikely(preempt_count_dec_and_test())) \
            __preempt_schedule(); \
    } while (0)

/* preempt_enable_no_resched() — 선점 검사 생략 변형 */
#define preempt_enable_no_resched() \
    do { \
        barrier(); \
        preempt_count_dec(); \
    } while (0)

/* __preempt_schedule — 커널 선점 진입점 */
asmlinkage void __preempt_schedule(void)
{
    if (likely(!preemptible()))
        return;
    do {
        preempt_disable();
        __schedule(SM_PREEMPT);
        preempt_enable_no_resched();
    } while (need_resched());
}

barrier()는 컴파일러 배리어로, preempt_count 변경이 임계 구역 코드와 재정렬되지 않도록 합니다. preempt_disable에서는 카운트 증가 에 배리어를, preempt_enable에서는 배리어 에 카운트 감소를 수행하여 임계 구역 코드가 반드시 보호 범위 안에 위치하도록 보장합니다.

배리어 순서가 왜 중요한지 다음 예시로 설명합니다:

/* barrier()가 없다면? — 컴파일러가 재정렬할 수 있는 위험 */

/* 의도한 순서: */
preempt_count_inc();       /* ① 보호 시작 */
shared_data++;               /* ② 임계 구역 코드 */
preempt_count_dec();       /* ③ 보호 종료 */

/* 컴파일러가 최적화로 재정렬할 수 있는 순서: */
shared_data++;               /* ② 보호되지 않은 상태에서 실행! */
preempt_count_inc();       /* ① */
preempt_count_dec();       /* ③ */
/* → 임계 구역 코드가 보호 범위 바깥으로 이동 — 데이터 경합 발생! */

/* barrier()를 넣으면 컴파일러가 barrier() 너머로 재정렬 불가 */
preempt_count_inc();  /* ① */
barrier();              /* ═══ 컴파일러 펜스 ═══ */
shared_data++;          /* ② 여기서부터는 ① 이후 보장 */
barrier();              /* ═══ 컴파일러 펜스 ═══ */
preempt_count_dec();  /* ③ 여기는 ② 이후 보장 */
preempt_count 중첩 변화 타임라인 시간 preempt _count 0 1 2 +BH spin_lock(&a) spin_lock(&b) bh_disable() bh_enable() spin_unlock(&b) spin_unlock(&a) 선점 검사! 0 1 2 2+BH 2 1 0 선점 불가 구간 (preempt_count ≠ 0)
/* 위 타임라인에 해당하는 코드 */
spin_lock(&lock_a);         /* preempt_count: 0 → 1 (PREEMPT 필드 +1) */
  spin_lock(&lock_b);       /* preempt_count: 1 → 2 (PREEMPT 필드 +1, 중첩) */
    local_bh_disable();     /* preempt_count: 2 → 0x200+2 (SOFTIRQ 필드 증가) */
    /* 여기서 preempt_count = 0x80000202
     *   PREEMPT=2 (spinlock ×2) + SOFTIRQ=0x200 (bh_disable ×1)
     *   in_softirq()=true, in_atomic()=true, in_irq()=false */
    local_bh_enable();      /* preempt_count: SOFTIRQ 필드 감소 → 2 */
  spin_unlock(&lock_b);     /* preempt_count: 2 → 1 (아직 ≠0이므로 선점 검사 안 함) */
spin_unlock(&lock_a);       /* preempt_count: 1 → 0 → preempt_count_dec_and_test() = true */
                             /* → need_resched 확인 → 설정되어 있으면 __preempt_schedule() */

preempt_enable의 변형들

커널은 상황에 따라 preempt_enable의 여러 변형을 제공합니다:

/* 1. preempt_enable() — 표준 버전: 카운트 감소 + 선점 검사 */
preempt_enable();

/* 2. preempt_enable_no_resched() — 카운트만 감소, 선점 검사 안 함 */
/*    용도: 스케줄러 내부에서 이미 reschedule을 다루는 경우 */
preempt_enable_no_resched();

/* 3. preempt_enable_notrace() — ftrace에서 추적하지 않는 버전 */
/*    용도: ftrace 자체 구현 내부에서 무한 재귀 방지 */
preempt_enable_notrace();

/* 4. preempt_count_dec() — 카운트만 감소 (배리어, 선점 검사 모두 없음) */
/*    용도: preempt_enable_no_resched() 내부 구현 */
preempt_count_dec();

/* 5. preempt_disable_notrace() / preempt_enable_notrace() */
/*    용도: ftrace, BPF, perf 등 트레이싱 인프라 내부 */
preempt_disable_notrace();
/* ... 트레이싱 코드 ... */
preempt_enable_notrace();
spin_lock과의 관계: spin_lock()은 내부적으로 preempt_disable()을 호출합니다. UP(단일 프로세서) 커널에서는 실제 스핀 대기 없이 preempt_disable()만 수행되어 임계 구역을 보호합니다. SMP 커널에서도 선점 비활성화는 spinlock의 필수 전제 조건입니다 — 선점이 가능한 상태에서 spinlock을 보유하면, 선점된 태스크가 동일 CPU에서 다시 그 lock을 잡으려 할 때 교착 상태가 됩니다.
preempt_disable/enable 규칙
  • 반드시 쌍으로 사용: disable/enable 불일치는 선점 카운터 언더플로우/오버플로우로 이어지며, CONFIG_DEBUG_PREEMPT가 이를 감지합니다.
  • enable은 잠들 수 있음: preempt_enable()은 카운터가 0이 되면 schedule()을 호출할 수 있으므로, IRQ disabled 상태에서 호출하면 안 됩니다. IRQ가 꺼진 채 schedule하면 데드락입니다.
  • 중첩 순서 무관: preempt_count는 단순 카운터이므로 disable A→disable B→enable A→enable B도 유효합니다(카운터 2→1→0). 다만 코드 가독성을 위해 LIFO 순서를 권장합니다.

선점 모델 (NONE / VOLUNTARY / FULL / RT)

Linux 커널은 빌드 시 선점 모델을 선택할 수 있으며, 이 선택은 preempt_count의 활용 범위와 선점 검사 빈도에 직접 영향을 미칩니다.

커널 선점 모델 비교 모델 선점 시점 preempt_count 역할 용도 CONFIG_PREEMPT_NONE (서버 최적화) 커널→유저 전환 시만 (syscall/IRQ 반환) IRQ/NMI 필드만 활성 PREEMPT 필드 미사용 처리량(throughput) 극대화 서버, HPC, 배치 처리 CONFIG_PREEMPT_VOLUNTARY (데스크톱 기본값) NONE + cond_resched() 명시적 양보 지점 NONE과 동일 + might_resched() 검사점 처리량-지연시간 균형 데스크톱, 범용 서버 CONFIG_PREEMPT (Full Preemption) preempt_count == 0인 모든 커널 코드 지점 모든 필드 완전 활성 spin_unlock마다 선점 검사 낮은 지연시간(latency) 임베디드, 오디오, 게임 CONFIG_PREEMPT_RT (Real-Time) FULL + spinlock을 rt_mutex로 대체 모든 필드 활성 + 임계구역도 선점 가능 결정론적 지연시간 산업제어, 로봇, 의료기기 지연시간 감소 처리량 감소
/* cond_resched() — PREEMPT_VOLUNTARY의 명시적 양보 지점 */
static inline int _cond_resched(void)
{
    if (should_resched(0)) {
        preempt_schedule_common();
        return 1;
    }
    return 0;
}

/* PREEMPT_NONE에서는 cond_resched가 nop으로 최적화됨 */
/* PREEMPT(FULL)에서도 cond_resched는 nop — 이미 모든 지점에서 선점 가능 */
/* PREEMPT_VOLUNTARY에서만 실제 리스케줄 검사 수행 */
PREEMPT_RT에서의 spinlock 변환: CONFIG_PREEMPT_RT에서 일반 spinlock_trt_mutex 기반의 sleeping lock으로 변환됩니다. 따라서 spin_lock() 구간에서도 선점이 가능해지며, preempt_count의 PREEMPT 필드가 증가하지 않습니다. 하드 IRQ 컨텍스트에서 반드시 원래의 스핀 동작이 필요한 경우 raw_spinlock_t를 사용해야 합니다.

선점 모델별 동작 타임라인

동일한 커널 코드가 실행될 때 선점 모델에 따라 선점 발생 시점이 어떻게 달라지는지 아래 타임라인으로 비교합니다. 높은 우선순위 태스크(RT)가 깨어나는 시점을 기준으로, 실제 CPU를 획득하기까지의 지연 시간이 핵심 차이입니다.

선점 모델별 RT 태스크 스케줄 지연 비교 시나리오: 낮은 우선순위 태스크가 커널 코드(spin_lock 구간 포함) 실행 중에 높은 우선순위 태스크가 깨어남 RT 태스크 wakeup spin_unlock() cond_resched() syscall 반환 PREEMPT_NONE 낮은 우선순위 태스크 계속 실행 (선점 안 됨) RT 실행 긴 지연 (ms ~ 수십 ms) VOLUNTARY 낮은 우선순위 태스크 실행 RT 태스크 실행 중간 지연 (cond_resched 도달까지) PREEMPT (FULL) spin_lock 구간 RT 태스크 실행 짧은 지연 (spin_unlock까지) PREEMPT_RT rt_mutex RT 태스크 즉시 실행 (spinlock이 sleeping lock이므로 선점 가능) 최소 지연 (거의 즉시) 모델 선택 가이드 NONE — 서버, HPC, 배치: 최대 처리량, 최대 지연. cond_resched도 nop. VOLUNTARY — 범용 데스크톱: 처리량-지연 균형. 긴 경로에 cond_resched() 삽입. FULL — 저지연 데스크톱, 오디오/게임: spin_unlock마다 선점 검사. 약간의 처리량 감소. RT — 산업제어, 의료기기, 로봇: 결정론적 지연. spinlock→rt_mutex, 처리량 10~30% 감소.

PREEMPT_DYNAMIC (런타임 선점 모델 전환)

커널 6.x부터 도입된 CONFIG_PREEMPT_DYNAMIC은 단일 커널 바이너리에서 부팅 시 선점 모델을 전환할 수 있게 합니다. 배포판이 하나의 커널로 서버(NONE)부터 데스크톱(FULL)까지 지원할 수 있어, 별도의 커널 빌드 없이 preempt= 부팅 파라미터로 모델을 선택합니다.

/* kernel/sched/core.c — PREEMPT_DYNAMIC 구현 핵심 */

/* 정적 호출(static call)로 런타임 디스패치 비용 제거 */
DEFINE_STATIC_CALL(preempt_schedule, __preempt_schedule_func);
DEFINE_STATIC_CALL(preempt_schedule_notrace, __preempt_schedule_notrace_func);
DEFINE_STATIC_CALL(cond_resched, __cond_resched);
DEFINE_STATIC_CALL(might_resched, __might_resched);

/* 부팅 파라미터에 따라 static call 대상 교체 */
void sched_dynamic_update(int mode)
{
    switch (mode) {
    case preempt_dynamic_none:
        static_call_update(cond_resched, __cond_resched);
        static_call_update(might_resched, __static_call_return0);
        static_call_update(preempt_schedule, NULL);
        static_call_update(preempt_schedule_notrace, NULL);
        break;
    case preempt_dynamic_voluntary:
        static_call_update(cond_resched, __cond_resched);
        static_call_update(might_resched, __might_resched);
        static_call_update(preempt_schedule, NULL);
        static_call_update(preempt_schedule_notrace, NULL);
        break;
    case preempt_dynamic_full:
        static_call_update(cond_resched, __static_call_return0);
        static_call_update(might_resched, __static_call_return0);
        static_call_update(preempt_schedule, __preempt_schedule_func);
        static_call_update(preempt_schedule_notrace, __preempt_schedule_notrace_func);
        break;
    }
}
# 부팅 파라미터로 선점 모델 선택
# /etc/default/grub 또는 커널 커맨드 라인
preempt=none        # 서버 최적화
preempt=voluntary   # 데스크톱 (기본값)
preempt=full        # 저지연

# 현재 모델 확인
cat /sys/kernel/debug/sched/preempt
# 출력 예: "full (dynamic)"
Static Call 메커니즘: PREEMPT_DYNAMICstatic_call을 사용하여 간접 호출(function pointer) 대신 직접 호출 명령어를 런타임에 패치합니다. 따라서 모드 전환 후에도 호출 오버헤드가 없으며, PREEMPT_NONE 모드에서 preempt_schedule() 호출 자체가 NOP로 대체됩니다. x86에서는 call 명령을 nop으로, ARM64에서는 blnop으로 바꿉니다.
PREEMPT_DYNAMIC: static_call 패칭 메커니즘 부팅 시 preempt= 파라미터에 따라 호출 대상이 NOP 또는 실제 함수로 패치됨 preempt=none preempt_schedule() → NOP (5 bytes) cond_resched() → __cond_resched might_resched() → return 0 (NOP) spin_unlock 시: 선점 검사 없음 (call이 NOP이므로) 최대 처리량 선점 경로 오버헤드 = 0 preempt=voluntary preempt_schedule() → NOP (5 bytes) cond_resched() → __cond_resched might_resched() → __might_resched spin_unlock 시: 선점 검사 없음 cond_resched() 지점에서만 처리량-지연 균형 긴 경로에 양보 지점 제공 preempt=full preempt_schedule() → __preempt_schedule cond_resched() → return 0 (NOP) might_resched() → return 0 (NOP) spin_unlock 시: 즉시 선점 검사! preempt_count==0이면 schedule 최소 지연 모든 선점 가능 지점에서 검사
/* x86에서 static_call이 실제로 패치하는 기계어 */

/* preempt=full 모드에서 spin_unlock 경로의 기계어: */
/*   e8 xx xx xx xx    call __preempt_schedule   ← 5바이트 직접 호출 */

/* preempt=none 모드로 전환하면: */
/*   0f 1f 44 00 00    nopl 0x0(%rax,%rax,1)      ← 5바이트 NOP로 패치 */

/* 패치는 text_poke_bp()를 통해 안전하게 수행됨:
 * 1. INT3(0xcc) 삽입 → 다른 CPU가 실행하면 INT3 핸들러에서 대기
 * 2. 나머지 4바이트 패치
 * 3. 첫 바이트를 최종 opcode로 교체
 * 4. IPI로 모든 CPU의 명령어 캐시 동기화
 */
PREEMPT_DYNAMIC의 제약

PREEMPT_DYNAMIC은 NONE↔VOLUNTARY↔FULL 사이만 전환 가능합니다. PREEMPT_RT로의 전환은 지원하지 않습니다 — RT 모드는 spinlock을 rt_mutex로 대체하는 등 코드 구조 자체가 다르므로 바이너리 패칭으로는 불가능합니다. 또한 현재는 부팅 시에만 전환 가능하며, 런타임 동적 전환은 아직 지원하지 않습니다.

local_bh_disable/enable과 preempt_count

local_bh_disable()/local_bh_enable()preempt_count의 SOFTIRQ 필드(비트 8-15)를 조작하여 현재 CPU에서 softirq 실행을 억제합니다. 이 API와 preempt_count의 관계를 이해하는 것은 네트워크 스택 등에서 올바른 동기화를 구현하는 데 필수적입니다.

/* kernel/softirq.c — local_bh_disable/enable 구현 */
void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
{
    /* preempt_count에 SOFTIRQ_DISABLE_OFFSET(0x200) 추가 */
    __preempt_count_add(cnt);
    barrier();
}

void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
    __preempt_count_sub(cnt);

    /* BH 활성화 시점에 pending softirq가 있으면 즉시 처리 */
    if (unlikely(!in_interrupt() && local_softirq_pending())) {
        do_softirq();
    }
    preempt_check_resched();
}

SOFTIRQ 필드와 SOFTIRQ_OFFSET의 비밀

local_bh_disable()은 SOFTIRQ 필드에 SOFTIRQ_DISABLE_OFFSET(0x200)을 더합니다. 그런데 softirq 핸들러가 실행될 때는 SOFTIRQ_OFFSET(0x100)만 더합니다. 이 차이가 in_serving_softirq()의 구현 핵심입니다:

/* include/linux/preempt.h */
#define SOFTIRQ_OFFSET          (1 << SOFTIRQ_SHIFT)     /* 0x100 — bit 8 */
#define SOFTIRQ_DISABLE_OFFSET  (2 * SOFTIRQ_OFFSET)    /* 0x200 — bit 9 */

/* local_bh_disable(): SOFTIRQ 필드에 0x200 추가
 *   → bit 9가 설정됨
 *   → in_softirq() = true (SOFTIRQ_MASK & preempt_count ≠ 0)
 *   → in_serving_softirq() = false (bit 8은 0)
 */

/* __do_softirq() 진입 시: SOFTIRQ 필드에 0x100 추가
 *   → bit 8이 설정됨
 *   → in_softirq() = true
 *   → in_serving_softirq() = true (bit 8 ≠ 0)
 *
 * in_serving_softirq() = softirq_count() & SOFTIRQ_OFFSET
 *   = preempt_count & 0x0000ff00 & 0x00000100
 *   → bit 8만 검사!
 */

/* 조합 시나리오: local_bh_disable 구간 안에서 softirq가 실행되면? */
/* → 불가능: local_bh_disable은 softirq 실행을 억제하므로,
 *    local_bh_enable() 시점에서 pending softirq를 처리합니다.
 *    이때 SOFTIRQ 필드 = 0x200 + 0x100 = 0x300이 됩니다. */
SOFTIRQ 필드의 비트별 역할 bit 15 bh_disable ×64 bit 14-10 bh_disable 카운트 bit 9 BH disabled bit 8 serving softirq SOFTIRQ_MASK = 0x0000ff00 (bits 8-15) in_softirq() = SOFTIRQ_MASK 내 어떤 비트든 설정 시나리오 A: local_bh_disable() SOFTIRQ 필드 += 0x200 bit 9 = 1, bit 8 = 0 in_softirq() → true in_serving_softirq() → false (프로세스 컨텍스트에서 BH 억제) 시나리오 B: __do_softirq() SOFTIRQ 필드 += 0x100 bit 9 = 0, bit 8 = 1 in_softirq() → true in_serving_softirq() → true (softirq 핸들러 실행 중) spin_lock_bh() = spin_lock() + local_bh_disable() preempt_count 변화: +1(PREEMPT) + 0x200(SOFTIRQ) = 0x80000201 프로세스 컨텍스트↔softirq 핸들러 간 공유 데이터 보호에 사용 softirq가 동일 CPU에서 실행되면 spin_lock만으로 보호 불가 → BH도 비활성화 필요

자세한 사용 패턴과 spin_lock_bh() 조합은 softirq/hardirq 문서의 local_bh_disable/enable 섹션을 참조하세요.

might_sleep / in_atomic 심화

might_sleep()는 디버그 빌드(CONFIG_DEBUG_ATOMIC_SLEEP)에서 현재 컨텍스트가 sleep 불가능한 atomic 컨텍스트인지 검사하고, 위반 시 경고를 출력합니다. 많은 커널 API(kmalloc(GFP_KERNEL), mutex_lock, copy_from_user 등) 내부에 이미 삽입되어 있습니다.

/* include/linux/kernel.h — might_sleep 매크로 */
#ifdef CONFIG_DEBUG_ATOMIC_SLEEP
#define might_sleep() \
    do { __might_sleep(__FILE__, __LINE__); } while (0)
#else
#define might_sleep() do { } while (0)
#endif

/* kernel/sched/core.c — __might_sleep 구현 */
void __might_sleep(const char *file, int line)
{
    if (preempt_count() || irqs_disabled()) {
        printk(KERN_ERR
            "BUG: sleeping function called from invalid context "
            "at %s:%d\n", file, line);
        printk(KERN_ERR
            "in_atomic(): %d, irqs_disabled(): %d, "
            "non_block: 0, pid: %d, name: %s\n",
            in_atomic(), irqs_disabled(),
            current->pid, current->comm);
        dump_stack();
    }
}
/* sleep 가능 여부 동적 판단 패턴 (권장하지 않음) */
void *my_alloc(size_t size)
{
    if (in_atomic() || irqs_disabled())
        return kmalloc(size, GFP_ATOMIC);
    return kmalloc(size, GFP_KERNEL);
}

/* 커스텀 sleep 가능 함수에 might_sleep() 추가 (권장) */
void my_blocking_function(void)
{
    might_sleep();  /* atomic 컨텍스트에서 호출 시 경고 */
    mutex_lock(&some_mutex);
    /* ... */
    mutex_unlock(&some_mutex);
}
in_atomic() 기반 분기의 문제점

in_atomic()으로 할당 플래그를 동적으로 결정하는 패턴(GFP_ATOMIC vs GFP_KERNEL)은 커널 커뮤니티에서 안티패턴으로 간주됩니다. CONFIG_PREEMPT_NONE 커널에서 in_atomic()spin_lock 구간을 감지하지 못하기 때문입니다. 올바른 접근은 함수 설계 시점에서 호출 컨텍스트를 명확히 정의하고, gfp_t 플래그를 매개변수로 전달받는 것입니다.

"이 함수에서 sleep할 수 있는가?" — might_sleep 판단 흐름 might_sleep() 호출 irqs_disabled() ? Yes BUG: sleep 불가! IRQ disabled → deadlock No in_interrupt() ? Yes BUG: sleep 불가! IRQ/softirq 컨텍스트 No preempt_count() & PREEMPT_MASK ≠ 0 ? Yes BUG: sleep 불가! preempt_disable 구간 No sleep 가능! 프로세스 컨텍스트 + IRQ 활성 + 선점 가능 CONFIG_PREEMPT_NONE에서: PREEMPT_MASK 검사 불완전! spin_lock 구간을 감지 못함 → might_sleep이 false negative CONFIG_DEBUG_ATOMIC_SLEEP 필수 sleep 가능 API (might_sleep 포함) kmalloc(GFP_KERNEL), mutex_lock, wait_event, copy_from_user, msleep, schedule, down atomic 컨텍스트 전용 API kmalloc(GFP_ATOMIC), spin_lock, kfree, tasklet_schedule, mod_timer, printk

might_sleep의 실전 활용: 디버그 경고 분석

CONFIG_DEBUG_ATOMIC_SLEEP이 활성화된 커널에서 atomic 컨텍스트 위반이 발생하면 다음과 같은 경고가 출력됩니다:

BUG: sleeping function called from invalid context at mm/page_alloc.c:5234
in_atomic(): 1, irqs_disabled(): 0, non_block: 0, pid: 1234, name: my_driver
preempt_count: 1 (depth: 1)
CPU: 3 PID: 1234 Comm: my_driver Tainted: G        W          6.8.0 #1
Call Trace:
 <TASK>
 dump_stack_lvl+0x48/0x70
 __might_resched+0x166/0x1e0
 __alloc_pages+0x188/0x340          <— 이 함수가 might_sleep() 호출
 kmalloc_large+0x2d/0x90
 __kmalloc+0xb8/0x120               <— kmalloc(GFP_KERNEL) 호출 지점
 my_driver_work+0x56/0x100 [my_mod] <— 문제의 드라이버 함수
 process_one_work+0x1d6/0x3e0       <— workqueue에서 호출됨

Preemption disabled at:
 [<ffffffff81234567>] _raw_spin_lock+0x1c/0x30     <— 여기서 preempt_disable됨!
 </TASK>

위 경고는 spin_lock()으로 선점이 비활성화된 상태에서 kmalloc(GFP_KERNEL)을 호출했음을 알려줍니다. 해결 방법: GFP_ATOMIC으로 변경하거나, 할당을 lock 바깥으로 이동시킵니다.

Per-CPU 저장과 접근

preempt_count는 태스크별이 아닌 Per-CPU 변수로 저장됩니다(아키텍처에 따라 thread_info 내부 또는 Per-CPU 영역). 이 설계는 현재 CPU의 실행 컨텍스트를 빠르게 읽을 수 있게 하며, 캐시 라인 경합 없이 접근이 가능합니다.

/* x86: Per-CPU 변수로 직접 접근 (커널 5.x+) */
DECLARE_PER_CPU(int, __preempt_count);

static __always_inline int preempt_count(void)
{
    return raw_cpu_read_4(__preempt_count);
}

static __always_inline void preempt_count_set(int pc)
{
    raw_cpu_write_4(__preempt_count, pc);
}

/* x86에서는 GS 세그먼트를 통한 단일 명령어 접근 */
/*   mov %gs:__preempt_count, %eax  */
/* 추가 락이나 원자적 연산 불필요 — 현재 CPU만 접근 */

/* ARM64: thread_info 내부 저장 */
struct thread_info {
    unsigned long   flags;
    int             preempt_count;  /* 0 → 선점 가능, <0 → 버그 */
    /* ... */
};

/* ARM64에서는 sp_el0 레지스터로 current_thread_info() 접근 */
/*   ldr w0, [x28, #TI_PREEMPT]   (x28 = current_thread_info) */
왜 Per-CPU인가? preempt_count는 인터럽트 핸들러(irq_enter/irq_exit)에서도 조작되므로, 락 없이 접근해야 합니다. Per-CPU 변수는 현재 CPU만 쓰기 때문에 원자적 연산이 불필요하고, x86에서는 GS 세그먼트를 통한 단일 명령어로 접근할 수 있어 오버헤드가 거의 없습니다.
아키텍처별 preempt_count 접근 경로 x86_64 GS 세그먼트 레지스터 기준 주소 Per-CPU 데이터 영역 (CPU N) current_task (struct task_struct *) __preempt_count (int) ← 이것! cpu_number, irq_count, ... 생성되는 기계어 (1 명령어!) mov %gs:__preempt_count, %eax GS 세그먼트 오프셋으로 직접 읽기 — 1 cycle 장점 단일 명령어 접근, 포인터 역참조 없음 캐시 미스 가능성 낮음 (항상 같은 CPU 접근) ARM64 (AArch64) SP_EL0 레지스터 → task_struct task_struct (현재 태스크) sp_el0 = current (커널 모드에서 재사용) 오프셋 thread_info (task_struct 내장) preempt_count (int) ← 이것! 생성되는 기계어 (2 명령어) mrs x0, sp_el0 ldr w1, [x0, #TI_PREEMPT] 장점 task_struct에 내장 → 컨텍스트 스위치 시 자동 전환 Per-CPU 변수 테이블 불필요

preempt_count 초기화: fork()에서의 설정

새 태스크가 fork()로 생성될 때 preempt_count는 어떻게 초기화될까요? 커널은 자식 태스크가 처음 스케줄될 때 안전하게 선점을 다룰 수 있도록 초기값을 신중하게 설정합니다.

/* kernel/fork.c — copy_thread()에서 preempt_count 초기화 */

/* 새 태스크의 preempt_count 초기값 */
#define FORK_PREEMPT_COUNT  (2 * PREEMPT_DISABLE_OFFSET + PREEMPT_ENABLED)
/* 값: 2 (PREEMPT 필드) → 선점 비활성화 상태로 시작
 *
 * 이유:
 *   1. 자식 태스크가 schedule()에서 처음 선택될 때,
 *      context_switch() → finish_task_switch()를 거침
 *   2. finish_task_switch()에서 preempt_enable() 1회 호출 → count: 2→1
 *   3. ret_from_fork()에서 preempt_enable() 1회 호출 → count: 1→0
 *   4. 이 시점에서 비로소 선점 가능한 상태가 됨
 *
 * 왜 2인가?
 *   context_switch 내부에서 아직 이전 태스크 정리가 끝나지 않은 상태에서
 *   선점되면 안 되므로, 2중으로 보호함
 */

static int copy_thread(struct task_struct *p, ...)
{
    /* ... */
    task_thread_info(p)->preempt_count = FORK_PREEMPT_COUNT;
    /* ... */
}
idle 태스크의 preempt_count: 각 CPU의 idle 태스크(pid 0, swapper)는 PREEMPT_DISABLED(값 1)로 초기화됩니다. idle 태스크는 schedule()을 직접 호출하므로 선점될 필요가 없으며, preempt_count가 1인 상태로 유지됩니다. CPU가 idle 상태일 때 인터럽트로 깨어나면, IRQ 반환 경로에서 need_resched를 확인하여 새 태스크로 전환합니다.

PREEMPT_LAZY (커널 6.12+)

커널 6.12에서 도입된 PREEMPT_LAZYpreempt_count의 비트 21을 활용하여 "지연된 선점(lazy preemption)" 개념을 구현합니다. 기존 FULL 선점 모델의 응답성은 유지하면서, 불필요한 선점을 줄여 처리량을 개선하는 것이 목표입니다.

/* include/linux/preempt.h — PREEMPT_LAZY 비트 정의 */
#define PREEMPT_NEED_RESCHED      0x80000000  /* bit 31: 즉시 선점 */
#define PREEMPT_NEED_RESCHED_LAZY 0x00200000  /* bit 21: 지연 선점 */

/* Lazy preemption 핵심 개념:
 *
 * 일반 태스크 깨우기(ttwu) 시:
 *   1. 먼저 NEED_RESCHED_LAZY만 설정 (비트 21)
 *   2. 커널 코드는 계속 실행 (spin_unlock에서 선점하지 않음)
 *   3. 다음 tick 인터럽트에서 LAZY를 NEED_RESCHED(비트 31)로 승격
 *   4. 이때 비로소 선점 발생
 *
 * 실시간 태스크 깨우기 시:
 *   1. 즉시 NEED_RESCHED(비트 31) 설정
 *   2. 기존 FULL preemption과 동일하게 즉시 선점
 */

/* tick 핸들러에서 lazy → urgent 승격 */
static void tick_check_preempt_lazy(void)
{
    if (test_preempt_need_resched_lazy()) {
        /* LAZY 요청을 URGENT로 승격 */
        set_tsk_need_resched(current);
        set_preempt_need_resched();
        clear_preempt_need_resched_lazy();
    }
}
PREEMPT_LAZY의 이점: FULL 선점 모델에서는 spin_unlock()마다 preempt_schedule()이 호출되어 불필요한 컨텍스트 스위치가 발생할 수 있습니다. PREEMPT_LAZY는 일반 태스크에 대해 선점을 다음 tick까지 지연시켜 처리량을 개선하면서, RT 태스크에 대해서는 즉시 선점을 보장합니다. 이로써 NONE과 FULL의 장점을 통합하는 것이 장기적 목표입니다.
PREEMPT_LAZY vs FULL: 일반 태스크 깨우기 비교 일반 태스크 wakeup spin_unlock spin_unlock tick 인터럽트 spin_unlock PREEMPT FULL spin_lock 구간 (선점 불가) 새 태스크 실행 (즉시 선점됨) bit31=0 (NEED_RESCHED 설정) preempt_count==0 → schedule()! PREEMPT LAZY 기존 태스크 계속 실행 (LAZY 비트만 설정, 즉시 선점 안 함) spin_lock 구간 선점! bit21=1 (LAZY 설정) spin_unlock: LAZY만 → 선점 안 함 tick: LAZY→bit31 승격! → schedule() 절약: 불필요한 컨텍스트 스위치 2회 회피 LAZY + RT 태스크 spin_lock 구간 RT 태스크 즉시 실행 (FULL과 동일하게 동작!) bit31=0 (NEED_RESCHED 즉시 설정 — LAZY 우회) PREEMPT_LAZY의 핵심 원리 1. 일반 태스크 wakeup → LAZY 비트(bit 21)만 설정 → spin_unlock에서 선점 안 함 → tick에서 승격 2. RT 태스크 wakeup → NEED_RESCHED(bit 31) 즉시 설정 → spin_unlock에서 즉시 선점 (FULL과 동일) 3. 결과: RT 응답성은 FULL과 동일 유지, 일반 태스크 간 불필요한 선점 감소 → 처리량 개선 장기 목표: PREEMPT_LAZY가 NONE/VOLUNTARY/FULL을 대체하여 단일 모델로 통합 (커널 6.13+ 논의 중)

PREEMPT_LAZY 구현 세부사항

/* kernel/sched/core.c — 태스크 깨우기 시 lazy vs urgent 결정 */
static void check_preempt_curr(struct rq *rq, struct task_struct *p)
{
    if (task_is_realtime(p)) {
        /* RT/DL 태스크: 즉시 선점 요청 (bit 31) */
        resched_curr(rq);  /* set_tsk_need_resched + PREEMPT_NEED_RESCHED */
    } else {
        /* 일반(CFS) 태스크: lazy 선점 요청 (bit 21) */
        if (sched_feat(PREEMPT_LAZY))
            resched_curr_lazy(rq);  /* PREEMPT_NEED_RESCHED_LAZY만 설정 */
        else
            resched_curr(rq);       /* fallback: 기존 동작 */
    }
}

/* kernel/sched/core.c — tick에서 lazy 승격 */
void scheduler_tick(void)
{
    /* ... */
    if (test_preempt_need_resched_lazy()) {
        /* 매 tick마다 검사: LAZY 요청이 있으면 URGENT로 승격 */
        resched_curr(rq);  /* bit 21 → bit 31로 승격 */
        clear_preempt_need_resched_lazy();
    }
    /* ... */
}

/* preempt_schedule()에서 bit 31만 검사 — bit 21은 무시 */
/* 따라서 spin_unlock → preempt_schedule → should_resched(0)에서
 * bit 31이 0(=NEED_RESCHED 미설정)이면 선점하지 않음
 * LAZY 비트만 설정된 상태에서는 spin_unlock이 무관통!
 */

디버깅 (DEBUG_PREEMPT)

CONFIG_DEBUG_PREEMPT를 활성화하면 preempt_count 관련 오류를 런타임에 감지합니다. 선점 카운터 언밸런스, 잘못된 컨텍스트에서의 호출, sleep-in-atomic 등의 버그를 조기에 발견할 수 있습니다.

/* CONFIG_DEBUG_PREEMPT 활성화 시 추가되는 검사 */

/* 1. preempt_count 언더플로우 감지 */
void preempt_count_sub(int val)
{
    if (DEBUG_LOCKS_WARN_ON(val > preempt_count()))
        return;  /* 감소량이 현재값보다 크면 언더플로우 경고 */
    __preempt_count_sub(val);
}

/* 2. 컨텍스트 불일치 감지 */
void preempt_count_add(int val)
{
    DEBUG_LOCKS_WARN_ON((preempt_count() < 0));
    __preempt_count_add(val);
    DEBUG_LOCKS_WARN_ON((preempt_count() < 0));
}

/* 3. might_sleep() — CONFIG_DEBUG_ATOMIC_SLEEP */
/* atomic 컨텍스트에서 sleep 가능 함수 호출 시 경고:
 *
 * BUG: sleeping function called from invalid context at mm/slab.h:XXX
 * in_atomic(): 1, irqs_disabled(): 0, non_block: 0, pid: 1234, name: test
 * Preemption disabled at:
 * [] my_function+0x12/0x80
 * Call Trace:
 *  dump_stack_lvl+0x...
 *  __might_resched+0x...
 *  kmem_cache_alloc+0x...
 *  my_function+0x...
 */
# ftrace로 선점 이벤트 추적
echo preemptirq:preempt_disable > /sys/kernel/tracing/set_event
echo preemptirq:preempt_enable > /sys/kernel/tracing/set_event
echo 1 > /sys/kernel/tracing/tracing_on
cat /sys/kernel/tracing/trace

# 출력 예:
#           TASK-PID  CPU#  d..1  TIMESTAMP  FUNCTION
#           |    |     |     |         |           |
#   kworker-123   [002] d..1  123.456: preempt_disable caller=_raw_spin_lock+0x1c
#   kworker-123   [002] d..1  123.457: preempt_enable  caller=_raw_spin_unlock+0x2f

# lockdep와 연계: 선점 관련 잠금 순서 위반 검출
echo 1 > /proc/sys/kernel/lock_stat

# preemptoff tracer: 선점 비활성화 최대 지연 측정
echo preemptoff > /sys/kernel/tracing/current_tracer
echo 1 > /sys/kernel/tracing/tracing_on
# ... 워크로드 실행 ...
cat /sys/kernel/tracing/tracing_max_latency
# 출력 예: 1523 (마이크로초 단위 최대 선점 비활성화 구간)

# eBPF로 preempt_disable 구간 히스토그램 수집
bpftrace -e 'kprobe:preempt_count_add { @start[tid] = nsecs; }
  kprobe:preempt_count_sub /@start[tid]/ {
    @us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
  }'
preemptoff 트레이서 활용: preemptoff 트레이서는 선점이 비활성화된 가장 긴 구간을 기록합니다. tracing_max_latency 값이 예상보다 크다면 해당 스택 트레이스를 분석하여 병목 spinlock이나 과도한 preempt_disable 구간을 식별할 수 있습니다. RT 시스템에서는 이 값이 수십 마이크로초를 넘지 않아야 합니다.
preempt_count 관련 문제 디버깅 워크플로우 선점 관련 문제 발생! sleep-in-atomic 경고 BUG: sleeping function... 진단 방법 1. CONFIG_DEBUG_ATOMIC_SLEEP 2. 스택 트레이스에서 호출 지점 확인 3. "Preemption disabled at:" 확인 → GFP_ATOMIC 또는 lock 범위 조정 preempt_count 언밸런스 언더플로우/오버플로우 경고 진단 방법 1. CONFIG_DEBUG_PREEMPT 활성화 2. ftrace preempt_disable/enable 추적 3. lock/unlock 쌍 검증 → 누락된 unlock/enable 경로 수정 높은 선점 지연시간 latency spike 발생 진단 방법 1. preemptoff 트레이서 사용 2. tracing_max_latency 확인 3. eBPF 히스토그램 수집 → 병목 lock 식별 및 최적화 디버깅 도구 요약 CONFIG_DEBUG_PREEMPT preempt_count 언더/오버플로우 즉시 감지, 음수 값 경고 CONFIG_DEBUG_ATOMIC_SLEEP atomic 컨텍스트에서 sleep 시도 경고 + 호출 스택 덤프 preemptoff tracer 선점 비활성화 최대 구간 측정 (마이크로초 단위 정밀도) eBPF + bpftrace preempt_disable 구간 히스토그램, 프로덕션 커널에서도 사용 가능 lockdep + lock_stat 잠금 순서 위반 검출, lock 보유 시간 통계 — 선점 관련 교착 식별

실전 디버깅 예제: preempt_count 값 해석

# /proc/<pid>/status에서 preempt_count 확인 (커널 제공 없으므로 ftrace 활용)

# 1. 특정 함수 진입 시 preempt_count 값 출력
echo 'p:myprobe my_driver_func preempt=%ax' > /sys/kernel/tracing/kprobe_events
echo 1 > /sys/kernel/tracing/events/kprobes/myprobe/enable
cat /sys/kernel/tracing/trace_pipe
# 출력: ... preempt=0x80000001 → PREEMPT필드=1, RESCHED 미설정

# 2. 선점 비활성화 구간 길이 히스토그램 (bpftrace)
bpftrace -e '
tracepoint:preemptirq:preempt_disable { @start[tid] = nsecs; }
tracepoint:preemptirq:preempt_enable /@start[tid]/ {
  @disable_us = hist((nsecs - @start[tid]) / 1000);
  delete(@start[tid]);
}'
# 결과 예:
# @disable_us:
# [0]              15234 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
# [1]               8921 |@@@@@@@@@@@@@@@@@@@@@@@                |
# [2, 4)            3456 |@@@@@@@@@                              |
# [4, 8)             892 |@@                                     |
# [8, 16)            234 |                                       |
# [16, 32)            45 |                                       |  ← 이런 구간 주의!
# [32, 64)             3 |                                       |  ← 문제 후보

# 3. preemptoff 트레이서로 최악 구간의 콜스택 확인
echo preemptoff > /sys/kernel/tracing/current_tracer
echo 0 > /sys/kernel/tracing/tracing_max_latency  # 리셋
echo 1 > /sys/kernel/tracing/tracing_on
# ... 워크로드 실행 ...
cat /sys/kernel/tracing/tracing_max_latency
# 출력: 847 (마이크로초)
cat /sys/kernel/tracing/trace
# → 최대 선점 비활성화 구간의 전체 함수 호출 트레이스가 출력됨
# → 어떤 lock이 847μs 동안 선점을 막았는지 식별 가능

# 4. /proc/sys/kernel/preempt_count_threshold (사용 가능 시)
# 선점 비활성화 임계값을 설정하여 초과 시 경고 출력
echo 1000 > /proc/sys/kernel/preempt_count_threshold  # 1000μs

irq_enter/irq_exit와 preempt_count 조작

하드웨어 인터럽트 핸들러가 실행되기 전후로 irq_enter()/irq_exit()preempt_count의 HARDIRQ 필드를 조작합니다. 이 과정은 인터럽트 컨텍스트를 정확히 추적하고, 인터럽트 반환 시 선점 결정에 핵심적인 역할을 합니다.

/* kernel/softirq.c — irq_enter/irq_exit 구현 */

void irq_enter(void)
{
    rcu_irq_enter();           /* RCU에 IRQ 컨텍스트 진입 알림 */
    irqtime_account_irq(current);  /* IRQ 시간 회계 시작 */
    preempt_count_add(HARDIRQ_OFFSET);  /* HARDIRQ 필드 +1 (0x10000) */
    /* 이 시점부터 in_irq() == true
     * in_interrupt() == true
     * preempt_schedule()이 호출되어도 즉시 리턴
     * sleep 가능 함수 호출하면 BUG */
}

void irq_exit(void)
{
    preempt_count_sub(HARDIRQ_OFFSET);  /* HARDIRQ 필드 -1 */

    /* hardirq에서 나온 직후: softirq 처리 필요한지 확인 */
    if (!in_interrupt() && local_softirq_pending()) {
        /* 모든 IRQ/softirq 컨텍스트에서 나왔고 pending softirq 있음 */
        invoke_softirq();
    }

    irqtime_account_irq(current);
    rcu_irq_exit();

    /* 선점 검사:
     * preempt_count가 0이 되었고 need_resched면 → 커널 선점!
     * 이 경로는 CONFIG_PREEMPT(FULL) 이상에서만 활성
     */
}

/* NMI 컨텍스트의 경우 */
void nmi_enter(void)
{
    preempt_count_add(NMI_OFFSET);  /* NMI 비트 설정 (0x100000) */
    /* NMI는 중첩 불가: 이미 NMI 내부이면 CPU 예외로 처리
     * 따라서 NMI_MASK는 1비트만 사용 (0 또는 1) */
}

void nmi_exit(void)
{
    preempt_count_sub(NMI_OFFSET);  /* NMI 비트 해제 */
}
IRQ 처리 중 preempt_count 변화 시간 프로세스 컨텍스트 count=0 IRQ! irq_enter() count+=0x10000 IRQ 핸들러 실행 in_irq()=true irq_exit() count-=0x10000 pending softirq? Yes __do_softirq() No preempt_count == 0 ? Yes preempt_schedule! No 중단 지점으로 복귀 irq_exit()에서 HARDIRQ 감소 후: ① pending softirq 있으면 처리, ② preempt_count==0이면 선점 검사 → 인터럽트 반환은 커널 선점의 가장 중요한 체크포인트

끝까지 따라가는 상태 전이 예제

개별 API 설명만으로는 preempt_count의 진짜 역할이 잘 안 보입니다. 가장 이해하기 쉬운 장면은 커널 코드가 락을 쥔 상태에서 하드 IRQ에 끊기고, 그 IRQ가 더 높은 우선순위 태스크를 깨운 뒤, 원래 코드가 마지막 unlock을 하자마자 선점이 발생하는 경우입니다. 이 시나리오 하나에 PREEMPT, HARDIRQ, need_resched, 인터럽트 반환, 마지막 spin_unlock() 검사가 모두 들어 있습니다.

  1. 시작: 태스크 A가 시스템 콜 내부에서 실행 중이고, 아직 재스케줄 필요는 없다고 가정합니다. 이 문서의 표기에서는 전체 값이 대략 0x80000000인 상태입니다.
  2. 락 획득: 태스크 A가 spin_lock()을 잡으면 PREEMPT 필드가 올라가 0x80000001이 됩니다. 이제 더 높은 우선순위 태스크가 준비되어도 즉시 CPU를 넘길 수 없습니다.
  3. 하드 IRQ 진입: 장치 인터럽트가 들어오면 irq_enter()가 HARDIRQ 필드를 올려 0x80010001처럼 보입니다. 이 순간 현재 CPU는 "락을 가진 채 하드 IRQ 문맥 위에 올라탄 상태"입니다.
  4. 더 높은 우선순위 태스크 깨움: IRQ 핸들러가 태스크 B를 깨워 즉시 실행시키고 싶어도, 현재는 hardirq 문맥이므로 스케줄할 수 없습니다. 대신 재스케줄 필요 표시만 남기고 값은 0x00010001 쪽으로 바뀝니다.
  5. IRQ 종료: irq_exit()가 HARDIRQ 필드를 내리면 0x00000001이 됩니다. 여전히 PREEMPT 값이 1이므로, "재스케줄 필요"는 알지만 아직 원래 락 보호 구역이 끝나지 않았습니다.
  6. 원래 커널 코드 복귀: 인터럽트 전의 시스템 콜 코드가 이어서 실행됩니다. 이때 태스크 A는 이미 CPU를 넘겨야 한다는 사실을 알고 있지만, lock을 쥔 동안은 끝까지 정리해야 합니다.
  7. 마지막 unlock: spin_unlock()이 PREEMPT 필드를 0으로 내리면 전체 워드가 0x00000000이 되고, 바로 이 지점에서 빠른 경로 선점 검사가 true가 됩니다.
  8. 문맥 전환: 스케줄러가 태스크 B로 전환합니다. 즉, 인터럽트가 들어온 시점이 아니라 마지막 금지 요인이 사라진 시점이 실제 문맥 전환 시점입니다.
"락 보유 중 IRQ 발생" 시나리오의 전체 상태 전이 시간 프로세스 A 실행 0x80000000 spin_lock() 0x80000001 irq_enter() 0x80010001 태스크 B 깨움 need_resched 설정 0x00010001 irq_exit() 0x00000001 irq_exit() 이후에도 즉시 스케줄되지 않는 이유 HARDIRQ는 내려갔지만 PREEMPT 값 1이 남아 있어 원래 락 보호 구역이 아직 끝나지 않았기 때문 즉, "깨워야 한다"와 "당장 바꿀 수 있다"는 다른 조건입니다 원래 커널 코드 복귀 여전히 lock 보유 spin_unlock() 0x00000001 → 0x00000000 선점 검사 통과 schedule() 진입 실제 문맥 전환은 IRQ가 태스크 B를 깨운 순간이 아니라 마지막 금지 요인(PREEMPT=1)이 사라진 직후에 발생 이 차이를 이해하면 "왜 need_resched가 켜졌는데 당장 안 바뀌지?"라는 질문이 대부분 해결됩니다
단계대표 값무슨 일이 벌어졌는가그 자리에서 스케줄 가능한가
시작0x80000000프로세스 컨텍스트, 재스케줄 불필요필요하지 않음
락 획득0x80000001PREEMPT 필드가 올라가 임계 구역 보호 시작불가
IRQ 진입0x80010001HARDIRQ까지 겹쳐져 인터럽트 문맥 위에 올라감불가
태스크 B 깨움0x00010001재스케줄 요청은 생겼지만 hardirq와 lock 때문에 지연불가
IRQ 종료0x00000001HARDIRQ는 사라졌지만 PREEMPT 중첩이 남아 있음불가
unlock 직후0x00000000마지막 금지 요인이 해제되어 빠른 경로 조건 충족가능
/* "락 보유 중 IRQ가 더 높은 우선순위 태스크를 깨움" 시나리오 의사 코드 */
void sys_write_path(struct rq *rq)
{
    spin_lock(&rq->lock);           /* PREEMPT +1 */
    /* ... 런큐 또는 장치 상태 갱신 ... */

    /* 이 사이에 하드 IRQ가 들어온다고 가정 */
    /* irq_enter(): HARDIRQ +1 */
    /* wake_up_process(task_b): need_resched 설정 */
    /* irq_exit(): HARDIRQ -1, 하지만 PREEMPT는 아직 1 */

    /* 인터럽트에서 돌아왔지만 아직 lock 보유 중이므로 선점 불가 */
    spin_unlock(&rq->lock);         /* PREEMPT 1 → 0 */
    /* 여기서 비로소 preempt_count_dec_and_test()가 true가 되어 */
    /* __preempt_schedule() 또는 대응 경로로 진입할 수 있음 */
}
모델별 차이: PREEMPT_NONE과 VOLUNTARY에서는 마지막 spin_unlock()만으로 바로 커널 선점이 일어나지 않을 수 있고, 명시적 양보 지점이나 유저 모드 복귀가 필요합니다. 반대로 FULL 계열에서는 이 지점이 실제 선점 발생 후보가 됩니다. 즉, 위 시나리오의 "상태 누적"은 비슷하지만, 언제 실제로 schedule()에 들어가느냐는 선점 모델에 따라 달라집니다.

자주 발생하는 버그 패턴

preempt_count 관련 버그는 발견이 어렵고 재현이 까다로운 경우가 많습니다. 아래는 실제 커널 개발에서 자주 만나는 버그 패턴과 해결 방법입니다.

/* 버그 1: 에러 경로에서 unlock 누락 */
int my_function(void)
{
    spin_lock(&my_lock);      /* preempt_count: 0 → 1 */

    if (some_error_condition()) {
        return -EINVAL;        /* BUG! spin_unlock 없이 반환 */
                                  /* → preempt_count 영구적으로 1 유지 */
                                  /* → 이 CPU에서 선점이 영원히 비활성화! */
    }

    spin_unlock(&my_lock);
    return 0;
}

/* 수정: goto 패턴으로 통일된 해제 경로 */
int my_function_fixed(void)
{
    int ret = 0;
    spin_lock(&my_lock);

    if (some_error_condition()) {
        ret = -EINVAL;
        goto out_unlock;       /* 통일된 해제 경로 */
    }

out_unlock:
    spin_unlock(&my_lock);
    return ret;
}

/* 버그 2: IRQ 핸들러에서 sleep 가능 함수 호출 */
irqreturn_t my_irq_handler(int irq, void *dev)
{
    void *buf = kmalloc(4096, GFP_KERNEL);  /* BUG! IRQ 컨텍스트에서 GFP_KERNEL */
    /* in_irq()=true → preempt_count의 HARDIRQ 필드 ≠ 0 */
    /* GFP_KERNEL은 sleep 가능 → 여기서 schedule() 시도하면 BUG */

    /* 수정: GFP_ATOMIC 사용 */
    buf = kmalloc(4096, GFP_ATOMIC);
    /* 또는: 작업을 workqueue로 이연 (deferred work) */
    return IRQ_HANDLED;
}

/* 버그 3: preempt_disable 구간에서 조건부 sleep */
void process_items(void)
{
    preempt_disable();        /* preempt_count: 0 → 1 */

    struct item *item = per_cpu_ptr(my_items, smp_processor_id());
    /* per-cpu 데이터를 안전하게 읽으려고 preempt_disable */

    if (item->needs_processing) {
        mutex_lock(&item->mutex);  /* BUG! mutex는 sleep 가능 */
        /* preempt_count=1 상태에서 schedule() 시도 → 교착 가능 */
    }

    preempt_enable();

    /* 수정: per-cpu 데이터를 로컬 변수에 복사한 후 lock */
    preempt_disable();
    struct item local_copy = *per_cpu_ptr(my_items, smp_processor_id());
    preempt_enable();          /* 선점 활성화 후 */
    if (local_copy.needs_processing)
        mutex_lock(&item->mutex); /* OK: 프로세스 컨텍스트에서 sleep 가능 */
}
RCU와 preempt_count의 관계

rcu_read_lock()CONFIG_PREEMPT 커널에서 preempt_disable()과 동일하게 preempt_count의 PREEMPT 필드를 증가시킵니다. 이는 RCU 읽기 측 임계 구역에서 선점이 발생하면 grace period 감지가 실패할 수 있기 때문입니다. CONFIG_PREEMPT_RCU에서는 별도의 카운터(rcu_read_lock_nesting)를 사용하여 선점을 허용하면서도 grace period를 올바르게 추적합니다. 자세한 내용은 RCU 문서를 참조하세요.