Per-CPU 변수

Linux 커널의 Per-CPU 변수는 각 CPU가 독립적인 데이터 복사본을 가지도록 하여 캐시라인 바운싱과 false sharing을 줄이고 lock contention을 제거하는 고성능 기법입니다. 이 문서는 DEFINE_PER_CPU, this_cpu_* fast path, alloc_percpu 기반 동적 할당, 전역 집계 시점의 동기화 전략, 네트워크/메모리 서브시스템 사례와 NUMA 환경 튜닝 포인트까지 상세히 설명합니다.

일상 비유: Per-CPU 변수는 개인 사물함과 비슷합니다. 공용 사물함(공유 변수)은 자물쇠(lock)로 보호해야 하지만, 각자의 사물함(per-CPU 변수)은 자물쇠 없이 빠르게 접근할 수 있습니다.

핵심 요약

  • 독립 복사본 — 각 CPU가 자신만의 변수 복사본을 가집니다.
  • Lock-free — 동일 CPU 내에서는 동기화가 필요 없습니다.
  • 캐시 효율 — False sharing이 없어 캐시 성능이 극대화됩니다.
  • 빠른 접근 — this_cpu_* 연산은 원자적이면서도 매우 빠릅니다.
  • 통계 집계 — 네트워크 패킷 수, 메모리 할당 통계 등에 최적입니다.

단계별 이해

  1. 핵심 요소 확인
    이 문서에서 다루는 자료구조/API를 먼저 정리합니다.
  2. 처리 흐름 추적
    요청 시작부터 완료까지 실행 경로를 순서대로 확인합니다.
  3. 문제 지점 점검
    실패 경로, 경합 구간, 성능 병목을 체크합니다.

개요 (Overview)

Per-CPU 변수는 SMP(Symmetric Multi-Processing) 시스템에서 성능을 극대화하는 핵심 기법입니다:

공유 변수 (False Sharing) 공유 캐시라인 counter (64 bytes) CPU0 CPU1 CPU2 CPU3 MESI: Invalidate → 캐시라인 무효화 모든 CPU가 재로드 필요 Per-CPU 변수 (독립 캐시라인) CPU0 캐시라인 counter[cpu0] CPU1 캐시라인 counter[cpu1] CPU2 캐시라인 counter[cpu2] CPU0 CPU1 CPU2 각 CPU 독립 접근 → 충돌 없음 캐시라인 무효화 불필요 캐시라인 충돌 독립 캐시라인
공유 변수는 여러 CPU가 같은 캐시라인을 두고 경쟁해 MESI 무효화가 발생하지만, per-CPU 변수는 CPU마다 독립 캐시라인을 사용합니다.
.data..percpu (링커 섹션 원본) CPU0 per-CPU 영역 offset[0] = 0x0000 CPU1 per-CPU 영역 offset[1] = 0x8000 CPU2 per-CPU 영역 offset[2] = 0x10000 __per_cpu_offset[] [0] = 0x0000 [1] = 0x8000 [2] = 0x10000 ... this_cpu_ptr(var) = &var + offset[cpu_id] CPU별 독립 영역 링커 원본 섹션 복사/오프셋 참조
부팅 시 setup_per_cpu_areas().data..percpu를 각 CPU별로 복사하며, __per_cpu_offset[]에 오프셋을 저장합니다.
ℹ️

실사용 빈도: 커널 통계 카운터의 90% 이상이 per-CPU 변수를 사용합니다. 네트워크 패킷 카운트, 메모리 할당 통계, 스케줄러 런큐, SLAB 할당자 등 모든 고성능 서브시스템에서 필수적입니다.

내부 구조 (Internals)

부팅 과정에서 커널은 각 CPU마다 .data..percpu 섹션의 복사본을 만들고, 각 복사본의 시작 주소를 __per_cpu_offset 배열에 저장합니다:

/* arch/x86/kernel/setup_percpu.c (개념 요약) */

/* 부팅 시 각 CPU에 .data..percpu 섹션 복사 */
void __init setup_per_cpu_areas(void)
{
    int cpu;
    for_each_possible_cpu(cpu) {
        /* NUMA 로컬 노드에 섹션 복사본 할당 */
        void *ptr = alloc_percpu_area(cpu);
        memcpy(ptr, __per_cpu_load, __per_cpu_size);
        __per_cpu_offset[cpu] = ptr - ((void *)&__per_cpu_start);
    }
}

/* this_cpu_ptr() 동작 원리 */
#define this_cpu_ptr(ptr) \
    ((typeof(ptr))(((void *)(ptr)) + __per_cpu_offset[smp_processor_id()]))

/* 예: CPU2의 my_counter 주소 */
/* = &my_counter + __per_cpu_offset[2]  */

__per_cpu_offset[NR_CPUS] 배열이 각 CPU의 per-CPU 영역 오프셋을 저장하며, this_cpu_ptr()은 단순 포인터 덧셈으로 현재 CPU의 변수 주소를 계산합니다.

x86 구현: %gs 세그먼트 레지스터

x86-64에서 per-CPU 변수 접근은 %gs 세그먼트 레지스터를 이용하여 CPU-local 어드레싱을 하드웨어 수준에서 구현합니다. 이 메커니즘 덕분에 this_cpu_* 연산이 단일 명령어로 컴파일됩니다.

x86-64: %gs 세그먼트 per-CPU 어드레싱 CPU 0 MSR_GS_BASE = 0xFFFF8800_00000000 this_cpu_inc(ctr) incl %gs:0x1234 CPU 1 MSR_GS_BASE = 0xFFFF8800_00008000 this_cpu_inc(ctr) incl %gs:0x1234 동일한 명령어, 다른 물리 주소 CPU0: %gs:0x1234 → 0xFFFF8800_00001234 CPU1: %gs:0x1234 → 0xFFFF8800_00009234 → 하드웨어가 CPU별 주소를 자동 해석! 물리 메모리 (per-CPU 영역) CPU0 per-CPU 영역 0xFFFF8800_00000000 ~ +0x8000 CPU1 per-CPU 영역 0xFFFF8800_00008000 ~ +0x8000 CPU2 per-CPU 영역 0xFFFF8800_00010000 ~ +0x8000 swapgs 명령: 커널 진입 시 MSR_GS_BASE를 커널용 per-CPU 베이스로 교체
/* arch/x86/kernel/setup_percpu.c */

/* 부팅 시 각 CPU의 GS 베이스 설정 */
void __init setup_per_cpu_areas(void)
{
    /* ... per-CPU 영역 할당 후 ... */
    for_each_possible_cpu(cpu) {
        /* 각 CPU의 per-CPU 영역 시작 주소를 GS 베이스로 설정 */
        per_cpu_offset(cpu) = delta;

        /* per_cpu 변수 current_task의 초기화 등 */
        per_cpu(this_cpu_off, cpu) = per_cpu_offset(cpu);
    }
}

/* CPU 초기화 시 GS 베이스 MSR 설정 */
/* wrmsrl(MSR_GS_BASE, per_cpu_offset(cpu)); */

/*
 * this_cpu_inc(counter)가 x86-64에서 컴파일되는 결과:
 *
 *   incl %gs:counter@NTPOFF    // NTPOFF = negative thread-pointer offset
 *
 * 이것은 단일 명령어로:
 * 1. MSR_GS_BASE + counter의 오프셋 = 현재 CPU의 counter 주소
 * 2. 해당 주소의 값을 1 증가
 * 3. 단일 CPU에서 원자적 (인터럽트 사이에 끼어들어도 같은 CPU)
 */
ℹ️

swapgs와 커널 진입: 사용자 공간에서 커널로 진입할 때(syscall, 인터럽트) swapgs 명령이 실행되어 MSR_GS_BASE를 사용자용 → 커널용 per-CPU 베이스로 교체합니다. 이로써 커널 코드의 %gs:offset 접근이 현재 CPU의 per-CPU 영역을 가리키게 됩니다. ARM64에서는 TPIDR_EL1 레지스터가 동일 역할을 합니다.

어셈블리 수준 비교: per-CPU vs atomic vs spinlock

this_cpu_inc()가 왜 빠른지, 실제 생성되는 기계어 명령어를 비교합니다:

; ── this_cpu_inc(counter) ──
; 단일 명령어, lock prefix 없음, 캐시라인 독점 불필요
incl %gs:counter(%rip)        ; 1 cycle (L1 히트 시)

