# Per-CPU 메모리
관련 소스:mm/percpu.c,mm/percpu-internal.h,mm/percpu-vm.c,include/linux/percpu.h
Per-CPU 할당기는 커널 내에서 CPU별로 독립적인 메모리 영역을 관리하는 할당기입니다. 각 CPU가 고유한 데이터 영역을 가지므로 락 없이 빠르게 접근할 수 있으며, 캐시 라인 충돌(False Sharing)을 근본적으로 방지합니다. Linux 7.0에서 Per-CPU 할당기는 static 변수 영역과 동적 할당 영역을 모두 담당하며, slab 할당기의 초기화 이전에도 동작할 수 있어 커널 부팅 과정에서 핵심적인 역할을 수행합니다.
할당기는 chunk 단위로 메모리를 관리합니다. 각 chunk는 여러 unit으로 나뉘며, 각 unit은 특정 CPU에 대응합니다. NUMA 환경에서는 unit 간 물리적 거리가 다를 수 있으므로 group 단위로 배치됩니다. bitmap 기반의 메타데이터를 사용하여 빈 영역을 추적하며, metadata block 힌트를 통해 대규모 비트맵 스캔 없이 빠르게 할당 위치를 찾습니다.
memcg-aware 할당은 __GFP_ACCOUNT가 붙은 경우에만 pcpu_memcg_pre_alloc_hook()을 통해 계정되며, 계정된 할당과 비계정 할당은 서로 다른 chunk 집합을 사용합니다.
일상 비유: Per-CPU 할당기는 도서관의 "개인 사물함"과 같습니다. 각 CPU(사람)마다 전용 사물함(Per-CPU 영역)이 있어 다른 사람의 사물함을 쓸 필요가 없으므로 락이 불필요합니다. 사물함은 더 큰 구역(chunk)으로 나뉘고, 각 구역은 여러 사람이 공유하지만 각자 다른 칸(unit)을 사용합니다.
소스 파일:
mm/percpu.c ← 메인 할당 로직 (3388줄)
mm/percpu-internal.h ← chunk/block 내부 구조체 정의 (288줄)
mm/percpu-vm.c ← vmalloc 기반 chunk 백업 구현 (410줄)
include/linux/percpu.h ← 사용자 API, 상수 정의 (164줄)
# Per-CPU 영역 기본 정보 출력
cat /proc/pagetypeinfo | grep -A5 "Per-CPU"
# 현재 시스템의 Per-CPU 동적 할당 통계
cat /proc/slabinfo | grep percpu
# Per-CPU 메모리 사용량 확인 (전체)
cat /proc/meminfo | grep Percpu
# 특정 프로세스의 Per-CPU 관련 메모리 맵 확인
cat /proc/1/maps | grep -i percpu 2>/dev/null || echo "해당 없음"
# 커널 모듈의 Per-CPU 영역 확인
cat /proc/modules | awk '{print $1}' | head -5 | xargs -I{} sh -c 'echo "--- {} ---"; cat /sys/module/{}/sections/.data..percpu 2>/dev/null || echo "없음"'
# Per-CPU 할당기 내부 상태 추적 (CONFIG_PERCPU_STATS 활성화 시)
cat /sys/kernel/debug/percpu_stats 2>/dev/null || echo "디버그 정보 없음 (CONFIG_PERCPU_STATS 비활성화)"
# 커널 시작 시 Per-CPU 초기화 로그 확인
dmesg | grep -i "pcpu" | head -20
# 시스템의 총 Per-CPU 영역 크기 확인
dmesg | grep "pcpu-alloc" | head -5
# NUMA 노드별 Per-CPU 영역 분포 확인
cat /proc/buddyinfo | head -5
# 커널 설정에서 Per-CPU 관련 옵션 확인
grep -i "PERCPU\|PER_CPU" /boot/config-$(uname -r) 2>/dev/null | head -10
# Per-CPU 할당 크기별 분포 확인 (CONFIG_PERCPU_STATS 시)
cat /sys/kernel/debug/percpu_stats 2>/dev/null | grep -A20 "alloc_size" || echo "통계 정보 없음"
# 현재 Per-CPU 영역의 실제 사용률 확인
cat /proc/vmstat | grep -i "percpu\|pcpu" || echo "관련 통계 없음"
할당기의 핵심 단위입니다. 각 chunk는 하나 이상의 unit(물리 페이지 세트)을 관리하며, bitmap 기반으로 할당 상태를 추적합니다.
/* mm/percpu-internal.h:48-88 */
struct pcpu_chunk {
#ifdef CONFIG_PERCPU_STATS
int nr_alloc; /* # of allocations */
size_t max_alloc_size; /* largest allocation size */
#endif
struct list_head list; /* pcpu_chunk_lists 슬롯에 연결 */
int free_bytes; /* chunk 내 빈 바이트 수 */
struct pcpu_block_md chunk_md; /* chunk 수준 메타데이터 블록 */
unsigned long *bound_map; /* 경계 비트맵 (할당 경계 표시) */
/* base_addr: 캐시 라인 분리를 위해 별도 캐시 라인에 배치 */
void *base_addr ____cacheline_aligned_in_smp;
unsigned long *alloc_map; /* 할당 비트맵 (1=할당됨, 0=빈 영역) */
struct pcpu_block_md *md_blocks;/* 메타데이터 블록 배열 */
void *data; /* chunk 데이터 */
bool immutable; /* [de]population 금지 (첫 번째 chunk) */
bool isolated; /* 활성 슬롯에서 격리됨 */
int start_offset; /* 이전 영역과의 겹침 (페이지 정렬 보정) */
int end_offset; /* 끝 정렬을 위한 추가 영역 */
#ifdef NEED_PCPUOBJ_EXT
struct pcpuobj_ext *obj_exts; /* obj_cgroup 벡터 (memcg용) */
#endif
int nr_pages; /* 이 chunk가 담당하는 물리 페이지 수 */
int nr_populated; /* 실제 할당된 페이지 수 */
int nr_empty_pop_pages; /* 빈 페이지 수 */
unsigned long populated[]; /* 페이지별 할당 상태 비트맵 (유연 배열) */
};
bitmap 내부의 메타데이터 블록입니다. chunk의 bitmap은 PCPU_BITMAP_BLOCK_BITS(=PAGE_SIZE/4)개의 비트로 나뉘며, 각 블록마다 빈 영역 힌트를 유지합니다.
/* mm/percpu-internal.h:20-33 */
struct pcpu_block_md {
int scan_hint; /* 블록 내 스캔 힌트 (최대 빈 영역) */
int scan_hint_start; /* 스캔 힌트의 시작 위치 (블록 상대) */
int contig_hint; /* 블록 내 최대 연속 빈 영역 크기 */
int contig_hint_start; /* contig_hint 시작 위치 */
int left_free; /* 블록 왼쪽 끝에서의 빈 영역 크기 */
int right_free; /* 블록 오른쪽 끝에서의 빈 영역 크기 */
int first_free; /* 블록 내 첫 번째 빈 비트 위치 */
int nr_bits; /* 이 블록이 담당하는 총 비트 수 */
};
초기화 시 Per-CPU 영역의 전체 레이아웃을 설명합니다. 그룹, 유닛, 정적/예약/동적 영역 크기를 포함합니다.
/* include/linux/percpu.h:85-95 */
struct pcpu_alloc_info {
size_t static_size; /* 정적 percpu 변수 영역 크기 */
size_t reserved_size; /* 예약 영역 크기 (모듈용) */
size_t dyn_size; /* 동적 할당 가능 영역 크기 */
size_t unit_size; /* 단일 유닛의 총 크기 */
size_t atom_size; /* 할당 정렬 단위 (vmalloc 매핑 기준) */
size_t alloc_size; /* 총 할당 크기 (atom_size 배수) */
size_t __ai_size; /* 내부 사용 크기 */
int nr_groups; /* NUMA 그룹 수 (0이면 단일 그룹) */
struct pcpu_group_info groups[];
};
/* include/linux/percpu.h:24-37 */
#define PCPU_MIN_UNIT_SIZE PFN_ALIGN(32 << 10) /* 최소 유닛 크기: 32KB */
#define PCPU_MIN_ALLOC_SHIFT 2 /* 최소 할당 단위: 4 bytes */
#define PCPU_MIN_ALLOC_SIZE (1 << PCPU_MIN_ALLOC_SHIFT) /* = 4 bytes */
#define PCPU_BITMAP_BLOCK_SIZE PAGE_SIZE /* 메타데이터 블록 = 페이지 크기 */
#define PCPU_BITMAP_BLOCK_BITS (PCPU_BITMAP_BLOCK_SIZE >> PCPU_MIN_ALLOC_SHIFT)
/* = PAGE_SIZE/4 비트 */
/* mm/percpu.c:132-198 */
static int pcpu_unit_pages; /* 유닛당 페이지 수 */
static int pcpu_unit_size; /* 유닛 크기 (bytes) */
static int pcpu_nr_units; /* 전체 유닛 수 */
static int pcpu_atom_size; /* 할당 정렬 단위 */
int pcpu_nr_slots; /* chunk 슬롯 수 */
static int pcpu_free_slot; /* 완전히 빈 chunk 슬롯 인덱스 */
int pcpu_sidelined_slot; /* 격리된 chunk 슬롯 인덱스 */
int pcpu_to_depopulate_slot; /* depopulate 대기 chunk 슬롯 인덱스 */
void *pcpu_base_addr; /* 첫 번째 chunk의 기본 주소 */
struct pcpu_chunk *pcpu_first_chunk; /* 첫 번째 chunk (정적 영역 포함) */
struct pcpu_chunk *pcpu_reserved_chunk; /* 예약 영역 chunk */
DEFINE_SPINLOCK(pcpu_lock); /* 내부 데이터 구조 보호 */
static DEFINE_MUTEX(pcpu_alloc_mutex); /* chunk 생성/파괴, [de]pop 보호 */
static int pcpu_nr_empty_pop_pages; /* 빈 페이지 수 (pcpu_lock 보호) */
static unsigned long pcpu_nr_populated; /* 할당된 페이지 수 */
Per-CPU 영역의 메인 할당 함수입니다. slab 초기화 이전에도 동작하며, memcg 인식 할당을 지원합니다.
/* mm/percpu.c:1736-1938 */
void __percpu *pcpu_alloc_noprof(size_t size, size_t align, bool reserved,
gfp_t gfp)
/* mm/percpu.c:1615-1633 */
#ifdef CONFIG_MEMCG
static bool pcpu_memcg_pre_alloc_hook(size_t size, gfp_t gfp,
struct obj_cgroup **objcgp)
{
struct obj_cgroup *objcg;
if (!memcg_kmem_online() || !(gfp & __GFP_ACCOUNT))
return true;
objcg = current_obj_cgroup();
if (!objcg)
return true;
if (obj_cgroup_charge(objcg, gfp, pcpu_obj_full_size(size)))
return false;
*objcgp = objcg;
return true;
}
#endif
흐름:
1. GFP 플래그에서 허용된 플래그만 추출 (pcpu_gfp)
2. 크기를 PCPU_MIN_ALLOC_SIZE(4B) 단위로 정렬 → 비트 수로 변환
3. memcg 사전 할당 검사 (pcpu_memcg_pre_alloc_hook)
4. 비원자적 할당 시 pcpu_alloc_mutex 획득 (데드락 방지 위해 killable 사용)
5. pcpu_lock 획득 후:
- 예약 chunk가 있으면 먼저 시도
- pcpu_chunk_lists[slot] 순회하며 pcpu_find_block_fit + pcpu_alloc_area 호출
- 공간 없으면 새 chunk 생성 후 재시도
6. 페이지 미할당 영역은 pcpu_populate_chunk로 물리 메모리 연결
7. 모든 유닛에 대해 memset으로 영역 제로 초기화
8. __addr_to_pcpu_ptr로 Per-CPU 포인터 변환 후 반환
분기 로직:
reserved == true → pcpu_reserved_chunk에서만 할당is_atomic == true → 페이지 pop 없이 bitmap만 사용, 실패 시 즉시 반환pcpu_nr_empty_pop_pages < PCPU_EMPTY_POP_PAGES_LOW → 밸런스 워크 스케줄할당된 Per-CPU 영역을 해제합니다. chunk 상태에 따라 밸런스 워크를 트리거할 수 있습니다.
/* mm/percpu.c:2232-2288 */
void free_percpu(void __percpu *ptr)
흐름:
1. __pcpu_ptr_to_addr로 일반 주소로 변환
2. pcpu_chunk_addr_search로 해당 chunk 탐색
3. pcpu_free_area로 bitmap 해제 → freed 바이트 수 반환
4. pcpu_memcg_free_hook / pcpu_alloc_tag_free_hook 호출
5. 완전히 빈 chunk가 2개 이상이면 밸런스 워크 스케줄
6. pcpu_should_reclaim_chunk 조건 충족 시 chunk 격리 후 밸런스 워크
분기 로직:
chunk->free_bytes == pcpu_unit_size → 완전히 빈 chunk → 추가 빈 chunk 존재 시 밸런스pcpu_should_reclaim_chunk(chunk) → 격리된 chunk → 밸런스 워크 트리거pcpu_nr_empty_pop_pages < PCPU_EMPTY_POP_PAGES_LOW → 빈 페이지 부족 → 밸런스 워크빈 chunk 정리, 페이지 repopulate, depopulate를 수행하는 비동기 워크입니다.
/* mm/percpu.c:2196-2221 */
static void pcpu_balance_workfn(struct work_struct *work)
흐름:
1. memalloc_noio_save()로 GFP_NOIO 컨텍스트 설정
2. pcpu_alloc_mutex + pcpu_lock 획득
3. pcpu_balance_free(false) → 완전히 빈 chunk 모두 정리 (하나 제외)
4. pcpu_reclaim_populated() → depopulate 대기 chunk에서 빈 페이지 해제
5. pcpu_balance_populated() → atomic 할당을 위해 미리 populating
6. pcpu_balance_free(true) → populating 없는 빈 chunk만 추가 정리
실제 bitmap에서 할당 가능한 영역을 찾아 할당합니다.
/* mm/percpu.c:1216-1263 */
static int pcpu_alloc_area(struct pcpu_chunk *chunk, int alloc_bits,
size_t align, int start)
흐름:
1. pcpu_find_zero_area로 정렬된 빈 영역 탐색
2. alloc_map에 비트 설정 (bitmap_set)
3. bound_map에 경계 비트 설정 (할당 시작/끝 표시)
4. chunk_md->first_free 갱신
5. pcpu_block_update_hint_alloc로 메타데이터 힌트 갱신
6. pcpu_chunk_relocate로 적절한 슬롯으로 이동
메타데이터 블록의 힌트를 활용하여 할당 가능한 시작 위치를 찾습니다.
/* mm/percpu.c:1110-1139 */
static int pcpu_find_block_fit(struct pcpu_chunk *chunk, int alloc_bits,
size_t align, bool pop_only)
흐름:
1. chunk_md->contig_hint로 전체 chunk 수준 검사 — 할당 불가능하면 즉시 실패
2. pcpu_next_hint로 스캔 시작 위치 결정
3. pcpu_for_each_fit_region 매크로로 fit 영역 순회
4. pop_only 시 pcpu_is_populated로 페이지 존재 확인
5. 유효한 offset 반환 또는 -1
alloc_percpu(type)
└─ pcpu_alloc_noprof(size, align, false, GFP_KERNEL)
├─ [메모리 크기 검증]
├─ pcpu_memcg_pre_alloc_hook()
├─ mutex_lock(&pcpu_alloc_mutex) ← 비원자적 할당만
├─ spin_lock_irqsave(&pcpu_lock)
│ ├─ [예약 chunk 시도] pcpu_find_block_fit → pcpu_alloc_area
│ ├─ [일반 chunk 순회] for each slot → pcpu_find_block_fit → pcpu_alloc_area
│ └─ [공간 없음] pcpu_create_chunk → 재시도
├─ pcpu_populate_chunk() ← 미할당 페이지 연결
├─ memset() (모든 CPU)
├─ __addr_to_pcpu_ptr() ← Per-CPU 포인터 변환
└─ pcpu_memcg_post_alloc_hook()
free_percpu(ptr)
├─ __pcpu_ptr_to_addr()
├─ pcpu_chunk_addr_search()
├─ spin_lock_irqsave(&pcpu_lock)
│ └─ pcpu_free_area()
│ └─ bitmap_clear + pcpu_block_update_hint_free
├─ pcpu_memcg_free_hook()
└─ pcpu_schedule_balance_work() ← 필요 시
pcpu_balance_workfn() ← 비동기 워크 큐
├─ pcpu_balance_free(false) ← 빈 chunk 정리
├─ pcpu_reclaim_populated() ← depopulate 대기 해제
├─ pcpu_balance_populated() ← atomic용 사전 populating
└─ pcpu_balance_free(true) ← 추가 정리
pcpu_create_chunk(gfp) ← 새 chunk 생성
├─ pcpu_mem_zalloc() ← chunk 구조체 할당
├─ pcpu_mem_zalloc() ← alloc_map 할당
├─ pcpu_mem_zalloc() ← bound_map 할당
├─ pcpu_mem_zalloc() ← md_blocks 할당
└─ pcpu_init_md_blocks() ← 메타데이터 블록 초기화
pcpu_populate_chunk(chunk, page_start, page_end, gfp)
└─ pcpu-vm.c/pcpu-km.c 구현 ← 물리 페이지 연결
| 조건 | 동작 | 설명 |
|---|---|---|
| `reserved == true` | `pcpu_reserved_chunk`에서만 할당 | 모듈용 정적 percpu 영역 |
| `reserved == false` | 일반 chunk 슬롯 순회 | 동적 percpu 할당 |
| `is_atomic == true` | populating 없이 bitmap만 사용 | IRQ 컨텍스트 등 블로킹 불가 |
| `is_atomic == false` | `pcpu_populate_chunk` 호출 | 물리 페이지 실제 연결 |
| `GFP_NOFS/NOIO` | `memalloc_noio_save()` 적용 | 파일 시스템 데드락 방지 |
| `__GFP_ACCOUNT` | memcg-aware chunk 세트 사용 | `obj_cgroup` 충전, root cgroup/비계정과 분리 |
| 슬롯 | 변수 | 용도 |
|---|---|---|
| 0 ~ N-1 | `pcpu_chunk_lists[0..N-1]` | 크기별 할당 가능 chunk 정렬 |
| `pcpu_sidelined_slot` | `pcpu_sidelined_slot` | 격리된 chunk (depopulate 불가) |
| `pcpu_free_slot` | `pcpu_free_slot` | 완전히 빈 chunk |
| `pcpu_to_depopulate_slot` | `pcpu_to_depopulate_slot` | depopulate 대기 chunk |
| 힌트 | 필드 | 역할 |
|---|---|---|
| `contig_hint` | 블록/ chunk 수준 | 최대 연속 빈 영역 크기 |
| `scan_hint` | 블록 수준 | contig_hint 이전의 두 번째로 큰 빈 영역 |
| `first_free` | 블록 수준 | 첫 번째 빈 비트 위치 |
| `left_free` / `right_free` | 블록 수준 | 블록 양 끝의 빈 영역 크기 |
/* 일반 주소 → Per-CPU 포인터 */
addr - pcpu_base_addr + __per_cpu_start
/* Per-CPU 포인터 → 일반 주소 */
ptr + pcpu_base_addr - __per_cpu_start
첫 번째 chunk의 구조:
┌─────────────────────────────────────────────────────────────┐
│ 첫 번째 chunk (pcpu_first_chunk) │
├──────────────┬──────────────────┬───────────────────────────┤
│ Static 영역 │ Reserved 영역 │ Dynamic 영역 │
│ (__per_cpu_ │ (모듈 percpu) │ (동적 할당 가능) │
│ start) │ │ │
├──────────────┴──────────────────┴───────────────────────────┤
│ 각 unit은 CPU에 1:1 대응, NUMA 그룹으로 분리 │
│ │
│ c0: [u0][u1][u2][u3] c1: [u0][u1][u2][u3] ... │
└─────────────────────────────────────────────────────────────┘