Per-CPU 변수
Linux 커널의 Per-CPU 변수는 각 CPU가 독립적인 데이터 복사본을 가지도록 하여 캐시라인 바운싱과 false sharing을 줄이고 lock contention을 제거하는 고성능 기법입니다. 이 문서는 DEFINE_PER_CPU, this_cpu_* fast path, alloc_percpu 기반 동적 할당, 전역 집계 시점의 동기화 전략, 네트워크/메모리 서브시스템 사례와 NUMA 환경 튜닝 포인트까지 상세히 설명합니다.
핵심 요약
- 독립 복사본 — 각 CPU가 자신만의 변수 복사본을 가집니다.
- Lock-free — 동일 CPU 내에서는 동기화가 필요 없습니다.
- 캐시 효율 — False sharing이 없어 캐시 성능이 극대화됩니다.
- 빠른 접근 — this_cpu_* 연산은 원자적이면서도 매우 빠릅니다.
- 통계 집계 — 네트워크 패킷 수, 메모리 할당 통계 등에 최적입니다.
단계별 이해
- 핵심 요소 확인
이 문서에서 다루는 자료구조/API를 먼저 정리합니다. - 처리 흐름 추적
요청 시작부터 완료까지 실행 경로를 순서대로 확인합니다. - 문제 지점 점검
실패 경로, 경합 구간, 성능 병목을 체크합니다.
개요 (Overview)
Per-CPU 변수는 SMP(Symmetric Multi-Processing) 시스템에서 성능을 극대화하는 핵심 기법입니다:
- 캐시 친화성 — 각 CPU가 자신의 캐시 라인에만 접근하므로 캐시 미스가 최소화됩니다.
- False sharing 회피 — 다른 CPU가 같은 캐시 라인을 수정하는 일이 없습니다.
- Lock contention 제거 — spinlock 경합이 없어 대기 시간이 사라집니다.
- 확장성 — 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_* 연산이 단일 명령어로 컴파일됩니다.
/* 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_inc | 1 | 불필요 | 없음 (CPU-local) | ~1 |
| atomic_inc | 1 | 필요 | MESI S→M 전이 | 10~40 |
| spinlock + inc | 3+ | 필요 | 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)을 포함합니다.
/* 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 chunk | first chunk 부족 시 vmalloc에서 런타임 생성 | 가변 (필요에 따라 확장) |
| Unit | 청크 내 각 CPU에 할당된 메모리 영역 | pcpu_unit_size (보통 32KB~256KB) |
| Atom | unit의 물리 페이지 할당 단위 (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 변수를 조작하지만, 사용 컨텍스트가 다릅니다:
- this_cpu_* — 컴파일러·아키텍처 수준에서 preemption-safe를 보장합니다. 내부적으로
preempt_disable()/preempt_enable()을 호출하지 않고, x86%gs세그먼트 레지스터 등 CPU-local 어드레싱을 이용해 현재 CPU 고정을 보장합니다. 일반 태스크 컨텍스트에서 사용합니다. - raw_cpu_* — preemption이 이미 비활성화된 컨텍스트(인터럽트 핸들러, NMI, 소프트IRQ 등)에서만 사용합니다. preemption 비활성화를 전제하므로 추가 보호 없이 직접 접근합니다.
| 함수 | 사용 컨텍스트 | 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)
/* 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() 호출이 잠금 없이 완료됩니다.
/* 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 성능을 유지하면서도 전역 값의 근사치를 효율적으로 제공합니다. 파일시스템의 프리 블록 수, 네트워크 소켓 수 등에 광범위하게 사용됩니다.
/* 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 핫플러그 콜백 등록 예제 */
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 확장성 | 캐시라인 전이 |
|---|---|---|---|---|
| 공유 변수 + spinlock | 5M | 200 | 역전 (코어↑→성능↓) | 매 접근 (bounce) |
| 공유 변수 + atomic_t | 15M | 67 | 포화 (~4코어에서 정체) | 매 접근 (MESI M→I) |
| per-CPU + get/put_cpu_var | 80M | 12 | 선형 | 없음 |
| per-CPU + this_cpu_inc | 250M | 4 | 완전 선형 | 없음 |
| per-CPU + raw_cpu_inc | 280M | 3.5 | 완전 선형 | 없음 |
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)
언제 사용하는가 — 결정 가이드
| 사용 패턴 | 적합 여부 | 권장 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, IPI | per-CPU는 공유 아닌 분리 목적 |
| 정확한 실시간 합계 | 부적합 | atomic_t | 근사값만 제공, 엄격한 일관성 불가 |
| 메모리 극도 제한 | 부적합 | 공유 변수 | NR_CPUS배 메모리 사용 |
| 대규모 구조체 (>4KB) | 주의 | alloc_percpu() | CPU당 4KB × 256CPU = 1MB, 메모리 비용 고려 |
주의사항과 안티패턴
안티패턴 상세 코드 예제
/* ━━━ 안티패턴 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