; ── atomic_inc(&shared_counter) ──
; lock prefix → 버스/캐시 잠금, MESI 프로토콜 동기화 필요
lock incl counter(%rip)       ; 10~40 cycles (캐시라인 상태 의존)

; ── spin_lock + inc + spin_unlock ──
; 3개 이상의 명령어 + lock prefix + 분기 + 메모리 배리어
lock cmpxchg %edx, lock(%rip) ; 잠금 획득 (경쟁 시 스핀)
incl counter(%rip)            ; 카운터 증가
movl $0, lock(%rip)           ; 잠금 해제
                              ; 총 50~200+ cycles (경쟁 시)
방법명령어 수lock prefix캐시라인 전이지연 (cycles)
this_cpu_inc1불필요없음 (CPU-local)~1
atomic_inc1필요MESI S→M 전이10~40
spinlock + inc3+필요MESI 전이 + 스핀50~200+
💡

왜 lock prefix가 불필요한가: this_cpu_inc현재 CPU만 해당 메모리를 접근합니다. 다른 CPU는 자신의 복사본에만 접근하므로, CPU 간 경쟁(cache coherency traffic)이 원천적으로 없습니다. 같은 CPU 내에서 인터럽트가 끼어들어도, 단일 명령어는 원자적으로 완료됩니다.

Per-CPU 할당자 내부 구조

동적 per-CPU 메모리 할당(alloc_percpu())은 커널의 전용 할당자인 pcpu allocator가 관리합니다. 이 할당자는 메모리를 pcpu_chunk 단위로 관리하며, 각 청크가 모든 CPU에 대한 동일 크기의 유닛(unit)을 포함합니다.

Per-CPU 할당자 (pcpu_chunk) 구조 First Chunk (부팅 시 생성) static 영역 DEFINE_PER_CPU 변수들 reserved 모듈 static per-CPU dynamic 영역 alloc_percpu() 초기 할당 빈 공간 ↑ 각 영역이 CPU마다 unit_size만큼 반복됨 (CPU0 unit | CPU1 unit | CPU2 unit | ...) Dynamic Chunks (런타임 생성) alloc #1 128 bytes alloc #2 64 bytes free free first chunk 공간 부족 시 vmalloc에서 새 청크 생성 비트맵으로 free/used 영역 관리 Chunk 내부: Unit 레이아웃 (unit_size = 32KB 예시) CPU0 Unit (32KB) NUMA node 0에 배치 CPU1 Unit (32KB) NUMA node 0에 배치 CPU2 Unit (32KB) NUMA node 1에 배치 CPU3 Unit (32KB) NUMA node 1에 배치 alloc_percpu(struct X) → 각 Unit 내 동일 오프셋에 sizeof(X) 바이트 할당 → per_cpu_ptr(ptr, cpu) = chunk_base + unit_offset[cpu] + alloc_offset
/* mm/percpu.c — pcpu_chunk 구조체 (핵심 필드) */
struct pcpu_chunk {
    struct list_head list;           /* 청크 리스트 연결 */
    int             free_bytes;      /* 남은 바이트 수 */
    int             contig_bits;     /* 최대 연속 빈 블록 */
    int             nr_populated;    /* 메모리 매핑된 페이지 수 */
    unsigned long   *alloc_map;      /* 할당 비트맵 */
    unsigned long   *bound_map;      /* 할당 경계 비트맵 */
    struct page     **pages;         /* 물리 페이지 배열 (lazy populate) */
    void            *base_addr;      /* vmalloc 영역 가상 주소 */
};

/* pcpu_alloc() 핵심 흐름 (개념 요약) */
static void __percpu *pcpu_alloc(size_t size, size_t align,
                                 bool reserved, gfp_t gfp)
{
    /* 1. 기존 청크에서 충분한 빈 공간 검색 */
    list_for_each_entry(chunk, &pcpu_chunk_lists[slot], list) {
        off = pcpu_find_block_fit(chunk, bits, bit_align);
        if (off >= 0)
            goto area_found;
    }

    /* 2. 기존 청크에 공간 없으면 새 청크 생성 */
    chunk = pcpu_create_chunk(pcpu_chunk_type, gfp);
    /* → vmalloc 영역에서 unit_size × nr_cpus 크기 예약 */

area_found:
    /* 3. 비트맵에서 할당 표시 */
    pcpu_block_update_hint_alloc(chunk, off, bits);

    /* 4. 필요하면 물리 페이지 매핑 (lazy populate) */
    pcpu_populate_chunk(chunk, off, size);

    /* 5. per-CPU 포인터 반환 */
    return __addr_to_pcpu_ptr(chunk->base_addr + off);
}
구성 요소설명크기
First chunk부팅 시 생성, static + reserved + dynamic 영역 포함pcpu_unit_size × NR_CPUS
Reserved 영역모듈의 정적 per-CPU 변수 전용PERCPU_MODULE_RESERVE (8KB)
Dynamic chunkfirst chunk 부족 시 vmalloc에서 런타임 생성가변 (필요에 따라 확장)
Unit청크 내 각 CPU에 할당된 메모리 영역pcpu_unit_size (보통 32KB~256KB)
Atomunit의 물리 페이지 할당 단위 (4KB~2MB)pcpu_atom_size
ℹ️

Lazy population: 동적 청크는 가상 주소만 먼저 예약하고, 실제 물리 페이지는 pcpu_populate_chunk()에서 할당 시점에 매핑합니다. 이로써 모든 CPU에 즉시 물리 메모리를 할당하지 않아도 되며, 특히 CPU 수가 많은 시스템에서 메모리 낭비를 줄입니다.

기본 API (Basic API)

DEFINE_PER_CPU 매크로

/* include/linux/percpu-defs.h */
#define DEFINE_PER_CPU(type, name) \
    __PCPU_ATTRS(sec) __typeof__(type) name

/* 사용 예: 정적 per-CPU 변수 선언 */
DEFINE_PER_CPU(unsigned long, my_counter);

/* 배열도 가능 */
DEFINE_PER_CPU(int[10], my_array);

/* 구조체도 가능 */
struct stats {
    unsigned long count;
    unsigned long bytes;
};
DEFINE_PER_CPU(struct stats, net_stats);

DEFINE_PER_CPU 변체

매크로용도특성
DEFINE_PER_CPU(type, name)일반 per-CPU 변수기본 정렬, 읽기/쓰기 모두 빠름
DEFINE_PER_CPU_READ_MOSTLY(type, name)읽기 전용에 가까운 변수읽기 캐시라인 최적화, 쓰기는 드묾
DEFINE_PER_CPU_SHARED_ALIGNED(type, name)False sharing 명시적 방지캐시라인 경계 정렬 강제
DEFINE_PER_CPU_ALIGNED(type, name)정렬 보장 필요 시SMP_CACHE_BYTES 단위 정렬
DEFINE_PER_CPU_PAGE_ALIGNED(type, name)페이지 정렬 필요 시PAGE_SIZE 단위 정렬
/* READ_MOSTLY: 부팅 시 설정되고 이후 읽기만 하는 값 */
DEFINE_PER_CPU_READ_MOSTLY(struct cpuinfo_x86, cpu_info);

/* SHARED_ALIGNED: 같은 캐시라인에 들어갈 수 있는 변수들을 분리 */
/* 예: 두 hot counter가 같은 캐시라인이면 False Sharing 발생 */
DEFINE_PER_CPU_SHARED_ALIGNED(struct net_device_stats, dev_stats);
/* → 각 CPU의 dev_stats는 서로 다른 캐시라인에 배치됨 */

접근 API (Access API)

/* 현재 CPU의 변수 접근 (preemption 비활성화 필요) */
unsigned long *ptr = get_cpu_var(my_counter);
(*ptr)++;
put_cpu_var(my_counter);  /* preemption 재활성화 */

/* 특정 CPU의 변수 접근 */
unsigned long *cpu5_counter = per_cpu_ptr(&my_counter, 5);

/* 원자적 연산 (가장 빠름!) */
this_cpu_inc(my_counter);      /* counter++ */
this_cpu_add(my_counter, 10);  /* counter += 10 */
this_cpu_read(my_counter);     /* 값 읽기 */
this_cpu_write(my_counter, 0); /* 값 쓰기 */

