Ryotta's Linux 7.0 MM

메모리 관리 서브시스템 완전 분석

# Per-CPU 메모리

관련 소스: mm/percpu.c, mm/percpu-internal.h, mm/percpu-vm.c, include/linux/percpu.h

개요 (Overview)

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 "관련 통계 없음"

핵심 자료구조

struct pcpu_chunk

할당기의 핵심 단위입니다. 각 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[];    /* 페이지별 할당 상태 비트맵 (유연 배열) */
};

struct pcpu_block_md

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;            /* 이 블록이 담당하는 총 비트 수 */
};

struct pcpu_alloc_info

초기화 시 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; /* 할당된 페이지 수 */

핵심 함수

pcpu_alloc_noprof (동적 Per-CPU 할당)

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 == truepcpu_reserved_chunk에서만 할당
  • is_atomic == true → 페이지 pop 없이 bitmap만 사용, 실패 시 즉시 반환
  • pcpu_nr_empty_pop_pages < PCPU_EMPTY_POP_PAGES_LOW → 밸런스 워크 스케줄
  • free_percpu (Per-CPU 해제)

    할당된 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 → 빈 페이지 부족 → 밸런스 워크
  • pcpu_balance_workfn (비동기 밸런스)

    빈 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만 추가 정리

    pcpu_alloc_area (bitmap 기반 영역 할당)

    실제 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로 적절한 슬롯으로 이동

    pcpu_find_block_fit (블록 수준 할당 가능 위치 탐색)

    메타데이터 블록의 힌트를 활용하여 할당 가능한 시작 위치를 찾습니다.

    /* 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_onlypcpu_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/비계정과 분리

    chunk 슬롯 유형

    슬롯변수용도
    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 포인터 변환

    /* 일반 주소 → Per-CPU 포인터 */
    addr - pcpu_base_addr + __per_cpu_start
    
    /* Per-CPU 포인터 → 일반 주소 */
    ptr + pcpu_base_addr - __per_cpu_start
  • SMP: 위 변환 사용
  • UP: 항등 매핑 (변환 불필요)

  • 메모리 레이아웃

    첫 번째 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]  ...          │
    └─────────────────────────────────────────────────────────────┘

    다이어그램

    Per-CPU 메모리 구조도
    Per-CPU 할당기 호출 흐름

    관련 문서

  • 메모리 관리 개요
  • Buddy Allocator
  • SLUB 할당자
  • vmalloc
  • Memblock 할당자