Per-CPU 변수
Linux 커널의 Per-CPU 변수는 각 CPU가 독립적인 데이터 복사본을 가지도록 하여 캐시(Cache)라인 바운싱과 false sharing을 줄이고 lock contention을 제거하는 고성능 기법입니다. 이 문서는 DEFINE_PER_CPU, this_cpu_* fast path, alloc_percpu 기반 동적 할당, 전역 집계 시점의 동기화 전략, 네트워크/메모리 서브시스템 사례와 NUMA 환경 튜닝 포인트까지 상세히 설명합니다.
핵심 요약
- 독립 복사본 — 각 CPU가 자신만의 변수 복사본을 가집니다.
- Lock-free — 동일 CPU 내에서는 동기화가 필요 없습니다.
- 캐시 효율 — False sharing이 없어 캐시 성능이 극대화됩니다.
- 빠른 접근 — this_cpu_* 연산은 원자적(Atomic)이면서도 매우 빠릅니다.
- 통계 집계 — 네트워크 패킷(Packet) 수, 메모리 할당 통계 등에 최적입니다.
단계별 이해
- 핵심 요소 확인
이 문서에서 다루는 자료구조/API를 먼저 정리합니다. - 처리 흐름 추적
요청 시작부터 완료까지 실행 경로를 순서대로 확인합니다. - 문제 지점 점검
실패 경로, 경합(Contention) 구간, 성능 병목(Bottleneck)을 체크합니다.
개요 (Overview)
Per-CPU 변수는 SMP(Symmetric Multi-Processing) 시스템에서 성능을 극대화하는 핵심 기법입니다:
- 캐시 친화성 — 각 CPU가 자신의 캐시 라인(Cache Line)에만 접근하므로 캐시 미스가 최소화됩니다.
- False sharing 회피 — 다른 CPU가 같은 캐시 라인을 수정하는 일이 없습니다.
- Lock contention 제거 — spinlock 경합이 없어 대기 시간(Latency)이 사라집니다.
- 확장성 — CPU 수가 증가해도 성능이 선형적으로 증가합니다.
setup_per_cpu_areas()가 .data..percpu를 각 CPU별로 복사하며, __per_cpu_offset[]에 오프셋(Offset)을 저장합니다.실사용 빈도: 커널 통계 카운터의 90% 이상이 per-CPU 변수를 사용합니다. 네트워크 패킷 카운트, 메모리 할당 통계, 스케줄러(Scheduler) 런큐(Runqueue), SLAB 할당자 등 모든 고성능 서브시스템에서 필수적입니다.
내부 구조 (Internals)
부팅 과정(Boot Process)에서 커널은 각 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 세그먼트 레지스터(Register)
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와 커널 진입: 사용자 공간(User Space)에서 커널로 진입할 때(syscall, 인터럽트(Interrupt)) swapgs 명령이 실행되어 MSR_GS_BASE를 사용자용 → 커널용 per-CPU 베이스로 교체합니다. 이로써 커널 코드의 %gs:offset 접근이 현재 CPU의 per-CPU 영역을 가리키게 됩니다. ARM64에서는 TPIDR_EL1 레지스터가 동일 역할을 합니다.
어셈블리(Assembly) 수준 비교: per-CPU vs atomic vs spinlock
this_cpu_inc()가 왜 빠른지, 실제 생성되는 기계어(Machine Code) 명령어를 비교합니다:
; ── 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의 물리 페이지(Page) 할당 단위 (4KB~2MB) | pcpu_atom_size |
Lazy population: 동적 청크는 가상 주소(Virtual Address)만 먼저 예약하고, 실제 물리 페이지는 pcpu_populate_chunk()에서 할당 시점에 매핑(Mapping)합니다. 이로써 모든 CPU에 즉시 물리 메모리(Physical Memory)를 할당하지 않아도 되며, 특히 CPU 수가 많은 시스템에서 메모리 낭비를 줄입니다.
기본 API (Basic API)
DEFINE_PER_CPU 매크로(Macro)
/* 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) | 읽기 전용(Read-Only)에 가까운 변수 | 읽기 캐시라인 최적화, 쓰기는 드묾 |
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 고정을 보장합니다. 일반 태스크(Task) 컨텍스트에서 사용합니다. - raw_cpu_* — preemption이 이미 비활성화된 컨텍스트(인터럽트 핸들러(Handler), 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)
네트워크 통계
네트워크 디바이스의 패킷/바이트 통계는 매 패킷마다 갱신되므로, per-CPU 카운터가 필수적입니다. u64_stats_sync는 32비트 아키텍처에서 64비트 카운터의 torn read(절반만 읽히는 현상)를 방지하는 시퀀스 카운터입니다. 64비트 아키텍처에서는 no-op으로 컴파일되어 추가 비용이 없습니다.
u64_stats_sync 원리: Writer가 u64_stats_update_begin()에서 시퀀스 카운터를 홀수로 만들고, u64_stats_update_end()에서 짝수로 복원합니다. Reader는 읽기 전후 시퀀스를 비교하여 Writer가 중간에 끼어들었으면 재시도합니다. per-CPU 특성상 Writer는 현재 CPU 하나뿐이므로 write-side 잠금(Lock)이 불필요합니다.
/* 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);
}
통계 읽기(예: dev_get_stats())는 모든 CPU의 카운터를 합산합니다:
/* 전체 CPU 합산 읽기 패턴 */
void dev_get_stats(struct net_device *dev, struct rtnl_link_stats64 *s)
{
int cpu;
for_each_possible_cpu(cpu) {
struct pcpu_lstats *stats = per_cpu_ptr(dev->lstats, cpu);
u64 packets, bytes;
unsigned int start;
do {
start = u64_stats_fetch_begin(&stats->syncp);
packets = stats->packets;
bytes = stats->bytes;
} while (u64_stats_fetch_retry(&stats->syncp, start));
s->rx_packets += packets;
s->rx_bytes += bytes;
}
}
메모리 할당자 (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 구조체(Struct) 중 하나(수 KB)입니다. DEFINE_PER_CPU_SHARED_ALIGNED로 선언하여 L3 캐시라인 경계에 정렬하므로, 스케줄러가 현재 CPU 런큐에 잠금 없이 접근할 때 다른 CPU 런큐와의 false sharing이 방지됩니다. 로드밸런싱(pull migration)에서만 타 CPU의 런큐를 접근하며, 이때는 rq->lock으로 직렬화(Serialization)합니다.
/* 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
softnet_data는 네트워크 스택(Network Stack)의 per-CPU 중심 구조체로, 각 CPU가 독립적으로 패킷을 수신·처리합니다. poll_list에 NAPI 디바이스가 등록되면 softirq에서 폴링(Polling)하여 패킷을 가져오고, input_pkt_queue에 쌓인 패킷은 process_queue로 이동 후 프로토콜 스택으로 전달됩니다. RPS(Receive Packet Steering)가 활성화되면 다른 CPU에서 IPI를 통해 패킷을 전달받아 received_rps 카운터가 증가합니다.
모니터링: /proc/net/softnet_stat에서 CPU별 통계를 확인할 수 있습니다. time_squeeze 값이 증가하면 해당 CPU의 NAPI budget이 부족하다는 신호로, net.core.netdev_budget 튜닝이 필요합니다.
/* 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별 통계 확인 가능 */
인터럽트 통계
인터럽트 통계 구조체 irq_cpustat_t는 ____cacheline_aligned로 선언되어 CPU 간 false sharing을 방지합니다. 인터럽트 핸들러에서는 이미 선점(Preemption)이 비활성화되어 있으므로 raw_cpu_inc()를 안전하게 사용할 수 있습니다. NMI 카운터는 NMI 컨텍스트에서도 동일 CPU의 per-CPU 변수를 수정하므로, 단일 명령어(incl)로 원자성이 보장되어야 합니다. /proc/interrupts와 /proc/stat의 intr 행은 모든 CPU의 per-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)
워크큐는 CPU당 normal과 high-priority 두 개의 worker_pool을 유지합니다. queue_work()는 현재 CPU의 풀에 작업을 추가하여 캐시 친화적 실행을 보장합니다. WQ_UNBOUND 플래그를 지정하면 특정 CPU에 바인딩하지 않고 시스템 전체의 unbound 풀에서 실행되므로, 지연 시간 민감 작업보다 처리량(Throughput) 위주 작업에 적합합니다. CPU 오프라인 시 해당 CPU의 대기 작업은 다른 CPU의 풀로 마이그레이션됩니다.
/* 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)
페이지 폴트(Page Fault), 페이지 할당/해제 등 VM 이벤트는 초당 수십만 회 발생할 수 있어 per-CPU 카운터가 필수입니다. vm_event_states는 단순 누적 카운터지만, zone_stat은 차등 카운터(differential counter) 방식을 사용합니다: 각 CPU가 로컬 카운터에 증감값을 누적하다가 임계값(stat_threshold, 보통 zone 크기에 비례)을 초과하면 전역 카운터에 한꺼번에 반영합니다. 이로써 쓰기 경합을 최소화하면서도 /proc/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 핫플러그(Hotplug)를 고려해야 합니다.
기본 집계 패턴
단순 정수 카운터는 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를 읽으면 중간에 갱신이 끼어들어 torn read가 발생할 수 있습니다. u64_stats_sync 시퀀스 카운터를 이용해 writer/reader 간 tearing을 방지합니다. 64비트 아키텍처에서는 no-op으로 컴파일됩니다.
u64_stats_sync의 writer/reader 패턴과 상세 코드는 네트워크 통계 섹션을 참고하세요.
일관성 주의: 집계 도중에도 다른 CPU가 값을 변경할 수 있으므로, 읽은 합계는 근사값입니다. 엄격한 실시간(Real-time) 일관성이 요구되는 경우(예: 결제 금액, 보안 카운터)에는 per-CPU 변수가 부적합하며, 대신 atomic 변수나 seqlock을 고려하십시오.
percpu_counter: 배치 집계 카운터
percpu_counter는 per-CPU 로컬 카운터와 전역 카운터를 결합한 자료구조로, 빈번한 업데이트에서 per-CPU 성능을 유지하면서도 전역 값의 근사치를 효율적으로 제공합니다. 파일시스템(Filesystem)의 프리 블록 수, 네트워크 소켓(Socket) 수 등에 광범위하게 사용됩니다.
/* 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_*를 우선 고려하세요.
percpu_ref: Per-CPU 참조 카운팅
percpu_ref는 참조 카운터의 증감을 per-CPU 변수로 분산하여 읽기/참조 경로의 확장성을 극대화하는 구조입니다. 일반 atomic_t 참조 카운터는 모든 CPU가 동일 캐시라인을 경합하지만, percpu_ref는 각 CPU가 로컬 카운터만 증감하므로 캐시라인 바운싱이 발생하지 않습니다. 객체 해제 시에는 per-CPU 모드에서 원자적 모드로 전환(kill)하여 정확한 참조 수를 확인합니다.
percpu_ref API
| 함수 | 동작 | 비고 |
|---|---|---|
percpu_ref_init(&ref, release, flags, gfp) | per-CPU 참조 카운터 초기화 | release: 카운트=0 도달 시 콜백 |
percpu_ref_get(&ref) | 참조 증가 (per-CPU 모드: this_cpu_inc) | ~1ns, lock-free |
percpu_ref_put(&ref) | 참조 감소 | 0 도달 시 release 콜백 호출 |
percpu_ref_tryget_live(&ref) | kill되지 않은 경우에만 참조 증가 | I/O 제출 경로에서 사용 |
percpu_ref_kill(&ref) | per-CPU → atomic 전환 + 초기 참조 해제 | RCU GP 후 전환 완료 |
percpu_ref_exit(&ref) | per-CPU 메모리 해제 | release 콜백 이후 호출 |
percpu_ref_is_dying(&ref) | kill 여부 확인 | 새 참조 획득 차단 판단용 |
내부 메커니즘
percpu_ref_kill()은 다음 순서로 동작합니다:
- DEAD 플래그 설정 —
percpu_ref_tryget_live()가 실패하도록 합니다. - RCU GP 대기 —
call_rcu()로 콜백을 등록하여, 진행 중인 모든 per-CPU get/put이 완료될 때까지 대기합니다. - per-CPU 합산 — RCU GP 완료 후, 모든 CPU의 로컬 카운터를
atomic_long_t에 합산합니다. - 모드 전환 — 이후 모든 get/put은 atomic 경로를 사용합니다.
/* lib/percpu-refcount.c — percpu_ref 핵심 구조 */
struct percpu_ref_data {
atomic_long_t count; /* atomic 모드 카운터 */
percpu_ref_func_t *release; /* 0 도달 시 콜백 */
percpu_ref_func_t *confirm_switch; /* 모드 전환 완료 콜백 */
struct rcu_head rcu; /* RCU GP 대기용 */
struct percpu_ref *ref;
};
struct percpu_ref {
unsigned long percpu_count_ptr; /* per-CPU 카운터 또는 플래그 */
struct percpu_ref_data *data;
};
/* get/put 핫 경로 */
static inline void percpu_ref_get(struct percpu_ref *ref)
{
unsigned long __percpu *percpu_count;
rcu_read_lock();
/* percpu_count_ptr의 하위 비트로 모드 판별 */
if (likely(!(ref->percpu_count_ptr & __PERCPU_REF_ATOMIC)))
this_cpu_inc(*percpu_count); /* per-CPU fast path */
else
atomic_long_inc(&ref->data->count); /* atomic fallback */
rcu_read_unlock();
}
/* 실사용: block I/O (blk-mq) */
struct request_queue {
struct percpu_ref q_usage_counter; /* I/O 제출 참조 */
/* ... */
};
/* I/O 제출 시: percpu_ref_tryget_live(&q->q_usage_counter)
* → 큐가 dying이면 실패 → I/O 거부
* → 성공 시 per-CPU 카운터 증가 (매우 빠름)
*
* 큐 해제 시: percpu_ref_kill(&q->q_usage_counter)
* → 진행 중인 I/O 완료 후 카운트=0 → 큐 메모리 해제
*/
실사용 사례
| 서브시스템 | 구조체 | 용도 |
|---|---|---|
| blk-mq | request_queue.q_usage_counter | I/O 제출 경로 참조 (큐 해제 보호) |
| cgroup | cgroup.self.refcnt | cgroup 계층 참조 카운팅 |
| io_uring | io_ring_ctx.refs | 링 컨텍스트 수명 관리 |
| Device Mapper | mapped_device.io_counter | DM 디바이스 I/O 추적 |
| NVMe | nvme_ns.kref | 네임스페이스(Namespace) 수명 관리 |
주의: percpu_ref는 0 감지가 필요한 참조 카운터에만 사용합니다. 단순 통계 카운터(패킷 수, 이벤트 수 등)에는 this_cpu_inc() 또는 percpu_counter가 적합합니다. percpu_ref의 kill/전환 비용은 RCU GP(수 ms)이므로, 빈번한 생성/해제가 필요한 경우에는 atomic_t + refcount_t가 더 효율적입니다.
CPU 핫플러그와 Per-CPU 생명주기
Per-CPU 데이터는 CPU가 온라인/오프라인이 되는 핫플러그 이벤트와 밀접하게 연관됩니다. 오프라인 CPU의 per-CPU 데이터를 올바르게 처리하지 않으면 데이터 손실이나 메모리 누수가 발생합니다.
집계 시 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 핫플러그 콜백 등록 예제 */
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 극대화 |
| 임시 문자열 버퍼(Buffer) | 최적 | 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별 독립 작업 큐(Workqueue) |
| 차등 카운터 | per-CPU + 전역 임계값 | zone_stat (vm_stat) | 빈번한 갱신 + 전역 워터마크(Watermark) 검사 |
공유 변수 → 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
디버깅(Debugging)과 트러블슈팅
crash 덤프(Dump)에서 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
참고 자료
커널 공식 문서
- this_cpu operations — this_cpu_read/write/add 등 Per-CPU 연산 API 공식 가이드
- Per-CPU variables — DEFINE_PER_CPU, alloc_percpu, Per-CPU 할당기 문서
- atomic_t — Pair Ordering and Semantics — per-CPU local_t와 atomic_t의 차이
- Lock types and their rules — preempt_disable/enable과 Per-CPU 접근 규칙
LWN.net 심층 기사
- Per-CPU variables and the realtime tree (2007) — PREEMPT_RT에서의 Per-CPU 접근 규칙
- What is RCU, Fundamentally? (2007) — RCU per-CPU 콜백 큐와 rcu_data
- Lockless patterns: an introduction to compare-and-swap (2014) — Per-CPU + CAS 조합 패턴
- Concurrency bugs should fear the big bad data-race detector (2019) — KCSAN으로 Per-CPU 접근 오류 탐지
학술 자료 및 외부 참고
- Paul McKenney — "Is Parallel Programming Hard?" — Chapter 5: Counting (Per-CPU 카운터 패턴)
- 커널 소스:
include/linux/percpu.h,include/linux/percpu-defs.h,mm/percpu.c - SLUB per-CPU 캐시:
mm/slub.c—kmem_cache_cpu구조체와 fast path - perf c2c:
perf c2c record/report— false sharing 감지 도구