this_cpu_* 연산 (this_cpu_* Operations)

함수 기능 특징
this_cpu_read(var) 값 읽기 원자적, preemption-safe
this_cpu_write(var, val) 값 쓰기 원자적
this_cpu_add(var, val) 덧셈 원자적, lock-free
this_cpu_inc(var) 1 증가 this_cpu_add(var, 1)
this_cpu_dec(var) 1 감소 this_cpu_add(var, -1)
this_cpu_and(var, val) 비트 AND 원자적
this_cpu_or(var, val) 비트 OR 원자적
this_cpu_xchg(var, val) 교환 이전 값 반환
this_cpu_cmpxchg(var, old, new) 조건부 교환 CAS 연산
💡

성능: this_cpu_* 연산은 x86에서 단일 명령어로 컴파일됩니다. 예를 들어 this_cpu_inc(counter)incl %gs:offset 하나로 끝나며, 이는 lock prefix가 붙은 lock incl보다 10배 이상 빠릅니다.

raw_cpu_* vs this_cpu_*

this_cpu_*raw_cpu_*는 모두 per-CPU 변수를 조작하지만, 사용 컨텍스트가 다릅니다:

함수 사용 컨텍스트 Preemption 상태 성능
this_cpu_inc(var) 일반 태스크, 소프트IRQ 자동 보장 (아키텍처 레벨) 빠름
raw_cpu_inc(var) IRQ 핸들러, NMI, preempt off 구간 이미 비활성화 전제 약간 더 빠름
/* 인터럽트 핸들러 — preemption 이미 비활성화 */
irqreturn_t my_irq_handler(int irq, void *dev)
{
    raw_cpu_inc(irq_counter);  /* IRQ 컨텍스트: raw_cpu_* 사용 */
    return IRQ_HANDLED;
}

/* 일반 태스크 컨텍스트 */
void handle_packet(void)
{
    this_cpu_inc(pkt_counter);  /* 태스크 컨텍스트: this_cpu_* 사용 */
}

동적 할당 (Dynamic Allocation)

Per-CPU 정적 할당 vs 동적 할당 정적 할당 (DEFINE_PER_CPU) 링크 시 .data..percpu 섹션에 배치 .data..percpu 원본 (커널 이미지) setup_per_cpu_areas() 에서 CPU별 복사: CPU 0 복사 CPU 1 복사 CPU N 복사 장점: • 부팅 시 자동 초기화 (0으로) • 동적 할당 오버헤드 없음 • 컴파일 시 크기 확정 DEFINE_PER_CPU(int, counter); 동적 할당 (alloc_percpu) 런타임에 vmalloc 영역에서 per-CPU 청크 할당 pcpu_chunk (vmalloc 영역) 각 CPU에 독립 청크 배분: CPU 0 청크 CPU 1 청크 CPU N 청크 장점: • 런타임 크기 결정 (모듈에서 사용) • 모듈 언로드 시 해제 가능 • 정렬 파라미터 지정 가능 ptr = alloc_percpu(int);
정적 할당은 커널 이미지에 포함되어 부팅 시 자동 복사되고, 동적 할당은 런타임에 vmalloc 기반 percpu 청크에서 할당됩니다. 모듈에서는 동적 할당을 사용해야 합니다.
/* include/linux/percpu.h */

/* per-CPU 메모리 할당 */
void __percpu *alloc_percpu(type);

/* 정렬 지정 할당 */
void __percpu *__alloc_percpu(size_t size, size_t align);

/* 해제 */
void free_percpu(void __percpu *ptr);

/* 사용 예 */
struct my_data {
    unsigned long count;
    unsigned long bytes;
};

struct my_data __percpu *stats;

stats = alloc_percpu(struct my_data);
if (!stats)
    return -ENOMEM;

/* 현재 CPU의 데이터 접근 */
struct my_data *ptr = this_cpu_ptr(stats);
ptr->count++;

/* 정리 */
free_percpu(stats);

NUMA 인식 할당

NUMA 시스템에서 per-CPU 변수는 각 CPU가 속한 로컬 NUMA 노드에 메모리를 배치하여 cross-node 접근 비용을 최소화합니다. 일반 alloc_percpu()는 기본 GFP 플래그를 사용하지만, GFP 플래그를 명시하면 메모리 부족 동작을 제어할 수 있습니다:

/* include/linux/percpu.h */

/* GFP 플래그 지정 저수준 할당 */
void __percpu *__alloc_percpu_gfp(size_t size, size_t align, gfp_t gfp);

/* 타입 기반 GFP 할당 (권장) */
#define alloc_percpu_gfp(type, gfp) \
    (typeof(type) __percpu *)__alloc_percpu_gfp(sizeof(type), \
                                     __alignof__(type), gfp)

/* NUMA 친화적 할당: GFP_KERNEL로 로컬 노드 우선 */
struct my_data __percpu *stats;
stats = alloc_percpu_gfp(struct my_data, GFP_KERNEL);
if (!stats)
    return -ENOMEM;

/* 각 CPU의 per-CPU 데이터는 해당 CPU의 NUMA 노드에 배치됨 */
💡

NUMA 팁: NUMA 시스템에서 cross-node 메모리 접근은 로컬 접근보다 2~4배 느립니다. alloc_percpu_gfp(type, GFP_KERNEL)을 사용하면 커널이 각 CPU의 로컬 NUMA 노드에 메모리를 배치하여 이 비용을 회피합니다.

실사용 사례 (Real-World Usage)

네트워크 통계

/* net/core/dev.c */
struct pcpu_lstats {
    u64 packets;
    u64 bytes;
    struct u64_stats_sync syncp;
};

struct net_device {
    struct pcpu_lstats __percpu *lstats;
    /* ... */
};

/* 패킷 수신 시 */
void update_rx_stats(struct net_device *dev, int len)
{
    struct pcpu_lstats *stats = this_cpu_ptr(dev->lstats);

    u64_stats_update_begin(&stats->syncp);
    stats->packets++;
    stats->bytes += len;
    u64_stats_update_end(&stats->syncp);
}

메모리 할당자 (SLUB)

SLUB 할당자는 per-CPU 프리리스트를 유지하여, 대부분의 kmalloc()/kmem_cache_alloc() 호출이 잠금 없이 완료됩니다.

SLUB per-CPU Fast Path kmem_cache_alloc() 또는 kmalloc() this_cpu_ptr(cpu_slab) Lock-free! freelist 비어있나? NO 즉시 반환 (Fast Path) freelist 포인터 교체만 YES __slab_alloc (Slow Path) partial 리스트 또는 새 slab Fast path: ~4ns (this_cpu_cmpxchg 한 번) | Slow path: ~50~200ns (잠금 + slab 보충) 일반 워크로드에서 fast path 비율: 95%+ → per-CPU 덕분에 SLUB 전체 성능 결정
/* mm/slub.c — SLUB per-CPU 캐시 구조 */
struct kmem_cache_cpu {
    void **freelist;       /* per-CPU 프리리스트 헤드 */
    unsigned long tid;     /* 트랜잭션 ID (cmpxchg 검증용) */
    struct slab *slab;     /* 현재 사용 중인 slab 페이지 */
    struct slab *partial;  /* per-CPU partial 리스트 */
};

struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;  /* per-CPU fast path */
    /* ... */
};

/* Fast path: this_cpu_cmpxchg_double로 lock-free 할당 */
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t flags)
{
    struct kmem_cache_cpu *c;
    void *object;
    unsigned long tid;

redo:
    c = raw_cpu_ptr(s->cpu_slab);
    tid = c->tid;
    object = c->freelist;

    if (unlikely(!object || !c->slab))
        return __slab_alloc(s, flags, c);  /* slow path */

    /* cmpxchg_double: freelist와 tid를 원자적으로 교체 */
    if (!this_cpu_cmpxchg_double(s->cpu_slab->freelist, s->cpu_slab->tid,
                                  object, tid,
                                  get_freepointer(s, object), next_tid(tid)))
        goto redo;  /* 다른 컨텍스트가 끼어들면 재시도 */

    return object;
}

스케줄러 런큐

각 CPU의 런큐(struct rq)는 per-CPU 변수로, 스케줄러가 잠금 없이 현재 CPU의 런큐에 접근할 수 있게 합니다.

/* kernel/sched/core.c */
DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);

/* 현재 CPU의 런큐에 접근 */
#define this_rq()     this_cpu_ptr(&runqueues)
#define cpu_rq(cpu)   (per_cpu_ptr(&runqueues, cpu))

/* 스케줄러 태스크 깨우기 fast path */
void ttwu_queue_wakelist(struct task_struct *p, int cpu)
{
    struct rq *rq = cpu_rq(cpu);
    /* ... */
}

/* struct rq는 수백 바이트 크기이며 SHARED_ALIGNED 로 캐시라인 정렬됨 */
/* → 다른 CPU의 런큐와 false sharing 방지 */
/* → 로드밸런싱 시에만 다른 CPU의 런큐 접근 (pull migration) */

네트워크 softnet_data

/* net/core/dev.c — 네트워크 per-CPU 처리 구조 */
struct softnet_data {
    struct list_head    poll_list;    /* NAPI 폴링 리스트 */
    struct sk_buff_head process_queue; /* 처리 대기 패킷 큐 */
    struct sk_buff_head input_pkt_queue; /* 입력 패킷 큐 */
    unsigned int        processed;    /* 처리된 패킷 수 */
    unsigned int        time_squeeze; /* NAPI 타임아웃 횟수 */
    unsigned int        received_rps; /* RPS로 받은 패킷 */
    struct softnet_data *rps_ipi_next; /* RPS IPI 체인 */
    /* ... */
};

DEFINE_PER_CPU_ALIGNED(struct softnet_data, softnet_data);

/* 패킷 수신 시 현재 CPU의 softnet_data 접근 */
struct softnet_data *sd = this_cpu_ptr(&softnet_data);
/* → 각 CPU가 독립적으로 패킷 처리, lock-free */
/* → /proc/net/softnet_stat에서 CPU별 통계 확인 가능 */

인터럽트 통계

/* arch/x86/include/asm/hardirq.h */
typedef struct {
    unsigned int __softirq_pending;
    unsigned int __nmi_count;
    unsigned int irq_tlb_count;
    unsigned int irq_call_count;
    unsigned int irq_thermal_count;
    /* ... 약 15~20개 카운터 */
} ____cacheline_aligned irq_cpustat_t;

DEFINE_PER_CPU_SHARED_ALIGNED(irq_cpustat_t, irq_stat);

/* 인터럽트 핸들러에서 (preemption 이미 비활성화) */
raw_cpu_inc(irq_stat.__nmi_count);

/* /proc/interrupts에서 표시되는 값 = 모든 CPU의 합 */
/* /proc/stat의 intr 행도 per-CPU 카운터 합산 결과 */

워크큐 (Workqueue)

/* kernel/workqueue.c — per-CPU 워크큐 풀 */
struct worker_pool {
    spinlock_t          lock;
    int                 cpu;          /* 바인딩된 CPU (-1이면 unbound) */
    struct list_head    worklist;     /* 대기 중인 작업 리스트 */
    int                 nr_workers;   /* 워커 스레드 수 */
    int                 nr_idle;      /* 유휴 워커 수 */
    /* ... */
};

/* 각 CPU에 normal과 high-priority 두 개의 worker pool */
static DEFINE_PER_CPU_SHARED_ALIGNED(struct worker_pool [NR_STD_WORKER_POOLS],
                                    cpu_worker_pools);

/* queue_work()는 현재 CPU의 pool에 작업을 추가 */
/* → CPU-local 실행으로 캐시 친화성 극대화 */
/* → WQ_UNBOUND 플래그가 없으면 per-CPU 바인딩 */

페이지 할당 통계 (vmstat)

/* mm/vmstat.c — 페이지 할당 통계 per-CPU 캐싱 */
DEFINE_PER_CPU(struct vm_event_state, vm_event_states) = {{0}};

/* 페이지 폴트 카운터 증가 (매우 빈번 → per-CPU 필수) */
#define count_vm_event(item) \
    this_cpu_inc(vm_event_states.event[item])

/* /proc/vmstat 출력 시 모든 CPU 합산 */
void all_vm_events(unsigned long *ret)
{
    int cpu;
    for_each_online_cpu(cpu) {
        struct vm_event_state *this = &per_cpu(vm_event_states, cpu);
        for (int i = 0; i < NR_VM_EVENT_ITEMS; i++)
            ret[i] += this->event[i];
    }
}

/* zone_stat도 per-CPU 차등 카운터 사용 */
/* → 임계값(stat_threshold) 초과 시에만 전역 카운터에 반영 */
/* → 읽기 빈도가 극히 낮고 쓰기 빈도가 극히 높은 패턴에 최적 */

전역 집계 동기화 (Global Aggregation)

per-CPU 변수를 전체 CPU에 걸쳐 집계할 때는 일관성과 CPU 핫플러그를 고려해야 합니다.

기본 집계 패턴

단순 정수 카운터는 for_each_possible_cpu()per_cpu()로 합산합니다:

unsigned long total = 0;
int cpu;

for_each_possible_cpu(cpu)
    total += per_cpu(my_counter, cpu);

pr_info("total = %lu\n", total);

64비트 통계 안전 집계

32비트 아키텍처에서 u64를 읽으면 중간에 갱신이 끼어들 수 있습니다. u64_stats_sync와 시퀀스 카운터를 이용해 tearing을 방지합니다:

/* 쓰기 측 (패킷 수신 등) */
struct pcpu_lstats *s = this_cpu_ptr(lstats);
u64_stats_update_begin(&s->syncp);
s->packets++;
s->bytes += len;
u64_stats_update_end(&s->syncp);

/* 읽기 측 (전역 집계) */
u64 sum_packets = 0, sum_bytes = 0;
int cpu;

for_each_possible_cpu(cpu) {
    struct pcpu_lstats *s = per_cpu_ptr(lstats, cpu);
    unsigned int start;
    u64 p, b;

    do {
        start = u64_stats_fetch_begin(&s->syncp);
        p = s->packets;
        b = s->bytes;
    } while (u64_stats_fetch_retry(&s->syncp, start));

    sum_packets += p;
    sum_bytes   += b;
}

CPU 핫플러그 고려

집계 도중 CPU가 오프라인이 되면 for_each_possible_cpu()는 오프라인 CPU도 순회하므로 주의가 필요합니다. 온라인 CPU만 대상으로 하거나 핫플러그를 잠금으로 보호합니다:

/* 온라인 CPU만 순회 + 핫플러그 방지 */
cpus_read_lock();          /* CPU 핫플러그 비활성화 */
for_each_online_cpu(cpu) {
    total += per_cpu(my_counter, cpu);
}
cpus_read_unlock();
⚠️

일관성 주의: 집계 도중에도 다른 CPU가 값을 변경할 수 있으므로, 읽은 합계는 근사값입니다. 엄격한 실시간 일관성이 요구되는 경우(예: 결제 금액, 보안 카운터)에는 per-CPU 변수가 부적합하며, 대신 atomic 변수나 seqlock을 고려하십시오.

percpu_counter: 배치 집계 카운터

percpu_counter는 per-CPU 로컬 카운터와 전역 카운터를 결합한 자료구조로, 빈번한 업데이트에서 per-CPU 성능을 유지하면서도 전역 값의 근사치를 효율적으로 제공합니다. 파일시스템의 프리 블록 수, 네트워크 소켓 수 등에 광범위하게 사용됩니다.

percpu_counter 배치 집계 흐름 CPU0 local: +3 this_cpu_add() Lock-free CPU1 local: +7 this_cpu_add() Lock-free CPU2 local: +35 임계값 초과! → 전역 반영 CPU3 local: +12 this_cpu_add() Lock-free batch 임계값 기본: 32 (NR_CPUS에 비례) batch flush (spinlock) 전역 카운터 (s64 count): 1000 spinlock 보호 | CPU2가 local 35를 여기에 합산 → 1035 percpu_counter_read_positive() 전역 count만 읽기 (빠름, 오차 ≤ batch×NR_CPUS) percpu_counter_sum() 모든 CPU 합산 (느림, 정확)
/* include/linux/percpu_counter.h */
struct percpu_counter {
    raw_spinlock_t lock;           /* 전역 카운터 보호 */
    s64 count;                     /* 전역 카운터 (배치 반영분) */
    s32 __percpu *counters;         /* per-CPU 로컬 카운터 */
};

/* 업데이트: 대부분 lock-free, 임계값 초과 시에만 lock */
void percpu_counter_add_batch(struct percpu_counter *fbc,
                              s64 amount, s32 batch)
{
    s64 count;
    preempt_disable();
    count = __this_cpu_read(*fbc->counters) + amount;

    if (abs(count) >= batch) {
        /* 임계값 초과 → 전역 카운터에 반영 */
        raw_spin_lock(&fbc->lock);
        fbc->count += count;
        __this_cpu_sub(*fbc->counters, count - amount);
        __this_cpu_write(*fbc->counters, 0);
        raw_spin_unlock(&fbc->lock);
    } else {
        /* 임계값 미만 → per-CPU 로컬에만 누적 (lock-free!) */
        __this_cpu_write(*fbc->counters, count);
    }
    preempt_enable();
}

/* 빠른 읽기 (근사치): 전역 값만 반환 */
static inline s64 percpu_counter_read_positive(struct percpu_counter *fbc)
{
    s64 ret = READ_ONCE(fbc->count);
    return ret > 0 ? ret : 0;
    /* 오차 범위: ±(batch × NR_CPUS) */
}

/* 정확한 읽기: 모든 CPU 합산 (느림) */
s64 percpu_counter_sum(struct percpu_counter *fbc)
{
    s64 ret = fbc->count;
    int cpu;
    for_each_online_cpu(cpu)
        ret += *per_cpu_ptr(fbc->counters, cpu);
    return ret;
}

/* 실사용: ext4 프리 블록 카운터 */
/* struct ext4_sb_info { */
/*     struct percpu_counter s_freeclusters_counter; */
/* }; */
/* → 블록 할당/해제 시 percpu_counter_add_batch()로 빠르게 업데이트 */
/* → statfs() 호출 시 percpu_counter_sum()으로 정확한 값 반환 */

local_t: per-CPU 원자적 카운터

local_t는 per-CPU 전용 원자적 타입으로, atomic_t와 달리 lock prefix 없이 동작합니다. 인터럽트와 NMI가 같은 CPU의 per-CPU 변수를 수정할 수 있는 환경에서, 단일 CPU 내의 원자성만 보장하면 되는 경우에 사용합니다.

/* include/asm-generic/local.h */
typedef struct {
    atomic_long_t a;
} local_t;

/* local_t 연산 (x86: lock prefix 없는 원자적 연산) */
local_inc(&counter);       /* incq (lock prefix 없음) */
local_dec(&counter);       /* decq */
local_add(10, &counter);   /* addq $10 */
local_read(&counter);      /* 값 읽기 */
local_set(&counter, 0);    /* 값 설정 */

/* local_t vs atomic_t vs this_cpu_* 비교 */
/*                                                        */
/* atomic_inc(&x)     → lock incq (CPU간 원자적, 느림)     */
/* local_inc(&x)      → incq      (CPU내 원자적, 빠름)     */
/* this_cpu_inc(x)    → incl %gs:x (CPU내 원자적+주소 자동) */
/*                                                        */
/* local_t는 포인터로 전달해야 할 때, this_cpu_*는 심볼명일 때 */

/* 실사용: ftrace 이벤트 카운터 */
DEFINE_PER_CPU(local_t, ftrace_test_event_disable);

/* NMI 컨텍스트에서도 안전 (같은 CPU 내 원자적) */
local_inc(this_cpu_ptr(&ftrace_test_event_disable));
💡

local_t vs this_cpu_*: 대부분의 경우 this_cpu_inc(var)가 더 간결하고 동일한 성능을 제공합니다. local_t는 per-CPU 변수의 포인터를 함수에 전달해야 하거나, 기존 atomic_t API와 유사한 인터페이스가 필요한 경우에 사용합니다. 새 코드에서는 this_cpu_*를 우선 고려하세요.

CPU 핫플러그와 Per-CPU 생명주기

Per-CPU 데이터는 CPU가 온라인/오프라인이 되는 핫플러그 이벤트와 밀접하게 연관됩니다. 오프라인 CPU의 per-CPU 데이터를 올바르게 처리하지 않으면 데이터 손실이나 메모리 누수가 발생합니다.

CPU 핫플러그 시 per-CPU 데이터 생명주기 CPU Online per-CPU 영역 활성 cpuhp_setup_state() startup/teardown 콜백 등록 Active (동작 중) this_cpu_* 접근 가능 CPU Offline teardown 콜백 실행 teardown 콜백에서 수행할 작업 1. 오프라인 CPU의 per-CPU 카운터를 다른 CPU나 전역 변수로 이전 (drain) 2. per-CPU 큐에 남은 작업을 다른 CPU로 마이그레이션 3. 주의: per-CPU 메모리 자체는 해제되지 않음 (possible_cpu 기준 할당됨) — 데이터만 정리
/* CPU 핫플러그 콜백 등록 예제 */
static int my_cpu_online(unsigned int cpu)
{
    struct my_data *data = per_cpu_ptr(my_percpu_data, cpu);
    /* per-CPU 데이터 초기화 */
    data->count = 0;
    return 0;
}

static int my_cpu_offline(unsigned int cpu)
{
    struct my_data *data = per_cpu_ptr(my_percpu_data, cpu);
    /* 오프라인 CPU의 카운터를 전역에 합산 (drain) */
    atomic_long_add(data->count, &global_total);
    data->count = 0;
    return 0;
}

/* 초기화 시 콜백 등록 */
cpuhp_setup_state(CPUHP_AP_ONLINE_DYN,
                  "my_subsystem:online",
                  my_cpu_online,     /* startup 콜백 */
                  my_cpu_offline);   /* teardown 콜백 */

/* for_each_possible_cpu vs for_each_online_cpu */
/* possible: 시스템이 지원 가능한 모든 CPU (메모리는 항상 할당됨) */
/* online: 현재 활성화된 CPU만 (핫플러그 보호 필요) */
/* present: 물리적으로 존재하는 CPU */

성능 최적화 (Performance)

성능 벤치마크

8코어 x86-64 시스템에서 동일한 카운터 증가 작업의 동기화 방법별 성능 비교입니다.

방법처리량 (ops/sec)지연 (ns)CPU 확장성캐시라인 전이
공유 변수 + spinlock5M200역전 (코어↑→성능↓)매 접근 (bounce)
공유 변수 + atomic_t15M67포화 (~4코어에서 정체)매 접근 (MESI M→I)
per-CPU + get/put_cpu_var80M12선형없음
per-CPU + this_cpu_inc250M4완전 선형없음
per-CPU + raw_cpu_inc280M3.5완전 선형없음
코어 수 증가에 따른 처리량 확장성 0 500M 1000M 1500M 2000M ops/sec 1 2 4 8 16 CPU 코어 수 this_cpu_inc atomic_inc spinlock

perf로 per-CPU 효과 측정

# per-CPU 변수의 캐시 효과 확인
perf stat -e cache-misses,cache-references,L1-dcache-load-misses \
         -e instructions,cycles \
         -- ./benchmark_percpu

# false sharing 감지 (per-CPU 미사용 시 높은 HITM)
perf c2c record -a -- ./benchmark_shared
perf c2c report --stdio
# → Rmt HITM이 높으면 캐시라인 바운싱 → per-CPU 전환 필요

# per-CPU vs atomic 비교 시 IPC 차이 확인
perf stat -e cycles,instructions,cache-misses -r 5 -- ./bench_atomic
perf stat -e cycles,instructions,cache-misses -r 5 -- ./bench_percpu
# per-CPU: IPC > 3.0, cache-misses ≈ 0
# atomic: IPC < 1.0, cache-misses 높음 (MESI 전이 때문)
💡

벤치마크 결론: per-CPU + this_cpu_inc는 spinlock 대비 50배, atomic 대비 17배 빠릅니다. 더 중요한 것은 확장성입니다: spinlock은 코어가 증가하면 성능이 오히려 떨어지고, atomic은 정체하지만, per-CPU는 코어 수에 비례해 선형 확장합니다.

모범 사례 (Best Practices)

언제 사용하는가 — 결정 가이드

Per-CPU 변수 사용 결정 흐름도 쓰기가 빈번한가? (write-heavy) YES NO atomic_t / spinlock 정확한 실시간 합계가 필요한가? NO YES percpu_counter (배치 집계) IRQ/NMI 컨텍스트에서 접근? YES raw_cpu_* 또는 local_t NO this_cpu_* (권장) 가장 빠르고 안전한 기본 선택 적합한 패턴 • 통계 카운터 (패킷, 할당) • CPU-local 캐시/버퍼 • 작업 큐 (per-CPU pool) • 빈번한 이벤트 집계 • 프리리스트/오브젝트 풀 • RCU 콜백 큐
사용 패턴적합 여부권장 API이유
고빈도 통계 카운터최적this_cpu_inc()쓰기 99%, 읽기 1% → lock-free 극대화
임시 문자열 버퍼최적DEFINE_PER_CPU(char[256], buf)함수 호출마다 동적 할당 불필요
프리리스트 (SLUB)최적this_cpu_cmpxchg()lock-free fast path
오브젝트 캐시적합alloc_percpu()CPU별 핫 오브젝트 유지
근사 합계 (fs 프리블록)적합percpu_counter배치 집계로 정확도/성능 균형
CPU 간 메시지 전달부적합kfifo, IPIper-CPU는 공유 아닌 분리 목적
정확한 실시간 합계부적합atomic_t근사값만 제공, 엄격한 일관성 불가
메모리 극도 제한부적합공유 변수NR_CPUS배 메모리 사용
대규모 구조체 (>4KB)주의alloc_percpu()CPU당 4KB × 256CPU = 1MB, 메모리 비용 고려

주의사항과 안티패턴

Per-CPU 변수 안티패턴 Top 5 1. Preemption 없이 직접 접근 per_cpu(x, smp_processor_id())++ → CPU 이동 시 데이터 오염 해결: this_cpu_inc(x) 원자적 per-CPU 연산, preemption 자동 보호 2. 포인터 저장 후 다른 CPU에서 사용 ptr = this_cpu_ptr(x); schedule(); ptr→field++; 해결: preempt_disable() 구간 내 사용 또는 매번 this_cpu_ptr() 재호출 3. CPU 핫플러그 무시한 집계 for_each_online_cpu() 도중 CPU offline → 누락 해결: cpus_read_lock() 보호 또는 for_each_possible_cpu() 사용 4. 동적 할당 해제 누락 alloc_percpu() 후 free_percpu() 안 함 → 메모리 누수 해결: 에러 경로에서도 반드시 free_percpu() devm_alloc_percpu() 사용 시 자동 해제

안티패턴 상세 코드 예제

/* ━━━ 안티패턴 1: preemption 없이 per-CPU 접근 ━━━ */

/* ✗ 잘못된 코드 */
int cpu = smp_processor_id();  /* 여기서 CPU 번호 획득 */
per_cpu(my_counter, cpu)++;    /* BUG: 이 사이에 preemption → 다른 CPU로 이동! */

/* ✗ 마찬가지로 잘못됨 */
my_counter++;  /* per-CPU 변수를 일반 변수처럼 접근 */

/* ✓ 올바른 코드: this_cpu_* (단일 명령어, 원자적) */
this_cpu_inc(my_counter);

/* ✓ 올바른 코드: 명시적 preemption 보호 */
preempt_disable();
__this_cpu_inc(my_counter);    /* preempt off 보장 시 __ 버전 사용 가능 */
preempt_enable();

/* ✓ 올바른 코드: get/put 패턴 (여러 필드 접근 시) */
struct my_data *ptr = &get_cpu_var(my_data);
ptr->count++;
ptr->bytes += len;
put_cpu_var(my_data);
/* ━━━ 안티패턴 2: per-CPU 포인터를 sleep 가능 구간에서 사용 ━━━ */

/* ✗ 잘못된 코드 */
struct my_data *ptr = this_cpu_ptr(my_percpu_data);
kmalloc(..., GFP_KERNEL);  /* 여기서 sleep 가능! → CPU 이동 → ptr 무효 */
ptr->count++;              /* BUG: 다른 CPU의 데이터 수정할 수 있음 */

/* ✓ 올바른 코드: sleep 후 포인터 재획득 */
void *mem = kmalloc(..., GFP_KERNEL);  /* sleep 가능한 작업 먼저 */
if (!mem)
    return -ENOMEM;
preempt_disable();
struct my_data *ptr = this_cpu_ptr(my_percpu_data);
ptr->buffer = mem;
ptr->count++;
preempt_enable();

/* ✓ 더 나은 코드: this_cpu_write()로 개별 필드 접근 */
this_cpu_inc(my_percpu_data.count);  /* 단일 명령어, preemption 안전 */
/* ━━━ 안티패턴 3: raw_cpu_* 오용 ━━━ */

/* ✗ 잘못된 코드: 일반 태스크에서 raw_cpu_* 사용 */
raw_cpu_inc(my_counter);
/* BUG: 태스크 컨텍스트에서는 preemption 가능 → 다른 CPU로 이동 후
   돌아와서 증가하지 않은 채로 남을 수 있음 (read-modify-write 비원자적) */

/* ✓ raw_cpu_*는 IRQ/NMI 컨텍스트 또는 preempt_disable 구간에서만! */
irqreturn_t my_irq(int irq, void *dev)
{
    raw_cpu_inc(irq_counter);  /* OK: IRQ 컨텍스트 */
    return IRQ_HANDLED;
}
/* ━━━ 안티패턴 4: 동적 per-CPU 할당 해제 누락 ━━━ */

/* ✗ 잘못된 코드: 에러 경로에서 해제 누락 */
struct foo __percpu *a = alloc_percpu(struct foo);
struct bar __percpu *b = alloc_percpu(struct bar);
if (!b)
    return -ENOMEM;  /* BUG: a 해제 안 됨! */

/* ✓ 올바른 코드: goto 에러 처리 패턴 */
struct foo __percpu *a = alloc_percpu(struct foo);
if (!a)
    return -ENOMEM;
struct bar __percpu *b = alloc_percpu(struct bar);
if (!b) {
    free_percpu(a);
    return -ENOMEM;
}
/* ... 사용 ... */
free_percpu(b);
free_percpu(a);
/* ━━━ 안티패턴 5: 불필요한 per-CPU (과도한 사용) ━━━ */

/* ✗ 나쁜 설계: 거의 접근하지 않는 데이터를 per-CPU로 */
DEFINE_PER_CPU(struct huge_config, config);  /* 4KB × 256 CPU = 1MB 낭비 */
/* 이 설정은 부팅 시 한 번 쓰고 이후 읽기만 → per-CPU 불필요 */

/* ✓ 올바른 접근: read-mostly 데이터는 RCU + 포인터 */
static struct huge_config __rcu *config;
/* → 읽기: rcu_dereference(config) */
/* → 쓰기: rcu_assign_pointer() + synchronize_rcu() + kfree_rcu() */

설계 패턴

패턴구현커널 사용 예적용 조건
단순 카운터 this_cpu_inc(counter) vmstat, irq_stat 빈번한 증가, 드문 합산
배치 집계 percpu_counter ext4 프리블록, 소켓 수 빈번한 증가 + 근사 읽기 필요
프리리스트 this_cpu_cmpxchg() SLUB cpu_slab 오브젝트 풀 fast path
u64 통계 u64_stats_sync 네트워크 pcpu_lstats 32비트 호환 + 64비트 카운터
CPU-local 큐 DEFINE_PER_CPU(list_head) 워크큐, RCU 콜백 CPU별 독립 작업 큐
차등 카운터 per-CPU + 전역 임계값 zone_stat (vm_stat) 빈번한 갱신 + 전역 워터마크 검사

공유 변수 → per-CPU 마이그레이션 가이드

/* ━━━ Before: atomic_t 기반 카운터 ━━━ */
static atomic_t packet_count = ATOMIC_INIT(0);

void rx_packet(void)
{
    atomic_inc(&packet_count);  /* lock prefix → 코어↑ 시 성능↓ */
}

unsigned long get_packet_count(void)
{
    return atomic_read(&packet_count);
}

/* ━━━ After: per-CPU 카운터 ━━━ */
static DEFINE_PER_CPU(unsigned long, packet_count);

void rx_packet(void)
{
    this_cpu_inc(packet_count);  /* lock-free, 선형 확장 */
}

unsigned long get_packet_count(void)
{
    unsigned long total = 0;
    int cpu;
    for_each_possible_cpu(cpu)
        total += per_cpu(packet_count, cpu);
    return total;  /* 근사값 (대부분의 통계에 충분) */
}
⚠️

마이그레이션 체크리스트:

  • 쓰기 빈도 ≫ 읽기 빈도인지 확인 (아니면 per-CPU 이점 없음)
  • 읽기 측에서 근사값을 허용하는지 확인 (엄격한 일관성 필요 시 부적합)
  • CPU 핫플러그 시 오프라인 CPU 데이터 처리 코드 추가
  • 모듈 해제 시 free_percpu() 경로 확인
  • 메모리 증가량 계산: sizeof(데이터) × nr_cpu_ids

디버깅과 트러블슈팅

crash 덤프에서 per-CPU 데이터 확인

# crash 유틸리티에서 per-CPU 변수 분석

# 1. per-CPU 변수 주소 확인
crash> p &runqueues
PER-CPU DATA TYPE:
  struct rq runqueues;
PER-CPU ADDRESSES:
  [0]: ffff88803fc16d00
  [1]: ffff88803fd16d00
  [2]: ffff88807fc16d00
  [3]: ffff88807fd16d00

# 2. 특정 CPU의 per-CPU 변수 값 확인
crash> p runqueues:0
# → CPU 0의 런큐 구조체 출력

crash> p runqueues:1
# → CPU 1의 런큐 구조체 출력

# 3. 모든 CPU의 per-CPU 변수 합산
crash> p irq_stat
PER-CPU DATA TYPE:
  irq_cpustat_t irq_stat;
# 각 CPU의 __softirq_pending, __nmi_count 등 확인 가능

# 4. per-CPU 동적 할당 데이터 (포인터 추적)
crash> struct kmem_cache.cpu_slab ffff888003c00000
  cpu_slab = 0x1d8c0  # per-CPU 오프셋
crash> p ((struct kmem_cache_cpu *)per_cpu(0x1d8c0, 0))->freelist
# → CPU 0의 SLUB 프리리스트 확인

런타임 디버깅 도구

# ━━━ 1. /proc/vmstat — per-CPU 차등 카운터의 합산 결과 ━━━
cat /proc/vmstat | grep -E 'pgfault|pgmajfault|pswpin|pswpout'
# 이 값들은 per-CPU vm_event_states의 합산 → 정밀하지 않을 수 있음

# ━━━ 2. perf로 per-CPU 캐시 효과 측정 ━━━
# false sharing 감지 (per-CPU 미사용 시 높은 HITM)
perf c2c record -a -- sleep 10
perf c2c report --stdio | head -50
# Shared Data Cache Line Table에서 높은 Rmt HITM → per-CPU 전환 대상

# ━━━ 3. ftrace로 per-CPU 함수 호출 추적 ━━━
echo 'p:pcpu_alloc pcpu_alloc size=%di align=%si' > \
    /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/pcpu_alloc/enable
cat /sys/kernel/debug/tracing/trace_pipe
# per-CPU 동적 할당 실시간 추적

# ━━━ 4. BPF로 per-CPU 할당 분석 ━━━
bpftrace -e 'kprobe:pcpu_alloc {
    @sizes = hist(arg0);
    @callers[kstack] = count();
}'
# per-CPU 할당 크기 분포와 호출 스택 추적

# ━━━ 5. per-CPU 메모리 사용량 확인 ━━━
grep Percpu /proc/meminfo
# Percpu:         12288 kB

# 상세 분석 (CONFIG_PERCPU_STATS 필요)
cat /sys/kernel/debug/percpu/stats
# nr_chunks, nr_alloc, free_size, min_alloc_size, max_alloc_size 등

# ━━━ 6. per-CPU 섹션 크기 확인 ━━━
nm -S vmlinux | grep __per_cpu_start
nm -S vmlinux | grep __per_cpu_end
# 두 값의 차이 = 정적 per-CPU 섹션 크기 (모든 DEFINE_PER_CPU의 합)

# 또는 System.map 활용
awk '/per_cpu_start/{start=strtonum("0x"$1)} /per_cpu_end/{end=strtonum("0x"$1)} END{printf "per-CPU section: %d bytes\n", end-start}' System.map

흔한 문제와 해결

증상원인진단 방법해결
카운터 합계가 예상보다 작음 preemption 중 CPU 이동 (lost update) CONFIG_DEBUG_PREEMPT=y 활성화 → BUG 메시지 this_cpu_inc()로 변환
간헐적 데이터 오염 태스크 컨텍스트에서 raw_cpu_* 사용 CONFIG_DEBUG_PREEMPT=y, KASAN this_cpu_*로 변환 또는 preempt_disable 추가
모듈 언로드 시 메모리 누수 free_percpu() 누락 kmemleak report 확인 에러 경로 포함 모든 경로에서 free_percpu()
CPU 오프라인 후 카운터 누락 핫플러그 teardown 콜백 미등록 CPU offline → 집계 감소 확인 cpuhp_setup_state()로 drain 콜백 등록
per-CPU 할당 실패 (-ENOMEM) pcpu_chunk 고갈 (대규모 할당 또는 모듈 다수) /proc/meminfo의 Percpu 값 확인 할당 크기 줄이기, 불필요한 모듈 제거
BUG: using smp_processor_id() in preemptible code preemptible 구간에서 smp_processor_id() 호출 커널 로그 (WARN 스택 트레이스) this_cpu_* API 사용 또는 raw_smp_processor_id() (디버그용만)
💡

디버그 커널 옵션: per-CPU 관련 버그를 조기에 잡기 위해 개발 중에는 다음 옵션을 활성화하세요:

  • CONFIG_DEBUG_PREEMPT=y — preemptible 구간에서 smp_processor_id() 호출 시 경고
  • CONFIG_PERCPU_STATS=y/sys/kernel/debug/percpu/ 통계 제공
  • CONFIG_DEBUG_VM=y — per-CPU 할당자 일관성 검사
  • CONFIG_KMEMLEAK=y — 동적 per-CPU 할당 메모리 누수 감지

실습 예제

실습 1: 기본 per-CPU 카운터

/* percpu_basic.c — 기본 per-CPU 카운터 모듈 */
#include <linux/module.h>
#include <linux/percpu.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>

DEFINE_PER_CPU(unsigned long, test_counter);

static int percpu_show(struct seq_file *m, void *v)
{
    int cpu;
    unsigned long total = 0;

    seq_printf(m, "=== Per-CPU Counter Status ===\n");

    for_each_possible_cpu(cpu) {
        unsigned long val = per_cpu(test_counter, cpu);
        seq_printf(m, "  CPU %d: %lu%s\n", cpu, val,
                   cpu_online(cpu) ? "" : " (offline)");
        total += val;
    }
    seq_printf(m, "  Total: %lu\n", total);
    return 0;
}

static ssize_t percpu_write(struct file *file,
                            const char __user *buf,
                            size_t count, loff_t *ppos)
{
    int i;
    /* 쓰기 시 현재 CPU에서 10000회 증가 */
    for (i = 0; i < 10000; i++)
        this_cpu_inc(test_counter);

    pr_info("percpu_test: incremented 10000 on CPU %d\n",
            smp_processor_id());
    return count;
}

static int percpu_open(struct inode *inode, struct file *file)
{
    return single_open(file, percpu_show, NULL);
}

static const struct proc_ops percpu_fops = {
    .proc_open    = percpu_open,
    .proc_read    = seq_read,
    .proc_write   = percpu_write,
    .proc_lseek   = seq_lseek,
    .proc_release = single_release,
};

static int __init percpu_init(void)
{
    proc_create("percpu_test", 0666, NULL, &percpu_fops);
    pr_info("percpu_test: loaded (nr_cpu_ids=%d)\n", nr_cpu_ids);
    return 0;
}

static void __exit percpu_exit(void)
{
    remove_proc_entry("percpu_test", NULL);
    pr_info("percpu_test: unloaded\n");
}

module_init(percpu_init);
module_exit(percpu_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Per-CPU counter demonstration");
# 테스트 방법
sudo insmod percpu_basic.ko
cat /proc/percpu_test            # CPU별 카운터 확인

# 여러 CPU에서 병렬로 증가시키기
for cpu in 0 1 2 3; do
    taskset -c $cpu sh -c 'echo x > /proc/percpu_test' &
done
wait

cat /proc/percpu_test            # 각 CPU에 10000씩 분산된 것 확인
# CPU 0: 10000
# CPU 1: 10000
# CPU 2: 10000
# CPU 3: 10000
# Total: 40000

실습 2: 동적 할당과 percpu_counter

/* percpu_dynamic.c — 동적 할당 + percpu_counter 모듈 */
#include <linux/module.h>
#include <linux/percpu.h>
#include <linux/percpu_counter.h>
#include <linux/kthread.h>
#include <linux/delay.h>

struct my_stats {
    u64 events;
    u64 bytes;
    struct u64_stats_sync syncp;
};

static struct my_stats __percpu *stats;
static struct percpu_counter total_ops;

static void simulate_work(void)
{
    struct my_stats *s = this_cpu_ptr(stats);

    /* u64_stats_sync로 64비트 tearing 방지 */
    u64_stats_update_begin(&s->syncp);
    s->events++;
    s->bytes += 1024;
    u64_stats_update_end(&s->syncp);

    /* percpu_counter: 배치 업데이트 */
    percpu_counter_inc(&total_ops);
}

static void print_stats(void)
{
    int cpu;
    u64 total_events = 0, total_bytes = 0;

    for_each_online_cpu(cpu) {
        struct my_stats *s = per_cpu_ptr(stats, cpu);
        unsigned int start;
        u64 ev, by;

        do {
            start = u64_stats_fetch_begin(&s->syncp);
            ev = s->events;
            by = s->bytes;
        } while (u64_stats_fetch_retry(&s->syncp, start));

        pr_info("  CPU%d: events=%llu bytes=%llu\n", cpu, ev, by);
        total_events += ev;
        total_bytes  += by;
    }
    pr_info("  Total: events=%llu bytes=%llu\n", total_events, total_bytes);
    pr_info("  percpu_counter approx: %lld (exact: %lld)\n",
            percpu_counter_read_positive(&total_ops),
            percpu_counter_sum(&total_ops));
}

static int __init mod_init(void)
{
    int i;

    stats = alloc_percpu(struct my_stats);
    if (!stats)
        return -ENOMEM;

    if (percpu_counter_init(&total_ops, 0, GFP_KERNEL)) {
        free_percpu(stats);
        return -ENOMEM;
    }

    for (i = 0; i < 1000; i++)
        simulate_work();

    print_stats();
    return 0;
}

static void __exit mod_exit(void)
{
    percpu_counter_destroy(&total_ops);
    free_percpu(stats);   /* 반드시 해제! */
}

module_init(mod_init);
module_exit(mod_exit);
MODULE_LICENSE("GPL");

실습 3: CPU 핫플러그 대응

/* percpu_hotplug.c — CPU 핫플러그 시 per-CPU 데이터 마이그레이션 */
#include <linux/module.h>
#include <linux/percpu.h>
#include <linux/cpuhotplug.h>

static DEFINE_PER_CPU(unsigned long, hp_counter);
static atomic_long_t drained_total = ATOMIC_LONG_INIT(0);
static enum cpuhp_state hp_state;

static int my_cpu_online(unsigned int cpu)
{
    per_cpu(hp_counter, cpu) = 0;
    pr_info("hp_percpu: CPU%u online, counter reset\n", cpu);
    return 0;
}

static int my_cpu_offline(unsigned int cpu)
{
    unsigned long val = per_cpu(hp_counter, cpu);
    atomic_long_add(val, &drained_total);
    per_cpu(hp_counter, cpu) = 0;
    pr_info("hp_percpu: CPU%u offline, drained %lu to global\n", cpu, val);
    return 0;
}

static int __init hp_init(void)
{
    int ret;
    ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN,
                            "test/percpu_hp:online",
                            my_cpu_online, my_cpu_offline);
    if (ret < 0)
        return ret;
    hp_state = ret;
    pr_info("hp_percpu: loaded (state=%d)\n", ret);
    return 0;
}

static void __exit hp_exit(void)
{
    cpuhp_remove_state(hp_state);
    pr_info("hp_percpu: drained_total=%ld\n",
            atomic_long_read(&drained_total));
}

module_init(hp_init);
module_exit(hp_exit);
MODULE_LICENSE("GPL");
# CPU 핫플러그 테스트
sudo insmod percpu_hotplug.ko

# CPU 1 오프라인
echo 0 | sudo tee /sys/devices/system/cpu/cpu1/online
# dmesg → "hp_percpu: CPU1 offline, drained X to global"

# CPU 1 다시 온라인
echo 1 | sudo tee /sys/devices/system/cpu/cpu1/online
# dmesg → "hp_percpu: CPU1 online, counter reset"

sudo rmmod percpu_hotplug

실습 4: per-CPU vs atomic 벤치마크

/* percpu_bench.c — per-CPU vs atomic 성능 비교 */
#include <linux/module.h>
#include <linux/percpu.h>
#include <linux/kthread.h>
#include <linux/sched/clock.h>

#define ITERATIONS  10000000

static DEFINE_PER_CPU(unsigned long, pcpu_cnt);
static atomic_long_t atomic_cnt = ATOMIC_LONG_INIT(0);

static int bench_percpu(void *data)
{
    int i;
    for (i = 0; i < ITERATIONS; i++)
        this_cpu_inc(pcpu_cnt);
    return 0;
}

static int bench_atomic(void *data)
{
    int i;
    for (i = 0; i < ITERATIONS; i++)
        atomic_long_inc(&atomic_cnt);
    return 0;
}

static void run_bench(const char *name,
                      int (*fn)(void *), int nr_threads)
{
    struct task_struct **tasks;
    u64 start, end;
    int i;

    tasks = kmalloc_array(nr_threads, sizeof(*tasks), GFP_KERNEL);
    if (!tasks)
        return;

    start = local_clock();
    for (i = 0; i < nr_threads; i++)
        tasks[i] = kthread_run(fn, NULL, "bench%d", i);
    for (i = 0; i < nr_threads; i++)
        kthread_stop(tasks[i]);
    end = local_clock();

    pr_info("[%s] %d threads × %d ops: %llu ns (%.1f ns/op)\n",
            name, nr_threads, ITERATIONS,
            end - start,
            (double)(end - start) / ((u64)nr_threads * ITERATIONS));
    kfree(tasks);
}

static int __init bench_init(void)
{
    pr_info("=== Per-CPU vs Atomic Benchmark ===\n");
    run_bench("per-CPU", bench_percpu, num_online_cpus());
    run_bench("atomic", bench_atomic, num_online_cpus());
    return -EAGAIN;  /* 측정만 하고 언로드 */
}

module_init(bench_init);
MODULE_LICENSE("GPL");

실습 5: /proc로 per-CPU 상태 관찰

# per-CPU 관련 커널 정보 확인 명령어 모음

# 1. per-CPU 할당자 상태 확인
cat /proc/meminfo | grep -i percpu
# Percpu:         12288 kB  ← per-CPU 영역 총 메모리

# 2. per-CPU chunk 상세 정보 (CONFIG_PERCPU_STATS=y 필요)
cat /sys/kernel/debug/percpu/chunks
# chunk_idx  nr_alloc  free_bytes  contig_bytes  ...

# 3. CPU별 vmstat 카운터 (per-CPU 차등 카운터)
cat /proc/vmstat | head -20
# nr_free_pages, pgfault, pgmajfault 등은 모두 per-CPU 합산값

# 4. CPU별 인터럽트 통계 (per-CPU irq_stat 기반)
cat /proc/interrupts

# 5. CPU별 네트워크 통계 (per-CPU softnet_data)
cat /proc/net/softnet_stat
# 각 행이 한 CPU의 통계 (processed, dropped, time_squeeze)

# 6. SLUB per-CPU 캐시 상태
cat /sys/kernel/slab/kmalloc-64/cpu_slabs
# CPU별 활성 slab 수

# 7. per-CPU 변수 주소 확인 (System.map)
grep __per_cpu_start /proc/kallsyms
grep __per_cpu_end   /proc/kallsyms
# → 두 값의 차이 = per-CPU 섹션 크기

# 8. 특정 CPU에서 실행하여 per-CPU 확인
taskset -c 0 cat /proc/self/status | grep Cpus_allowed