메모리 관리 (심화)
CMA, HugeTLB, tmpfs/ramfs/shmem, memfd, devtmpfs, fallocate, tmpfs quota, memory cgroup, DAMON, KSM, zswap, page migration, compaction 등 Linux 커널 고급 메모리 관리 기법을 다룹니다.
관련 페이지: 이 문서는 메모리 관리 심화 주제를 다룹니다. 아래 독립 페이지에서 특정 주제의 상세 내용을 확인할 수 있습니다.
- Swapping 서브시스템 — swap 공간 설정, swap cache, swap out/in 경로, swappiness 튜닝, zswap/zram
- VMA / mmap 심화 — mmap 시스템 콜, VMA 구조, 페이지 폴트, MAP_SHARED/PRIVATE, mremap/mprotect/madvise, userfaultfd
핵심 요약
- mmap — 파일이나 디바이스를 프로세스의 가상 주소 공간에 직접 매핑하는 시스템 콜입니다.
- CMA — DMA 디바이스를 위해 대용량 연속 물리 메모리를 예약·할당하는 메커니즘입니다.
- HugeTLB — 2MB/1GB 대형 페이지로 TLB 미스를 줄여 성능을 개선합니다.
- KSM / zswap — 동일 내용의 페이지를 병합(KSM)하거나 압축(zswap)하여 메모리를 절약합니다.
- tmpfs / shmem — RAM 기반 파일시스템으로, 공유 메모리와 임시 파일에 사용됩니다.
단계별 이해
- mmap 이해 —
mmap()은 가상 주소만 확보하고, 실제 물리 페이지는 접근 시(page fault) 할당됩니다.이것이 "Demand Paging"이며, 메모리를 효율적으로 사용하는 핵심 원리입니다.
- 대형 페이지 활용 — 4KB 페이지 수천 개 대신 2MB HugePage 하나를 사용하면 TLB 엔트리를 절약할 수 있습니다.
데이터베이스, JVM 등 대용량 메모리 애플리케이션에서 큰 성능 향상을 줍니다.
- 메모리 절약 기법 — KSM은 동일 페이지를 탐지·병합하고, zswap은 swap-out 전에 페이지를 압축합니다.
가상 머신 환경에서 KSM은 게스트 간 동일 페이지를 공유하여 메모리를 크게 절약합니다.
- Memory cgroup과 DAMON — cgroup으로 프로세스 그룹의 메모리 사용량을 제한하고, DAMON으로 접근 패턴을 모니터링합니다.
컨테이너 환경에서 메모리 격리와 최적화의 핵심 도구입니다.
CMA (Contiguous Memory Allocator)
CMA는 DMA 디바이스를 위한 대용량 연속 메모리 할당을 지원합니다. 일반적인 메모리 단편화 상황에서도 연속된 물리 페이지를 확보할 수 있습니다.
/* CMA 영역에서 연속 페이지 할당 */
struct page *pages = cma_alloc(cma, count, align, gfp_mask);
/* CMA 메모리 해제 */
bool ok = cma_release(cma, pages, count);
/* DMA API를 통한 사용 (일반적인 방법) */
void *vaddr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
dma_free_coherent(dev, size, vaddr, dma_handle);
# 커널 부트 파라미터로 CMA 영역 설정
cma=256M # 기본 CMA 영역 256MB
cma=256M@0-4G # 0~4GB 범위에 256MB CMA
# 디바이스 트리에서 CMA 설정
reserved-memory {
cma_region: linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0x10000000>; /* 256MB */
};
};
# CMA 상태 확인
cat /proc/meminfo | grep Cma
# CmaTotal: 262144 kB
# CmaFree: 245760 kB
HugeTLB Pages
Huge Pages는 기본 4KB 대신 2MB(x86) 또는 1GB 크기의 페이지를 사용하여 TLB 미스를 획기적으로 줄입니다. 일반적인 x86_64 CPU의 TLB는 4KB 엔트리를 수백~수천 개만 캐시할 수 있어, 대용량 메모리를 접근하면 빈번한 TLB 미스가 발생합니다. 2MB 페이지를 사용하면 동일한 TLB 엔트리 수로 512배 더 넓은 주소 공간을 커버할 수 있습니다.
페이지 테이블과 Huge Page 매핑
x86_64의 4단계 페이지 테이블에서, Huge Page는 중간 레벨에서 직접 물리 프레임을 가리키는 방식으로 동작합니다:
- PTE (Page Table Entry) — 4KB 일반 페이지. PGD→P4D→PUD→PMD→PTE→물리 페이지
- PMD (Page Middle Directory) — 2MB Huge Page. PMD 엔트리가
_PAGE_PSE(Page Size Extension) 비트를 설정하여 직접 2MB 물리 프레임 지시. PTE 단계 생략 - PUD (Page Upper Directory) — 1GB Huge Page. PUD 엔트리가 직접 1GB 물리 프레임 지시. PMD, PTE 단계 모두 생략
/* 4KB 일반 페이지: 4단계 페이지 워크 */
PGD → P4D → PUD → PMD → PTE → 4KB 물리 프레임
↑ 12비트 오프셋
/* 2MB Huge Page: PMD에서 직접 매핑 (PSE 비트) */
PGD → P4D → PUD → PMD ──────→ 2MB 물리 프레임
↑ PSE=1 ↑ 21비트 오프셋
/* 1GB Huge Page: PUD에서 직접 매핑 */
PGD → P4D → PUD ─────────────→ 1GB 물리 프레임
↑ PSE=1 ↑ 30비트 오프셋
Compound Page 내부 구조
커널 내부에서 Huge Page는 compound page로 관리됩니다. 연속된 물리 페이지들을 하나의 논리 단위로 묶어, 첫 번째 페이지(head page)가 전체를 대표합니다:
/* 커널 6.x: folio 기반 huge page 관리 (include/linux/mm_types.h) */
struct folio {
struct page page; /* head page — 참조 카운트, 매핑 정보 */
unsigned long _flags_1;
unsigned long _head_1;
unsigned char _folio_order; /* 2MB = order-9 (512 pages), 1GB = order-18 */
atomic_t _total_mapcount;
atomic_t _nr_pages_mapped;
/* ... */
};
/* compound page 구조:
* page[0] = head page (PG_head 플래그 설정)
* page[1] = first tail — compound_order, compound_dtor 저장
* page[2..N] = tail pages — compound_head 포인터로 head 참조
*
* 2MB huge page = 512개 연속 struct page (order-9)
* head page의 compound_order = 9
* 모든 tail page의 compound_head = head page 주소 | 1
*/
/* folio API로 huge page 조작 (mm/hugetlb.c) */
static struct folio *alloc_hugetlb_folio(
struct vm_area_struct *vma,
unsigned long addr, int avoid_reserve)
{
struct hugepage_subpool *spool = subpool_vma(vma);
struct hstate *h = hstate_vma(vma);
struct folio *folio;
/* hstate: hugepage 크기별(2MB, 1GB) 관리 구조체
* 각 hstate가 자체 free_hugepages 리스트를 관리 */
folio = dequeue_hugetlb_folio_vma(h, vma, addr, avoid_reserve);
/* ... */
}
HugeTLB Pages 설정
# Huge Pages 설정
echo 512 > /proc/sys/vm/nr_hugepages # 2MB 페이지 512개 (1GB)
echo 4 > /sys/kernel/mm/hugepages/hugepages-1048576kB/nr_hugepages # 1GB 페이지 4개
# 확인
cat /proc/meminfo | grep -i huge
# HugePages_Total: 512
# HugePages_Free: 512
# Hugepagesize: 2048 kB
# 마운트하여 사용
mount -t hugetlbfs nodev /mnt/hugepages
# 커널 부팅 파라미터 (GRUB에 추가 — 부팅 시 즉시 예약)
# hugepagesz=2M hugepages=512 ← 2MB 512개
# hugepagesz=1G hugepages=4 ← 1GB 4개 (부팅 시만 가능)
# default_hugepagesz=2M ← 기본 hugepage 크기
# Surplus hugepage (overcommit) — 긴급 시 풀 외 할당
cat /proc/sys/vm/nr_overcommit_hugepages # 기본값 0
echo 64 > /proc/sys/vm/nr_overcommit_hugepages # 풀 소진 시 최대 64개 추가 할당
THP (Transparent Huge Pages)
THP는 애플리케이션 수정 없이 자동으로 Huge Page를 사용하게 합니다. Static HugeTLB와 달리 사전 예약이 불필요하며, 커널이 페이지 폴트 시점이나 백그라운드에서 자동으로 2MB 페이지를 할당합니다:
# THP 모드 설정
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
# madvise 모드: MADV_HUGEPAGE를 호출한 영역만 THP 사용
# khugepaged 데몬: 4KB 페이지를 백그라운드에서 2MB로 합침
cat /sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan
# 4096 (기본값: 스캔당 4096 페이지 검사)
/* 애플리케이션에서 THP 요청 */
madvise(addr, length, MADV_HUGEPAGE); /* THP 사용 요청 */
madvise(addr, length, MADV_NOHUGEPAGE); /* THP 비사용 요청 */
THP 생명주기 — 할당, 승격, 분할
THP는 3가지 경로로 할당되고, 필요 시 다시 분할(split)됩니다:
/* THP 할당 경로 (mm/huge_memory.c) */
/* 경로 1: 페이지 폴트 시 즉시 할당 (synchronous) */
/*
* do_anonymous_page() 또는 do_huge_pmd_anonymous_page()
* → 2MB 연속 물리 메모리 할당 시도
* → 성공: PMD 엔트리에 직접 매핑 (PTE 단계 생략)
* → 실패: defrag 정책에 따라 compaction 또는 4KB fallback
*/
vm_fault_t do_huge_pmd_anonymous_page(struct vm_fault *vmf)
{
struct folio *folio;
gfp_t gfp = vma_thp_gfp_mask(vma);
folio = vma_alloc_folio(gfp, HPAGE_PMD_ORDER, vma, haddr);
if (!folio) {
/* 2MB 할당 실패 → 일반 4KB 페이지로 fallback */
count_vm_event(THP_FAULT_FALLBACK);
return VM_FAULT_FALLBACK;
}
count_vm_event(THP_FAULT_ALLOC);
/* PMD에 2MB 매핑 설치 */
set_pmd_at(mm, haddr, vmf->pmd, pmd_mkhuge(entry));
/* ... */
}
/* 경로 2: khugepaged 백그라운드 승격 (collapse) */
/*
* khugepaged 커널 스레드가 주기적으로 VMA를 스캔
* → 연속된 512개 4KB 페이지를 발견하면 2MB로 합체(collapse)
* → 새 compound page 할당 → 기존 데이터 복사 → PTE를 PMD로 교체
*
* scan_sleep_millisecs: 스캔 주기 (기본 10000ms)
* pages_to_scan: 1회 스캔할 페이지 수 (기본 4096)
* max_ptes_none: 빈 PTE 허용 수 (기본 511 — 512개 중 1개만 있어도 합체 시도)
*/
/* 경로 3: THP 분할 (split) */
/*
* 2MB THP를 다시 512개 4KB 페이지로 분할하는 경우:
* - 부분적 munmap() — 2MB 영역의 일부만 해제
* - 부분적 mprotect() — 영역 내 권한 변경
* - 메모리 회수(reclaim) — 2MB 전체를 회수할 수 없을 때
* - swap out — 2MB 단위 swap이 비효율적일 때
* - MADV_DONTNEED — 부분 영역 무효화
*/
int split_huge_page_to_list(struct page *page, struct list_head *list)
{
/* compound page 해제 → 개별 struct page로 전환
* head page에서 PG_head 플래그 제거
* 각 tail page를 독립 페이지로 초기화
* PMD 엔트리를 512개 PTE로 교체 */
}
THP defrag 모드 상세
THP 할당 실패 시 커널의 동작을 제어하는 defrag 파라미터입니다. 메모리 compaction(단편화 해소)의 동기/비동기 여부를 결정합니다:
| defrag 모드 | 페이지 폴트 시 동작 | khugepaged | 적합한 환경 |
|---|---|---|---|
always |
동기 compaction 실행 (프로세스 블록됨) | 활성 | HPC, 과학 계산 — THP 할당을 최대화 |
defer |
비동기 compaction 요청 후 즉시 fallback | 활성 | 일반 서버 — 지연 스파이크 방지 |
defer+madvise |
일반: defer 동작 / MADV_HUGEPAGE 영역: 동기 compaction | 활성 | 혼합 워크로드 — 명시 요청만 동기 대기 (권장) |
madvise |
MADV_HUGEPAGE 영역만 동기 compaction | 활성 | 지연 민감 서비스 + 선택적 THP |
never |
compaction 없음. 즉시 사용 가능한 2MB가 없으면 fallback | 활성 | 지연 최소화 — 이미 가용한 경우만 THP 사용 |
# defrag 현재 설정 확인
cat /sys/kernel/mm/transparent_hugepage/defrag
# always defer defer+madvise [madvise] never
# 프로덕션 권장: defer+madvise
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag
# → 일반 페이지 폴트에서는 동기 compaction 없음 (지연 스파이크 방지)
# → MADV_HUGEPAGE 요청 영역에서만 동기 compaction (확실한 THP 할당)
Multi-size THP (mTHP) — 커널 6.8+
전통적인 THP는 2MB(PMD 크기) 단일 옵션만 제공했습니다. 커널 6.8부터 도입된 Multi-size THP(mTHP)는 2MB뿐 아니라 16KB, 32KB, 64KB, 128KB, 256KB, 512KB, 1MB 등 다양한 크기의 "large folio"를 지원합니다. PTE 레벨에서 연속된 엔트리를 사용하므로 PMD 매핑이 아닌 PTE-mapped large folio입니다:
CONT_PTE(64KB 연속 PTE)와도 자연스럽게 결합되어 모바일/임베디드에서 특히 효과적입니다.
# mTHP 지원 크기 확인 및 설정 (커널 6.8+)
ls /sys/kernel/mm/transparent_hugepage/hugepages-*
# hugepages-16kB/ hugepages-32kB/ hugepages-64kB/ hugepages-128kB/
# hugepages-256kB/ hugepages-512kB/ hugepages-1024kB/ hugepages-2048kB/
# 크기별 활성화 제어
cat /sys/kernel/mm/transparent_hugepage/hugepages-64kB/enabled
# [always] inherit madvise never
# 특정 크기만 활성화 (예: 64KB만)
echo always > /sys/kernel/mm/transparent_hugepage/hugepages-64kB/enabled
echo never > /sys/kernel/mm/transparent_hugepage/hugepages-2048kB/enabled
# inherit: 시스템 전역 THP 설정을 따름
# 각 크기별로 독립적으로 always/madvise/never 설정 가능
/* mTHP 할당 흐름 (mm/memory.c, 커널 6.8+) */
/*
* 페이지 폴트 발생 시:
* 1. PMD 크기(2MB) THP 할당 시도
* 2. 실패하면 점차 작은 크기로 fallback:
* 1MB → 512KB → 256KB → 128KB → 64KB → 32KB → 16KB → 4KB
* 3. 각 크기마다 해당 order의 compound page(folio) 할당 시도
* 4. PTE 연속 엔트리로 매핑 (contpte on ARM64)
*
* /proc/vmstat 카운터:
* thp_fault_alloc — PMD(2MB) 할당 성공
* thp_fault_fallback — PMD 할당 실패 → 더 작은 크기 시도
* thp_fault_fallback_charge — memcg 충전 실패로 fallback
*/
Hugepage 심화 — 주의사항과 고려사항
Hugepage 유형 비교
| 유형 | 페이지 크기 | 할당 방식 | 설정 방법 | 적합한 워크로드 |
|---|---|---|---|---|
| Static HugeTLB (2MB) | 2MB (x86 PMD) | 부팅 시 또는 런타임 예약 | nr_hugepages, hugetlbfs |
DB, DPDK, KVM 게스트 메모리 |
| Static HugeTLB (1GB) | 1GB (x86 PUD) | 부팅 시 커널 파라미터로만 예약 | hugepagesz=1G hugepages=4 |
대규모 인메모리 DB, ML/HPC |
| THP (Transparent) | 2MB (자동) | 페이지 폴트 시 자동 승격 | transparent_hugepage=always|madvise |
일반 애플리케이션 (자동 최적화) |
| HugeTLB + NUMA | 2MB/1GB | 노드별 예약 | /sys/devices/system/node/nodeN/hugepages/ |
NUMA 인지 애플리케이션 |
| mTHP (Multi-size THP) | 16KB~1MB (가변) | 페이지 폴트 시 자동 (PTE 레벨) | hugepages-NkB/enabled (커널 6.8+) |
내부 단편화 최소화, ARM64 CONT_PTE |
hugetlbfs 프로그래밍
HugeTLB 예약(Reservation) 메커니즘
HugeTLB는 mmap() 시점에 huge page를 예약(reserve)하고, 실제 접근 시 할당합니다. 이는 OOM 방지를 위한 핵심 메커니즘입니다:
/* HugeTLB 예약 흐름 (mm/hugetlb.c) */
/* 1. mmap() 호출 시: 예약 확보 (아직 물리 페이지 할당 아님) */
/*
* hugetlb_reserve_pages()
* → 풀(hstate->free_hugepages)에서 요청 크기만큼 예약
* → HugePages_Rsvd 카운터 증가
* → 풀이 부족하면 ENOMEM 반환 (mmap 실패)
*
* 이 단계에서 물리 페이지를 할당하지 않으므로:
* HugePages_Free는 변하지 않음
* HugePages_Rsvd만 증가
*/
/* 2. 첫 접근(페이지 폴트) 시: 예약된 페이지를 실제 할당 */
/*
* hugetlb_no_page() → alloc_hugetlb_folio()
* → 예약된 페이지에서 하나를 꺼내 물리 매핑
* → HugePages_Free 감소, HugePages_Rsvd 감소
*
* 예약이 있으므로 페이지 폴트 시 OOM이 발생하지 않음 보장
*/
/* /proc/meminfo 예시:
* HugePages_Total: 512 ← 전체 풀 크기
* HugePages_Free: 480 ← 미할당 (예약 포함)
* HugePages_Rsvd: 32 ← 예약됨 (mmap 했지만 미접근)
* HugePages_Surp: 0 ← overcommit으로 추가 할당된 수
*
* 실제 가용 = Free - Rsvd = 480 - 32 = 448
* 이미 매핑됨 = Total - Free = 512 - 480 = 32
*/
size= 또는 nr_inodes= 옵션으로 서브풀을 생성할 수 있습니다.
서브풀은 전체 hugepage 풀에서 일정량을 격리하여 특정 마운트포인트의 사용량을 제한합니다.
예: mount -t hugetlbfs -o size=1G nodev /mnt/app_hugepages — 최대 1GB(512개 2MB 페이지)로 제한.
HugeTLB Cgroup 제어
cgroup v2의 hugetlb 컨트롤러를 통해 프로세스 그룹별로 hugepage 사용량을 제한할 수 있습니다:
# cgroup v2에서 hugetlb 컨트롤러 활성화
echo "+hugetlb" > /sys/fs/cgroup/cgroup.subtree_control
# 프로세스 그룹별 hugepage 제한
mkdir /sys/fs/cgroup/myapp
echo "2048M" > /sys/fs/cgroup/myapp/hugetlb.2MB.max # 2MB hugepage 최대 2GB
echo "4G" > /sys/fs/cgroup/myapp/hugetlb.1GB.max # 1GB hugepage 최대 4GB
# 현재 사용량 확인
cat /sys/fs/cgroup/myapp/hugetlb.2MB.current # 현재 사용 중인 2MB hugepage 바이트
# 제한 초과 이벤트 카운터
cat /sys/fs/cgroup/myapp/hugetlb.2MB.events
# max 0 ← 제한에 도달하여 할당 실패한 횟수
# 예약(reservation) 제한도 별도로 설정 가능
echo "1024M" > /sys/fs/cgroup/myapp/hugetlb.2MB.rsvd.max # 예약 포함 최대 1GB
cat /sys/fs/cgroup/myapp/hugetlb.2MB.rsvd.current
THP (Transparent Huge Pages) 주의사항
- 메모리 팽창 — 4KB만 필요한 곳에 2MB 할당 시 내부 단편화로 메모리 낭비. 특히 희소(sparse) 접근 패턴에서 심각
- 페이지 폴트 지연 스파이크 — THP 할당 실패 시 compaction이 동기적으로 실행되어 수십~수백 ms 지연 발생 가능
- khugepaged CPU 오버헤드 — 백그라운드 THP 합체 데몬이 지속적으로 CPU 소비. 지연 민감 워크로드에서 jitter 유발
- CoW(Copy-on-Write) 비용 — fork() 후 2MB 전체를 복사해야 함 (4KB 대비 512배). Redis/PostgreSQL의 fork 기반 스냅샷에서 심각한 지연
- 메모리 회수 지연 — 2MB 페이지를 회수하려면 내부의 모든 4KB 서브페이지가 비어야 함. 메모리 압력 상황에서 OOM 더 빨리 도달
- NUMA 불균형 — THP 합체가 특정 NUMA 노드에서만 성공하면 메모리 편향 발생
THP 워크로드별 권장 설정
| 워크로드 | THP 설정 | 이유 |
|---|---|---|
| 일반 서버 | madvise |
애플리케이션이 명시적으로 요청한 영역만 THP 사용 |
| Redis, MongoDB | madvise 또는 never |
fork 기반 스냅샷의 CoW 오버헤드 방지 |
| Oracle, SAP HANA | Static HugeTLB | 예측 가능한 메모리 할당, THP 오버헤드 완전 제거 |
| DPDK | Static HugeTLB (1GB) | 대용량 연속 메모리 필요, DMA 매핑 효율 |
| KVM 게스트 | Static HugeTLB 또는 THP | EPT/NPT 페이지 테이블 크기 감소, TLB 미스 최소화 |
| 지연 민감 (HFT) | Static HugeTLB + mlock |
페이지 폴트 완전 제거, 결정적 지연 보장 |
Hugepage와 NUMA
# NUMA 노드별 hugepage 예약
echo 256 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 256 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
# 현재 NUMA 노드별 hugepage 상태
cat /sys/devices/system/node/node*/hugepages/hugepages-2048kB/free_hugepages
# numactl로 특정 NUMA 노드에 hugepage 할당
numactl --membind=0 ./my_app # node0의 hugepage만 사용
# hugepage 단편화 문제:
# 부팅 후 시간이 지나면 2MB 연속 블록 확보가 어려워짐
# 해결: 부팅 시 예약, 또는 CMA 영역 활용
# 커널 파라미터: hugepages=512 (부팅 시 즉시 예약 — 단편화 없음)
Hugepage 모니터링
# /proc/meminfo에서 hugepage 통계
grep -i huge /proc/meminfo
# AnonHugePages: 524288 kB ← THP 사용량
# ShmemHugePages: 0 kB ← tmpfs THP
# HugePages_Total: 512 ← 예약된 static hugepage
# HugePages_Free: 480 ← 미사용
# HugePages_Rsvd: 32 ← 예약 (mmap 했지만 아직 접근 안 함)
# HugePages_Surp: 0 ← surplus (overcommit)
# THP 이벤트 통계
grep thp /proc/vmstat
# thp_fault_alloc 12345 ← 페이지 폴트 시 THP 할당 성공
# thp_fault_fallback 6789 ← THP 할당 실패 → 4KB fallback
# thp_collapse_alloc 1234 ← khugepaged 합체 성공
# thp_split_page 567 ← THP → 4KB 분할 발생
# ↑ fallback/split이 높으면 단편화 문제 또는 THP가 부적합한 워크로드
# 프로세스별 hugepage 사용량
grep -i huge /proc/<pid>/smaps_rollup
# 프로세스의 smaps에서 THP 매핑 상세
grep -E "AnonHugePages|ShmemPmdMapped|FilePmdMapped" /proc/<pid>/smaps_rollup
# AnonHugePages: 524288 kB ← 익명 THP (heap, stack)
# ShmemPmdMapped: 0 kB ← PMD 매핑된 공유 메모리 THP
# FilePmdMapped: 0 kB ← PMD 매핑된 파일 backed THP
perf를 이용한 TLB 미스 분석
Huge Page 도입 전후의 TLB 성능 차이를 perf로 정량적으로 측정할 수 있습니다:
# 1. TLB 미스 카운터 측정 (PMU hardware event)
perf stat -e dTLB-loads,dTLB-load-misses,dTLB-stores,dTLB-store-misses \
-e iTLB-loads,iTLB-load-misses \
./my_application
# Performance counter stats:
# 1,234,567,890 dTLB-loads
# 456,789 dTLB-load-misses # 0.04% of dTLB-loads ← 낮을수록 좋음
# 345,678,901 dTLB-stores
# 12,345 dTLB-store-misses
# 234,567,890 iTLB-loads
# 1,234 iTLB-load-misses # 0.00%
# 2. 페이지 워크 사이클 측정 (TLB 미스의 실제 비용)
perf stat -e dTLB-load-misses,dtlb_load_misses.walk_completed,dtlb_load_misses.walk_active \
./my_application
# walk_completed: 페이지 테이블 워크 완료 횟수
# walk_active: 페이지 테이블 워크에 소비된 사이클
# 3. Huge Page 전후 비교 스크립트
# (a) 일반 4KB 페이지로 실행
echo never > /sys/kernel/mm/transparent_hugepage/enabled
perf stat -e dTLB-load-misses -- ./benchmark 2>> result_4kb.txt
# (b) THP 활성화 후 실행
echo always > /sys/kernel/mm/transparent_hugepage/enabled
perf stat -e dTLB-load-misses -- ./benchmark 2>> result_thp.txt
# (c) Static HugeTLB로 실행
echo 512 > /proc/sys/vm/nr_hugepages
LD_PRELOAD=libhugetlbfs.so HUGETLB_MORECORE=yes ./benchmark
perf stat -e dTLB-load-misses -- env LD_PRELOAD=libhugetlbfs.so \
HUGETLB_MORECORE=yes ./benchmark 2>> result_hugetlb.txt
# 4. THP 관련 tracepoint
perf stat -e 'huge_memory:mm_khugepaged_scan_pmd' \
-e 'huge_memory:mm_collapse_huge_page' \
-e 'huge_memory:mm_collapse_huge_page_swapin' \
-a sleep 10
# khugepaged의 스캔 및 합체 활동 모니터링 (시스템 전체, 10초간)
- < 0.1% — 양호. TLB 캐시가 워킹셋을 충분히 커버
- 0.1% ~ 1% — 경미한 오버헤드. Huge Page 도입으로 개선 가능
- > 1% — TLB 병목 심각. Huge Page가 큰 효과를 줄 수 있음
- 데이터 TLB(dTLB) 미스가 높으면 heap/data 영역에 Huge Page 적용
- 명령어 TLB(iTLB) 미스가 높으면 코드 영역(text)이 크거나 분산됨 — Huge Page 효과 제한적
Memory Compaction
메모리 단편화(fragmentation)를 해소하여 연속 페이지 할당을 가능하게 합니다. 빈 메모리는 충분하지만 연속되지 않아 Huge Pages나 order가 큰 할당이 실패하는 상황을 해결합니다:
빈 메모리: 1000 × 4KB = 4MB (충분!)
하지만 order 9 할당(2MB) 실패!
[사용중][빈][빈][사용중][빈][빈][빈][사용중][빈]...
Compaction 후:
[사용중][사용중][사용중][빈][빈][빈][빈][빈][빈]...
↑ 2MB 연속 블록 확보!
Two-Scanner 알고리즘
/* mm/compaction.c */
static void compact_zone(struct zone *zone)
{
unsigned long migrate_pfn = zone->zone_start_pfn;
unsigned long free_pfn = zone_end_pfn(zone);
while (migrate_pfn < free_pfn) {
/* migrate scanner: 이동 가능한 페이지 찾기 */
struct page *page = find_suitable_migration_target(migrate_pfn);
/* free scanner: 빈 페이지 찾기 */
struct page *free_page = find_free_pfn(free_pfn);
/* 페이지 마이그레이션 */
migrate_pages(&page_list, compaction_alloc, ...);
migrate_pfn++;
free_pfn--;
}
}
Migrate Types
| Type | 설명 | 예시 |
|---|---|---|
MIGRATE_UNMOVABLE | 이동 불가 | 커널 코드, DMA 버퍼 |
MIGRATE_MOVABLE | 이동 가능 | 유저 공간 페이지 |
MIGRATE_RECLAIMABLE | 회수 가능 | Page Cache, Slab |
MIGRATE_PCPTYPES | Per-CPU | Per-CPU 페이지 캐시 |
Anti-fragmentation: 커널은 Migrate Type별로 페이지를 분리하여 관리합니다. Unmovable 페이지가 Movable 영역을 침범하지 않도록 하여 단편화를 사전에 방지합니다.
kcompactd — Compaction 데몬
/* mm/compaction.c */
static int kcompactd(void *p)
{
while (!kthread_should_stop()) {
/* 단편화 점수 확인 */
if (should_compact_retry(...))
compact_node(pgdat);
/* 대기 (기본 20초) */
schedule_timeout_interruptible(msecs_to_jiffies(20000));
}
return 0;
}
Compaction 트리거
- Direct Compaction — Huge Pages 할당 실패 시 할당 경로에서 직접 실행
- Background Compaction — kcompactd가 주기적으로 실행
- Proactive Compaction — 미리 단편화 방지 (v5.9+)
- 명시적 트리거 —
/proc/sys/vm/compact_memory에 쓰기
# 수동 compaction 트리거
echo 1 > /proc/sys/vm/compact_memory
# 특정 Node Compaction
echo 1 > /sys/devices/system/node/node0/compact
# 단편화 상태 확인
cat /proc/buddyinfo
# Node 0, zone Normal 1024 512 256 128 64 32 16 8 4 2 1
# 오른쪽으로 갈수록 큰 연속 블록 수
# 상세 페이지 타입 정보
cat /proc/pagetypeinfo
# Fragmentation Index (1에 가까울수록 단편화 심함)
cat /sys/kernel/debug/extfrag/extfrag_index
# Compaction 통계
cat /proc/vmstat | grep compact
# compact_stall: Direct compaction 횟수 (계속 증가하면 문제!)
# compact_fail / compact_success: 실패/성공 횟수
# proactive compaction (v5.9+): 0=비활성, 100=매우 공격적
echo 20 > /proc/sys/vm/compaction_proactiveness
# Fragmentation threshold (0~1000, 낮을수록 compaction 빈도 증가)
echo 500 > /proc/sys/vm/extfrag_threshold
Compaction 성능 영향
| 시나리오 | CPU 오버헤드 | 지연시간 | 권장 설정 |
|---|---|---|---|
| 데이터베이스 (Huge Pages) | 낮음 | 중간 | proactiveness=30 |
| 실시간 애플리케이션 | 높음 | 높음 | proactiveness=0 |
| 범용 서버 | 중간 | 낮음 | proactiveness=20 |
| 컨테이너 호스트 | 중간 | 중간 | proactiveness=25 |
Direct Compaction 지연: 할당 경로에서 실행되어 수십 ms의 지연을 유발합니다. compact_stall 카운터가 계속 증가하면 proactiveness를 높여 사전 compaction을 강화하세요.
Page Migration
NUMA 시스템에서 페이지를 노드 간 이동시켜 메모리 접근 지역성을 개선합니다:
/* 커널 내부 API */
int migrate_pages(struct list_head *l, new_page_t get_new,
free_page_t put_new, unsigned long private,
enum migrate_mode mode, int reason);
/* 사용자 공간에서 */
#include <numaif.h>
long move_pages(int pid, unsigned long count,
void **pages, const int *nodes,
int *status, int flags);
KSM (Kernel Samepage Merging)
KSM은 내용이 동일한 페이지를 자동으로 병합하여 메모리를 절약합니다. KVM 가상화에서 동일 게스트 OS 이미지를 여러 VM이 공유할 때 특히 유용합니다.
# KSM 활성화
echo 1 > /sys/kernel/mm/ksm/run
# KSM 상태 확인
cat /sys/kernel/mm/ksm/pages_shared # 공유된 페이지 수
cat /sys/kernel/mm/ksm/pages_sharing # 공유로 절약된 페이지 수
cat /sys/kernel/mm/ksm/pages_unshared # 고유한 페이지 수
# 튜닝 파라미터
echo 200 > /sys/kernel/mm/ksm/sleep_millisecs # 스캔 간격
echo 256 > /sys/kernel/mm/ksm/pages_to_scan # 스캔당 페이지 수
/* 애플리케이션에서 KSM 영역 지정 */
madvise(addr, length, MADV_MERGEABLE); /* KSM 스캔 대상 등록 */
madvise(addr, length, MADV_UNMERGEABLE); /* KSM 대상에서 제외 */
Swapping 서브시스템
Swapping은 물리 메모리(RAM) 부족 시 익명 페이지(anonymous page)를 디스크의 스왑 영역으로 내보내고, 다시 접근할 때 복원하는 메커니즘입니다. 파일 기반 페이지는 원본 파일에서 다시 읽을 수 있지만, 힙·스택·mmap(MAP_ANONYMOUS) 등 backing store가 없는 익명 페이지는 스왑 영역이 유일한 저장소입니다.
상세 문서: Swapping 서브시스템의 심층 분석은 Swapping 서브시스템 페이지에서 확인할 수 있습니다. swap 공간 설정, swap cache, swap out/in 경로, swappiness 튜닝, zswap/zram 압축 스왑 등 전체 내용을 다룹니다.
주요 개념
- Swap 공간: 전용 파티션 또는 파일로 구성, 우선순위 기반 다중 스왑 지원
- Swap Cache: 스왑 아웃된 페이지를 메모리에 캐싱하여 중복 I/O 방지
- kswapd: 백그라운드 메모리 회수 데몬
- zswap/zram: 압축 기반 스왑으로 I/O 부하 감소
- swappiness: 익명 페이지 vs 파일 캐시 회수 비율 조정 (0-200)
자세한 내용은 Swapping 서브시스템 페이지를 참고하세요.
tmpfs (RAM 기반 파일시스템)
tmpfs는 페이지 캐시와 스왑을 저장소로 사용하는 메모리 기반 파일시스템입니다. 커널 내부적으로 mm/shmem.c에 구현되며, POSIX shared memory(/dev/shm), System V shared memory, mmap(MAP_ANONYMOUS)의 내부 backing store로도 사용됩니다.
tmpfs vs ramfs
| 특성 | tmpfs | ramfs |
|---|---|---|
| 크기 제한 | size= 옵션으로 제한 가능 (기본: 물리 메모리의 50%) |
제한 없음 — 메모리를 모두 소진할 수 있음 |
| 스왑 사용 | 메모리 부족 시 스왑으로 페이지 이동 | 스왑 불가 — 항상 RAM에 상주 |
| 페이지 회수 | kswapd/direct reclaim 대상 | 회수 불가능 (pinned) |
| memcg 계정 | memory cgroup에 정확히 계정됨 | 계정됨 (커널 버전에 따라 다름) |
| THP 지원 | 지원 (huge= 옵션) |
미지원 |
| 구현 | mm/shmem.c (복잡, 기능 풍부) |
fs/ramfs/ (단순, ~200줄) |
| 프로덕션 사용 | 권장 (/tmp, /run, /dev/shm) |
비권장 (OOM 위험) |
ramfs의 위험성: ramfs는 크기 제한 메커니즘이 없어, 실수로 대용량 데이터를 쓰면 시스템 전체 메모리를 소진하여 OOM Killer가 발동합니다. 프로덕션 환경에서는 tmpfs 사용을 강력히 권장합니다.
ramfs 내부 구현
ramfs는 Linux VFS의 가장 단순한 파일시스템 구현으로, fs/ramfs/inode.c 약 300줄로 이루어져 있습니다. VFS가 제공하는 simple_* 헬퍼 함수를 최대한 활용하여 최소한의 코드로 완전한 파일시스템을 구성합니다.
/* fs/ramfs/inode.c — ramfs 핵심 구현 */
/* ramfs의 inode 생성 */
struct inode *ramfs_get_inode(struct super_block *sb,
const struct inode *dir,
umode_t mode, dev_t dev)
{
struct inode *inode = new_inode(sb);
if (inode) {
inode->i_ino = get_next_ino();
inode_init_owner(&nop_mnt_idmap, inode, dir, mode);
inode->i_atime = inode->i_mtime = inode->i_ctime =
current_time(inode);
/* 매핑 초기화: 페이지 캐시에 직접 저장 */
mapping_set_gfp_mask(inode->i_mapping,
GFP_HIGHUSER | __GFP_ZERO);
mapping_set_unevictable(inode->i_mapping);
/* ↑ 핵심: AS_UNEVICTABLE 설정 → 페이지 회수 불가 */
switch (mode & S_IFMT) {
case S_IFREG: /* 일반 파일 */
inode->i_op = &ramfs_file_inode_operations;
inode->i_fop = &ramfs_file_operations;
break;
case S_IFDIR: /* 디렉토리 */
inode->i_op = &ramfs_dir_inode_operations;
inode->i_fop = &simple_dir_operations;
inc_nlink(inode); /* . 엔트리 */
break;
case S_IFLNK: /* 심볼릭 링크 */
inode->i_op = &page_symlink_inode_operations;
inode_nohighmem(inode);
break;
default: /* 디바이스 노드 등 */
init_special_inode(inode, mode, dev);
break;
}
}
return inode;
}
/* ramfs 슈퍼블록 초기화 */
static int ramfs_fill_super(struct super_block *sb,
struct fs_context *fc)
{
sb->s_maxbytes = MAX_LFS_FILESIZE; /* 파일 크기 제한 없음 */
sb->s_blocksize = PAGE_SIZE;
sb->s_magic = RAMFS_MAGIC; /* 0x858458f6 */
sb->s_op = &ramfs_ops;
sb->s_time_gran = 1;
/* 루트 inode/dentry 생성 */
struct inode *inode = ramfs_get_inode(sb, NULL,
S_IFDIR | fsi->mount_opts.mode, 0);
sb->s_root = d_make_root(inode);
return sb->s_root ? 0 : -ENOMEM;
}
/* ramfs 파일 오퍼레이션 — 대부분 커널 generic 함수 재사용 */
const struct file_operations ramfs_file_operations = {
.read_iter = generic_file_read_iter,
.write_iter = generic_file_write_iter,
.mmap = generic_file_mmap,
.fsync = noop_fsync, /* 디스크 없음 → noop */
.splice_read = filemap_splice_read,
.llseek = generic_file_llseek,
};
/* ramfs 디렉토리 inode 오퍼레이션 */
const struct inode_operations ramfs_dir_inode_operations = {
.create = ramfs_create,
.lookup = simple_lookup,
.link = simple_link,
.unlink = simple_unlink,
.symlink = ramfs_symlink,
.mkdir = ramfs_mkdir,
.rmdir = simple_rmdir,
.mknod = ramfs_mknod,
.rename = simple_rename,
};
simple_* 함수(simple_lookup, simple_link, simple_unlink, simple_rmdir, simple_rename)와 generic_file_* 함수를 재사용합니다. 자체 구현은 inode 생성(ramfs_get_inode)과 슈퍼블록 초기화(ramfs_fill_super) 정도뿐입니다. 스왑, 크기 제한, quota 등 복잡한 기능이 없어 파일시스템 구현의 최소 사례(minimal reference)로 자주 인용됩니다.
mapping_set_unevictable() 호출이 ramfs의 핵심 특성을 결정합니다. 이 플래그(AS_UNEVICTABLE)가 설정되면 해당 매핑의 페이지들은 LRU의 unevictable 리스트에 배치되어 kswapd와 direct reclaim의 회수 대상에서 완전히 제외됩니다. 따라서 ramfs에 쓴 데이터는 삭제하기 전까지 RAM에서 절대 제거되지 않습니다.
rootfs와 initramfs
Linux 커널 부팅 과정에서 rootfs는 특수한 ramfs 인스턴스로, 커널이 가장 먼저 마운트하는 루트 파일시스템입니다. CONFIG_TMPFS가 활성화된 경우 rootfs는 tmpfs로 동작합니다.
/* init/do_mounts.c — rootfs 초기화 */
/*
* rootfs는 커널 부팅 초기에 마운트되는 특수 파일시스템입니다.
* init/main.c → vfs_caches_init() → mnt_init() → init_rootfs()
* → init_mount_tree() 순서로 초기화됩니다.
*/
static struct file_system_type rootfs_fs_type = {
.name = "rootfs",
.init_fs_context = rootfs_init_fs_context,
.kill_sb = kill_litter_super,
};
int __init rootfs_init_fs_context(struct fs_context *fc)
{
#ifdef CONFIG_TMPFS
/* CONFIG_TMPFS 활성화 → rootfs를 tmpfs로 (스왑/크기 제한 지원) */
return shmem_init_fs_context(fc);
#else
/* CONFIG_TMPFS 비활성화 → 순수 ramfs */
return ramfs_init_fs_context(fc);
#endif
}
# initramfs 내용 확인
lsinitrd /boot/initramfs-$(uname -r).img # RHEL/Fedora
lsinitramfs /boot/initrd.img-$(uname -r) # Debian/Ubuntu
# initramfs 수동 해제
mkdir /tmp/initramfs && cd /tmp/initramfs
zcat /boot/initramfs-$(uname -r).img | cpio -idmv
# 커널 내장 initramfs 확인 (CONFIG_INITRAMFS_SOURCE)
cat /proc/cmdline | grep -o 'initrd=[^ ]*'
# rootfs가 tmpfs인지 ramfs인지 확인
grep CONFIG_TMPFS /boot/config-$(uname -r)
# CONFIG_TMPFS=y → rootfs는 tmpfs 기반
# CONFIG_TMPFS=n → rootfs는 ramfs 기반 (매우 드묾)
# switch_root 과정 (initramfs의 /init에서 실행)
# mount /dev/sda2 /mnt/root
# exec switch_root /mnt/root /sbin/init
# → rootfs의 모든 파일 삭제, 새 root로 pivot
switch_root vs pivot_root: switch_root는 initramfs(ramfs/tmpfs) 전용으로, 기존 rootfs의 모든 파일을 삭제한 후 새 루트로 전환합니다. ramfs/tmpfs는 마운트 해제가 불가능하므로(커널의 초기 마운트) 내용만 비웁니다. 반면 pivot_root는 블록 디바이스 기반 initrd에서 사용되며, 기존 루트를 다른 위치에 재마운트합니다. 현대 시스템은 대부분 initramfs + switch_root 조합을 사용합니다.
tmpfs 내부 구조 (shmem)
tmpfs는 커널의 shmem(shared memory) 서브시스템 위에 구축됩니다. 데이터는 페이지 캐시에 저장되고, 메모리 부족 시 스왑으로 내려갑니다.
/* mm/shmem.c — tmpfs의 핵심 자료구조 */
/* shmem inode 정보 */
struct shmem_inode_info {
spinlock_t lock;
unsigned int seals; /* F_SEAL_* 플래그 */
unsigned long flags;
unsigned long alloced; /* 할당된 페이지 수 */
unsigned long swapped; /* 스왑된 페이지 수 */
struct shared_policy policy; /* NUMA 정책 */
struct simple_xattrs xattrs; /* 확장 속성 */
struct inode vfs_inode; /* 내장 VFS inode */
};
/* shmem 슈퍼블록 정보 */
struct shmem_sb_info {
unsigned long max_blocks; /* 최대 페이지 수 (size= 옵션) */
unsigned long used_blocks; /* 현재 사용 중인 페이지 */
unsigned long max_inodes; /* 최대 inode 수 (nr_inodes=) */
unsigned long free_inodes; /* 남은 inode 수 */
int huge; /* THP 정책 */
kuid_t uid; /* 마운트 시 소유자 */
kgid_t gid;
umode_t mode; /* 퍼미션 */
struct mempolicy *mpol; /* NUMA 메모리 정책 */
spinlock_t stat_lock;
};
tmpfs 페이지 폴트 처리 경로
프로세스가 tmpfs 파일을 mmap()한 후 실제 메모리에 접근하면, 아직 물리 페이지가 할당되지 않았으므로 페이지 폴트가 발생합니다. 커널은 shmem_fault() → shmem_get_folio() 경로를 통해 페이지를 할당하거나 스왑에서 복원합니다.
/* mm/shmem.c — tmpfs 페이지 폴트 처리 */
/*
* VM 폴트 핸들러: VFS → vm_operations_struct.fault → shmem_fault()
* mmap된 tmpfs 파일의 페이지 접근 시 호출됩니다.
*/
static vm_fault_t shmem_fault(struct vm_fault *vmf)
{
struct inode *inode = file_inode(vmf->vma->vm_file);
gfp_t gfp = mapping_gfp_mask(inode->i_mapping);
struct folio *folio;
int err;
/* fallocate와의 경합 방지 */
if (unlikely(inode->i_private)) {
struct shmem_falloc *shmem_falloc;
spin_lock(&inode->i_lock);
shmem_falloc = inode->i_private;
if (shmem_falloc &&
shmem_falloc->waitq &&
vmf->pgoff >= shmem_falloc->start &&
vmf->pgoff < shmem_falloc->next)
wait_event_killable(*shmem_falloc->waitq, ...);
spin_unlock(&inode->i_lock);
}
/* 핵심: folio(페이지) 획득 */
err = shmem_get_folio_gfp(inode, vmf->pgoff,
&folio, SGP_CACHE, gfp, vmf, &vmf->ret);
if (err)
return vmf_error(err);
vmf->page = folio_file_page(folio, vmf->pgoff);
return VM_FAULT_LOCKED; /* 페이지가 잠긴 상태로 반환 */
}
/*
* shmem_get_folio_gfp() — tmpfs 페이지 획득의 핵심 함수
* 3가지 경로 중 하나로 페이지를 반환합니다:
*
* 1. Page Cache 히트: XArray에서 folio를 찾아 즉시 반환
* 2. Swap-in: XArray에 swap entry가 있으면 스왑에서 읽어 복원
* 3. 새 할당: folio를 할당하고 XArray에 삽입
*/
static int shmem_get_folio_gfp(struct inode *inode,
pgoff_t index, struct folio **foliop,
enum sgp_type sgp, gfp_t gfp,
struct vm_fault *vmf, vm_fault_t *fault_type)
{
struct address_space *mapping = inode->i_mapping;
struct shmem_inode_info *info = SHMEM_I(inode);
struct folio *folio;
int error;
repeat:
/* 1단계: XArray(Page Cache)에서 검색 */
folio = filemap_get_folio(mapping, index);
if (!IS_ERR(folio)) {
/* Page Cache 히트 — 빠른 경로 */
*foliop = folio;
return 0;
}
/* 2단계: swap entry인지 확인 */
folio = xa_load(&mapping->i_pages, index);
if (xa_is_value(folio)) {
/* Swap entry 발견 → 스왑에서 읽기 */
error = shmem_swapin_folio(inode, index,
foliop, sgp, gfp, vmf, fault_type);
if (!error)
info->swapped--;
return error;
}
/* 3단계: 새 folio 할당 */
folio = shmem_alloc_and_add_folio(gfp, inode,
index, fault_type, is_huge_enabled(info));
if (IS_ERR(folio))
return PTR_ERR(folio);
/* 새 페이지를 0으로 초기화 */
folio_zero_range(folio, 0, folio_size(folio));
/* 사용량 계정 업데이트 */
info->alloced += folio_nr_pages(folio);
shmem_recalc_inode(inode, ...);
*foliop = folio;
return 0;
}
shmem_get_folio_gfp()의 sgp 파라미터는 페이지 획득 정책을 결정합니다. SGP_READ: 존재하는 페이지만 반환 (없으면 zero page), SGP_CACHE: 없으면 새로 할당하여 캐시에 등록, SGP_WRITE: 쓰기를 위한 획득 (CoW 등 처리), SGP_FALLOC: fallocate용 (i_size 초과 영역도 할당).
tmpfs 마운트 옵션
# 기본 마운트 (기본 크기: 물리 메모리의 50%)
mount -t tmpfs tmpfs /mnt/tmp
# 크기 제한 지정
mount -t tmpfs -o size=2G tmpfs /mnt/tmp
mount -t tmpfs -o size=50% tmpfs /mnt/tmp # RAM의 50%
# 전체 옵션 예시
mount -t tmpfs -o size=1G,nr_inodes=100k,mode=1777,uid=0,gid=0 tmpfs /tmp
# 런타임 크기 변경 (리마운트)
mount -o remount,size=4G /tmp
# THP(Transparent Huge Pages) 활성화
mount -t tmpfs -o size=2G,huge=always tmpfs /mnt/huge_tmp
mount -t tmpfs -o size=2G,huge=within_size tmpfs /mnt/huge_tmp
# NUMA 메모리 정책 적용
mount -t tmpfs -o size=1G,mpol=bind:0 tmpfs /mnt/numa_tmp
mount -t tmpfs -o size=1G,mpol=interleave:0-1 tmpfs /mnt/numa_tmp
mount -t tmpfs -o size=1G,mpol=prefer:0 tmpfs /mnt/numa_tmp
| 옵션 | 기본값 | 설명 |
|---|---|---|
size= |
물리 메모리의 50% | 최대 사용 가능 크기 (bytes, k, m, g, 또는 %) |
nr_blocks= |
size 기반 계산 | 최대 블록 수 (페이지 단위). size=와 상호 배타적 |
nr_inodes= |
물리 메모리 페이지의 50% | 최대 inode(파일/디렉토리) 수. 0이면 무제한 |
mode= |
1777 |
루트 디렉토리 퍼미션 |
uid= / gid= |
0 / 0 | 루트 디렉토리 소유자 |
huge= |
never |
THP 정책: never, always, within_size, advise |
mpol= |
default |
NUMA 정책: default, prefer:N, bind:N, interleave:N-M |
noswap |
비활성 | 스왑 사용 금지 (v6.4+). 데이터가 항상 RAM에 상주 |
tmpfs와 스왑의 관계
tmpfs의 핵심 특성은 메모리 부족 시 데이터를 스왑 영역으로 내보낼 수 있다는 점입니다. ramfs와의 가장 큰 차이점이며, 이를 통해 물리 메모리보다 큰 tmpfs 사용이 이론적으로 가능합니다.
/* mm/shmem.c — 스왑 아웃 경로 (간략화) */
/*
* shmem_writepage()는 메모리 회수 시 호출됩니다.
* Page Cache의 페이지를 스왑 영역에 기록하고,
* XArray에는 swap entry를 저장합니다.
*/
static int shmem_writepage(struct page *page,
struct writeback_control *wbc)
{
struct shmem_inode_info *info;
swp_entry_t swap;
/* 스왑 슬롯 할당 */
swap = get_swap_page(page);
if (!swap.val)
return 0; /* 스왑 공간 부족 */
/* XArray에서 page를 swap entry로 교체 */
xa_store(&mapping->i_pages, index, swp_to_radix_entry(swap), ...);
info->swapped++;
/* 스왑 영역에 페이지 데이터 기록 */
swap_writepage(page, wbc);
return 0;
}
/*
* shmem_swapin_folio()는 스왑된 페이지 접근 시 호출됩니다.
* 스왑 영역에서 데이터를 읽어 Page Cache로 복원합니다.
*/
# tmpfs 스왑 관련 확인
df -h /tmp
# tmpfs 2.0G 156M 1.9G 8% /tmp
# tmpfs가 스왑에 얼마나 기록했는지 확인
cat /proc/meminfo | grep -i shmem
# Shmem: 512000 kB ← 모든 tmpfs/shmem의 총 사용량
# ShmemHugePages: 0 kB ← THP 사용 중인 tmpfs 페이지
# ShmemPmdMapped: 0 kB ← PMD 매핑된 tmpfs 페이지
# 개별 마운트 포인트의 스왑 사용량은 직접 확인 불가
# → inode별 swapped 카운트는 커널 내부 정보
# 스왑 공간이 없으면 tmpfs는 size= 제한과 물리 메모리 중 작은 값만 사용 가능
swapon --show
# NAME TYPE SIZE USED PRIO
# /dev/sda2 partition 8G 1.2G -2
mount -t tmpfs -o noswap tmpfs /mnt/secure로 마운트하면 해당 tmpfs의 데이터가 스왑 영역에 기록되지 않습니다. 암호화 키, 비밀번호 등 민감한 데이터가 디스크에 유출되는 것을 방지할 때 유용합니다. 단, 물리 메모리만 사용하므로 메모리 부족 시 쓰기가 실패(-ENOMEM)합니다.
시스템에서의 tmpfs 활용
현대 Linux 시스템에서 tmpfs는 다양한 핵심 위치에 마운트됩니다:
| 마운트 포인트 | 용도 | 일반적 크기 | 관리 주체 |
|---|---|---|---|
/tmp |
임시 파일 | RAM의 50% | systemd (tmp.mount unit) |
/run |
런타임 데이터 (PID 파일, 소켓) | RAM의 10~25% | initramfs / systemd |
/dev/shm |
POSIX shared memory (shm_open()) |
RAM의 50% | systemd / fstab |
/sys/fs/cgroup |
cgroup v1 마운트 포인트 | 소량 | systemd (cgroup v1 한정) |
/run/user/<uid> |
사용자별 런타임 디렉토리 | RAM의 10% | systemd-logind (RuntimeDirectory) |
컨테이너 /dev |
컨테이너 내부 디바이스 노드 | 64MB (Docker 기본) | 컨테이너 런타임 (runc) |
# 시스템의 모든 tmpfs 마운트 확인
mount -t tmpfs
# tmpfs on /run type tmpfs (rw,nosuid,nodev,noexec,size=3244852k,mode=755)
# tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
# tmpfs on /tmp type tmpfs (rw,nosuid,nodev,size=16224256k)
# tmpfs on /run/user/1000 type tmpfs (rw,nosuid,nodev,size=3244848k,...
# tmpfs 사용량 통합 확인
df -h -t tmpfs
# Filesystem Size Used Avail Use% Mounted on
# tmpfs 3.1G 1.5M 3.1G 1% /run
# tmpfs 16G 0 16G 0% /dev/shm
# tmpfs 16G 128K 16G 1% /tmp
# systemd에서 /tmp를 tmpfs로 활성화/비활성화
systemctl enable tmp.mount # /tmp를 tmpfs로
systemctl disable tmp.mount # /tmp를 디스크에 유지
memfd와 File Sealing
memfd_create()는 tmpfs 기반의 익명 파일을 생성합니다. File sealing 메커니즘을 통해 파일 내용이나 크기를 봉인(seal)하여, IPC 시 수신 측이 데이터의 불변성을 보장받을 수 있습니다.
#include <sys/mman.h>
#include <linux/memfd.h>
#include <fcntl.h>
/* 1. memfd 생성 (sealing 가능) */
int fd = memfd_create("shared_buf", MFD_ALLOW_SEALING);
ftruncate(fd, 4096); /* 크기 설정 */
/* 2. 데이터 기록 */
void *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
memcpy(p, data, data_len);
munmap(p, 4096);
/* 3. 봉인 적용 — 이후 쓰기/크기변경/축소 불가 */
fcntl(fd, F_ADD_SEALS, F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_GROW);
/* 4. fd를 Unix 도메인 소켓으로 전달 (SCM_RIGHTS) */
/* 수신 측은 seal을 검증하여 데이터 불변성 확인 가능 */
int seals = fcntl(fd, F_GET_SEALS);
if (seals & F_SEAL_WRITE)
/* 안전: 송신 측이 더 이상 수정 불가 */
/* Seal 종류:
* F_SEAL_SEAL — 추가 seal 적용 금지
* F_SEAL_SHRINK — ftruncate 축소 금지
* F_SEAL_GROW — ftruncate 확대 금지
* F_SEAL_WRITE — write/mmap(PROT_WRITE) 금지
* F_SEAL_FUTURE_WRITE — 기존 writable mmap은 유지, 새 write 금지 (v5.1+)
*/
/* memfd + huge pages (THP) */
int fd = memfd_create("huge_buf", MFD_HUGETLB | MFD_HUGE_2MB);
/* HugeTLB 기반 memfd — DPDK, QEMU 등에서 활용 */
memfd 활용 사례: Wayland 컴포지터(Weston, Mutter)는 memfd_create() + F_SEAL_SHRINK로 클라이언트와 공유 버퍼를 교환합니다. QEMU/KVM은 MFD_HUGETLB로 게스트 메모리를 할당합니다. systemd의 MemoryDenyWriteExecute=와 함께 사용하면 JIT-free IPC 구현도 가능합니다.
tmpfs와 Memory Cgroup
tmpfs 사용량은 해당 파일을 생성한 프로세스의 memory cgroup에 계정됩니다. 컨테이너 환경에서 tmpfs 사용량이 메모리 제한에 포함된다는 점을 명확히 인지해야 합니다.
# 컨테이너 내 tmpfs 사용량이 memcg 제한에 포함됨
# Docker에서 --tmpfs 옵션 사용 시:
docker run --memory=512m --tmpfs /tmp:size=256m myapp
# /tmp에 200MB 쓰면 → memory.current가 200MB 증가
# 총 메모리 사용이 512MB 초과 시 OOM!
# cgroup v2에서 tmpfs/shmem 사용량 확인
cat /sys/fs/cgroup/myapp/memory.stat
# shmem 204800 ← tmpfs + shared memory 사용량 (bytes)
# file 1048576 ← 파일 캐시 (tmpfs 포함)
# Kubernetes Pod에서 emptyDir medium: Memory (tmpfs 기반)
# → Pod의 메모리 limit에 포함되므로 sizeLimit 설정 필수
# volumes:
# - name: cache
# emptyDir:
# medium: Memory
# sizeLimit: 128Mi
컨테이너 OOM 주의: Docker의 --tmpfs 또는 Kubernetes의 emptyDir.medium: Memory로 생성된 tmpfs 사용량은 컨테이너의 메모리 제한(cgroup memory.max)에 포함됩니다. tmpfs에 대량 데이터를 쓰면 예기치 않은 OOM Kill이 발생할 수 있습니다.
tmpfs와 Transparent Huge Pages
tmpfs는 THP를 사용하여 대용량 파일의 TLB 미스를 줄일 수 있습니다. 마운트 시 huge= 옵션으로 제어합니다.
huge= 값 | 동작 | 적합한 사용 사례 |
|---|---|---|
never |
THP 비사용 (기본값) | 소규모 파일이 많은 일반적인 /tmp |
always |
항상 THP 할당 시도 | 대용량 공유 메모리 (DB, QEMU) |
within_size |
파일 크기 범위 내에서만 THP | 크기가 다양한 파일 혼용 시 |
advise |
madvise(MADV_HUGEPAGE) 요청 시만 |
애플리케이션이 명시적으로 제어 |
# tmpfs THP 사용량 모니터링
grep -i shmem /proc/meminfo
# ShmemHugePages: 2097152 kB ← 2MB 단위 THP 사용량
# ShmemPmdMapped: 2097152 kB ← PMD(2MB) 매핑된 페이지
# 시스템 전역 tmpfs THP 정책 (per-mount 설정 우선)
cat /sys/kernel/mm/transparent_hugepage/shmem_enabled
# [never] always within_size advise deny force
tmpfs 보안 고려사항
| 위협 | 설명 | 대응 |
|---|---|---|
| 스왑 유출 | tmpfs 데이터가 스왑을 통해 디스크에 기록될 수 있음 | noswap 옵션 (v6.4+), mlock(), 또는 암호화 스왑 사용 |
| 메모리 고갈 (DoS) | 사용자가 tmpfs에 대량 데이터를 써서 시스템 메모리 고갈 | size=로 크기 제한, memcg 쿼터 설정 |
| world-writable /tmp | 다른 사용자의 임시 파일 접근/심볼릭 링크 공격 | mode=1777 (sticky bit), O_NOFOLLOW, mkstemp() |
| 코어 덤프 유출 | tmpfs에 저장된 민감 데이터가 코어 덤프에 포함 | madvise(MADV_DONTDUMP), prctl(PR_SET_DUMPABLE, 0) |
| 재부팅 후 데이터 잔존 | tmpfs는 RAM 기반이므로 재부팅 시 데이터 소멸. 이를 장점으로 활용 가능 | 민감 데이터를 tmpfs에 저장하여 자동 삭제 보장 |
# 보안 강화된 /tmp tmpfs 마운트 (fstab 예시)
# tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev,size=2G,mode=1777 0 0
# noexec: /tmp에서 실행 파일 실행 금지 (악성코드 방지)
# nosuid: SUID 비트 무시
# nodev: 디바이스 파일 생성 금지
# 암호화 스왑으로 tmpfs 스왑 유출 방지
# /etc/crypttab:
# cswap /dev/sda2 /dev/urandom swap,cipher=aes-xts-plain64,size=256
# 프로세스별 tmpfs 격리 (systemd PrivateTmp)
# [Service]
# PrivateTmp=yes ← 서비스별 /tmp, /var/tmp를 별도 tmpfs로 격리
tmpfs 성능 특성
# tmpfs vs ext4 vs xfs 성능 비교 (일반적 경향)
#
# 작업 tmpfs ext4(SSD) ext4(HDD)
# ───────────────────────────────────────────────
# 순차 읽기 ~10GB/s ~3GB/s ~200MB/s
# 순차 쓰기 ~8GB/s ~2.5GB/s ~180MB/s
# 랜덤 4K IOPS ~2M ~500K ~200
# 파일 생성 ~500K/s ~100K/s ~5K/s
# 지연 시간 ~1μs ~10μs ~5ms
#
# ※ 실제 수치는 하드웨어, 워크로드, 메모리 상태에 따라 달라짐
# fio로 tmpfs 벤치마크
fio --name=tmpfs_test --directory=/tmp \
--rw=randrw --bs=4k --size=1G \
--numjobs=4 --time_based --runtime=30 \
--ioengine=libaio --direct=0
# tmpfs에서 direct=1(O_DIRECT)은 의미 없음
# tmpfs는 페이지 캐시 자체가 저장소이므로 항상 "cached" I/O
빌드 성능 최적화: 대규모 소프트웨어 빌드 시 /tmp이나 빌드 디렉토리를 tmpfs에 배치하면 I/O 병목을 제거할 수 있습니다. 특히 ccache, sccache와 함께 사용하면 빌드 시간이 크게 단축됩니다. 단, 충분한 RAM이 있어야 하며, 빌드 산출물이 tmpfs 크기를 초과하지 않도록 주의하십시오.
tmpfs fallocate & Hole Punch
tmpfs는 fallocate() 시스템 콜을 지원하여 공간 사전 할당과 hole punching(부분 해제)이 가능합니다. 특히 hole punch는 tmpfs에서 파일의 특정 범위만 메모리에서 해제하여 RAM을 즉시 회수하는 강력한 기법입니다.
/* mm/shmem.c — shmem_fallocate() 핵심 경로 */
static long shmem_fallocate(struct file *file,
int mode, loff_t offset, loff_t len)
{
struct inode *inode = file_inode(file);
struct shmem_sb_info *sbinfo = SHMEM_SB(inode->i_sb);
if (mode & FALLOC_FL_PUNCH_HOLE) {
/* Hole Punch: 지정 범위의 페이지를 Page Cache에서 제거
* → 물리 메모리 즉시 회수 (스왑된 페이지도 해제) */
shmem_truncate_range(inode,
offset, offset + len - 1);
return 0;
}
if (mode & FALLOC_FL_KEEP_SIZE) {
/* i_size 변경 없이 블록만 사전 할당
* → ENOSPC 방지용 공간 예약 */
}
/* 기본 fallocate: 페이지를 미리 할당하고 0으로 초기화
* sbinfo->used_blocks 업데이트 → 할당량(size=) 차감 */
for (index = start; index < end; index++) {
error = shmem_get_folio(inode, index,
&folio, SGP_FALLOC);
/* 이미 존재하는 페이지는 건너뜀 */
if (!error)
folio_unlock(folio);
}
/* 필요 시 i_size 확장 */
if (!(mode & FALLOC_FL_KEEP_SIZE))
i_size_write(inode, offset + len);
return error;
}
/* 사용자 공간에서 tmpfs fallocate 활용 예제 */
#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
int fd = open("/tmp/data", O_RDWR | O_CREAT, 0644);
/* 1. 공간 사전 할당 (1GB) — ENOSPC 사전 감지 */
if (fallocate(fd, 0, 0, 1ULL << 30) < 0)
perror("tmpfs 공간 부족"); /* ENOSPC */
/* 2. Hole Punch — 오프셋 4KB~1MB 범위의 메모리 즉시 해제 */
fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
4096, 1048576 - 4096);
/* 3. 파일 축소 없이 끝부분 블록만 예약 */
fallocate(fd, FALLOC_FL_KEEP_SIZE, 0, 2ULL << 30);
/* 4. Zero Range (v3.15+) — 범위를 0으로 초기화하되 할당 유지 */
fallocate(fd, FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE,
0, 4096);
Hole Punch 활용: 대용량 tmpfs 파일에서 더 이상 필요 없는 영역을 FALLOC_FL_PUNCH_HOLE로 해제하면 unlink() 없이도 메모리를 즉시 회수할 수 있습니다. 데이터베이스 임시 파일, 대규모 버퍼 풀, 또는 링 버퍼 구현 시 유용합니다. madvise(MADV_REMOVE)도 동일한 효과를 제공합니다.
tmpfs Quota (v6.1+)
커널 6.1부터 tmpfs는 사용자/그룹별 용량 제한(quota)을 지원합니다. 기존에는 size= 옵션으로 전체 크기만 제한할 수 있었지만, quota를 통해 멀티유저 환경에서 사용자별 tmpfs 사용량을 제어할 수 있습니다.
# tmpfs quota 활성화 마운트 (v6.1+)
mount -t tmpfs -o size=4G,usrquota tmpfs /tmp
mount -t tmpfs -o size=4G,usrquota,grpquota tmpfs /tmp
# quota 제한 설정 (xfs_quota, quota 도구 사용)
# setquota -u username 1G 2G 0 0 /tmp
# └─ 소프트 1G, 하드 2G (inode는 무제한)
# quota 사용량 확인
repquota -s /tmp
# User used soft hard grace files
# root -- 128M 0 0 15
# user1 +- 1.2G 1G 2G 6days 230
# 개별 사용자 quota 확인
quota -u user1 -f /tmp
/* mm/shmem.c — tmpfs quota 내부 구현 */
/* tmpfs quota는 VFS의 generic quota를 활용합니다.
* CONFIG_TMPFS_QUOTA=y 필요 (v6.1+)
*
* 기존 디스크 기반 quota와 달리:
* - quota 데이터가 메모리에만 존재 (재부팅 시 초기화)
* - quotacheck 불필요 (항상 정확)
* - 저널링 오버헤드 없음
*/
/* shmem 슈퍼블록에 quota 관련 옵션 추가 */
enum shmem_param {
Opt_quota,
Opt_usrquota,
Opt_grpquota,
Opt_usrquota_block_hardlimit,
Opt_usrquota_inode_hardlimit,
Opt_grpquota_block_hardlimit,
Opt_grpquota_inode_hardlimit,
/* ... */
};
/* 페이지 할당 시 quota 차감 흐름 */
/* shmem_get_folio() → dquot_alloc_block()
* → 사용자/그룹의 현재 사용량 + 요청량 vs 제한 검사
* → 초과 시 -EDQUOT 반환 → write()가 실패 */
| 마운트 옵션 | 설명 | 비고 |
|---|---|---|
usrquota |
사용자별 quota 활성화 | CONFIG_TMPFS_QUOTA=y 필요 |
grpquota |
그룹별 quota 활성화 | CONFIG_TMPFS_QUOTA=y 필요 |
usrquota_block_hardlimit= |
사용자 기본 블록 하드 제한 | v6.7+ (마운트 시 기본값 설정) |
usrquota_inode_hardlimit= |
사용자 기본 inode 하드 제한 | v6.7+ |
grpquota_block_hardlimit= |
그룹 기본 블록 하드 제한 | v6.7+ |
grpquota_inode_hardlimit= |
그룹 기본 inode 하드 제한 | v6.7+ |
tmpfs quota의 휘발성: tmpfs quota 정보는 RAM에만 존재하므로 재부팅 시 모든 quota 설정이 초기화됩니다. 영구적 quota 설정이 필요하면 /etc/fstab의 마운트 옵션에 하드 제한 기본값을 지정하고, 부팅 스크립트에서 setquota를 실행해야 합니다.
tmpfs ENOSPC 처리
tmpfs에서 size= 제한이나 물리 메모리 부족으로 공간이 부족하면 -ENOSPC(No space left on device) 에러가 발생합니다. 디스크 파일시스템과 달리 tmpfs의 공간 부족은 메모리 상황에 따라 동적으로 변할 수 있습니다.
/* mm/shmem.c — ENOSPC 발생 조건 */
/* 블록(페이지) 할당 시 검사 */
static int shmem_reserve_inode(struct super_block *sb, ...)
{
struct shmem_sb_info *sbinfo = SHMEM_SB(sb);
if (sbinfo->max_inodes) {
if (sbinfo->free_inodes < 1)
return -ENOSPC; /* nr_inodes= 제한 초과 */
sbinfo->free_inodes--;
}
return 0;
}
/* 페이지 쓰기 시 검사 (shmem_get_folio 내부) */
/*
* ENOSPC 발생 시나리오:
* 1. used_blocks >= max_blocks (size= 제한 도달)
* 2. free_inodes == 0 (nr_inodes= 제한 도달)
* 3. 메모리 할당 실패 + 스왑 공간 부족
* 4. quota 초과 시 -EDQUOT (v6.1+)
*/
# tmpfs 공간 상태 모니터링
df -h /tmp
# Filesystem Size Used Avail Use% Mounted on
# tmpfs 2.0G 1.8G 200M 90% /tmp
# inode 사용률 확인 (파일 수 제한)
df -ih /tmp
# Filesystem Inodes IUsed IFree IUse% Mounted on
# tmpfs 100K 95K 5K 95% /tmp
# 런타임 크기 확장 (서비스 중단 없음)
mount -o remount,size=4G /tmp
# inode 제한도 런타임 변경 가능
mount -o remount,nr_inodes=200k /tmp
# 큰 파일부터 찾아서 정리
find /tmp -xdev -type f -size +100M -exec ls -lh {} \;
# 열린 파일이 삭제된 경우 (공간 미회수)
lsof +L1 /tmp # unlinked but open 파일 목록
# 해당 프로세스를 재시작해야 공간 회수
숨겨진 tmpfs 공간 소비: df로 확인되는 Used와 실제 메모리 사용량이 다를 수 있습니다. 삭제(unlink)되었지만 아직 열려 있는 파일, 스왑으로 내려간 페이지, 그리고 fallocate()로 예약만 하고 미사용 중인 블록이 있을 수 있습니다. lsof +L1로 삭제되었으나 열린 파일을 확인하고, /proc/meminfo의 Shmem 값으로 실제 메모리 사용량을 파악하세요.
devtmpfs (/dev 관리)
devtmpfs는 커널이 자동으로 디바이스 노드를 생성하는 특수한 tmpfs 변형입니다. 부팅 초기에 /dev에 마운트되어, 커널이 디바이스를 감지할 때마다 해당 디바이스 노드(/dev/sda, /dev/tty0 등)를 자동으로 생성합니다.
/* drivers/base/devtmpfs.c — devtmpfs 핵심 구현 */
/*
* devtmpfs는 kdevtmpfs 커널 스레드가 관리합니다.
* device_add() → devtmpfs_create_node()
* device_del() → devtmpfs_delete_node()
*/
static struct file_system_type dev_fs_type = {
.name = "devtmpfs",
.init_fs_context = shmem_init_fs_context, /* tmpfs 기반! */
/* CONFIG_TMPFS 비활성화 시 ramfs 기반으로 폴백 */
};
/* 디바이스 추가 시 노드 자동 생성 */
static int handle_create(const char *name,
umode_t mode, kuid_t uid,
kgid_t gid, struct device *dev)
{
struct dentry *dentry;
struct path path;
/* 필요한 중간 디렉토리 자동 생성 (/dev/bus/usb 등) */
dentry = kern_path_create(AT_FDCWD, name, &path, 0);
/* mknod로 디바이스 노드 생성 */
vfs_mknod(&nop_mnt_idmap, d_inode(path.dentry),
dentry, mode, dev->devt);
/* 소유권/퍼미션 설정 */
vfs_fchown(dentry, uid, gid);
vfs_fchmod(dentry, mode);
return 0;
}
/* kdevtmpfs 스레드: 요청 큐를 처리하는 루프 */
static int devtmpfsd(void *p)
{
while (1) {
wait_for_completion(&req_done);
/* 큐의 create/delete 요청 순차 처리 */
handle(requests->name, requests->mode, ...);
}
}
# devtmpfs 마운트 확인
mount | grep devtmpfs
# devtmpfs on /dev type devtmpfs (rw,nosuid,size=8168440k,nr_inodes=2042110,mode=755)
# devtmpfs가 자동 생성한 노드 vs udev가 보정한 노드
ls -la /dev/sda
# brw-rw---- 1 root disk 8, 0 ... /dev/sda ← udev가 퍼미션/그룹 설정
# udev 없이 devtmpfs만으로 부팅 가능 (임베디드/rescue)
# 커널 파라미터: devtmpfs.mount=1
# → 커널이 /dev를 직접 마운트 (initramfs 이전)
# devtmpfs 비활성화 (매우 드묾)
grep CONFIG_DEVTMPFS /boot/config-$(uname -r)
# CONFIG_DEVTMPFS=y
# CONFIG_DEVTMPFS_MOUNT=y ← 커널이 자동 마운트
# kdevtmpfs 커널 스레드 확인
ps aux | grep kdevtmpfs
# root 27 0.0 0.0 0 0 ? S ... [kdevtmpfs]
/dev/sda 등)를 즉시 생성합니다. udev는 이후 netlink uevent를 받아 심볼릭 링크(/dev/disk/by-id/..., /dev/disk/by-uuid/...), 퍼미션, 소유자 등을 rules에 따라 보정합니다. devtmpfs 덕분에 udev가 아직 시작되지 않은 부팅 초기에도 디바이스에 접근할 수 있으며, 임베디드 환경에서는 udev 없이 devtmpfs만으로 운영이 가능합니다.
Memory Cgroup (memcg)
cgroup을 통한 프로세스 그룹별 메모리 제한:
# cgroup v2 메모리 제한 설정
mkdir /sys/fs/cgroup/myapp
echo 512M > /sys/fs/cgroup/myapp/memory.max # 하드 제한
echo 400M > /sys/fs/cgroup/myapp/memory.high # 소프트 제한 (reclaim 유도)
echo 256M > /sys/fs/cgroup/myapp/memory.low # 보호 수준
echo 128M > /sys/fs/cgroup/myapp/memory.min # 절대 보호
# 프로세스를 cgroup에 할당
echo $PID > /sys/fs/cgroup/myapp/cgroup.procs
# 메모리 사용량 확인
cat /sys/fs/cgroup/myapp/memory.current # 현재 사용량
cat /sys/fs/cgroup/myapp/memory.stat # 상세 통계
cat /sys/fs/cgroup/myapp/memory.events # OOM 등 이벤트
DAMON (Data Access Monitor)
DAMON은 커널 5.15에서 도입된 메모리 접근 패턴 모니터링 프레임워크입니다. 실제 메모리 접근 패턴을 기반으로 reclaim, compaction 등을 최적화합니다.
# DAMON sysfs 인터페이스 (v5.18+)
ls /sys/kernel/mm/damon/
# admin/ (관리 인터페이스)
# DAMON 기반 reclaim (DAMON_RECLAIM)
echo Y > /sys/module/damon_reclaim/parameters/enabled
echo 200 > /sys/module/damon_reclaim/parameters/min_age # 최소 비접근 시간(ms)
# DAMON 기반 LRU 정렬 (DAMON_LRU_SORT, v6.0+)
echo Y > /sys/module/damon_lru_sort/parameters/enabled
Memory Hotplug
가상화 환경에서 런타임에 메모리를 추가/제거합니다:
# 메모리 블록 상태 확인
ls /sys/devices/system/memory/
# memory0/ memory1/ memory2/ ...
cat /sys/devices/system/memory/memory32/state
# online
# 메모리 블록 오프라인
echo offline > /sys/devices/system/memory/memory32/state
# 메모리 블록 온라인
echo online > /sys/devices/system/memory/memory32/state
KASAN / KFENCE
메모리 안전성 검증 도구:
# KASAN (Kernel Address Sanitizer) - 개발/테스트용
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y # 또는 CONFIG_KASAN_SW_TAGS (ARM64)
# 성능 오버헤드: ~2-3x, 메모리 오버헤드: ~2-3x
# KFENCE (Kernel Electric Fence) - 프로덕션 가능 (v5.12+)
CONFIG_KFENCE=y
# 확률적 샘플링 기반, 매우 낮은 오버헤드 (<1%)
echo 100 > /sys/module/kfence/parameters/sample_interval # ms
/proc 메모리 인터페이스
# /proc/meminfo 주요 항목
cat /proc/meminfo
# MemTotal: 32768000 kB ← 전체 물리 메모리
# MemFree: 10240000 kB ← 미사용 메모리
# MemAvailable: 20480000 kB ← 실제 사용 가능 추정치
# Buffers: 512000 kB ← 블록 디바이스 캐시
# Cached: 8192000 kB ← 페이지 캐시
# SwapCached: 10240 kB ← 스왑에서 다시 읽은 캐시
# Slab: 1024000 kB ← Slab 할당자 사용량
# SReclaimable: 768000 kB ← 회수 가능 Slab
# /proc/vmstat - VM 통계
cat /proc/vmstat | grep -E "pgfault|pgmajfault|pswpin|pswpout|compact"
# /proc/zoneinfo - 존별 상세 정보
cat /proc/zoneinfo | head -50
커널 버전별 주요 변경
| 버전 | 기능 | 설명 |
|---|---|---|
| 3.5 | CMA | Contiguous Memory Allocator 도입 |
| 4.14 | memcg v2 | cgroup v2 메모리 컨트롤러 안정화 |
| 5.9 | Proactive Compaction | 백그라운드 자동 compaction |
| 5.12 | KFENCE | 프로덕션용 경량 메모리 오류 감지 |
| 5.15 | DAMON | 데이터 접근 모니터링 프레임워크 |
| 5.18 | DAMON sysfs | DAMON sysfs 관리 인터페이스 |
| 6.0 | DAMON LRU Sort | DAMON 기반 LRU 리스트 최적화 |
| 6.1 | maple tree | VMA 관리에 maple tree 도입 (rbtree 대체) |
참고 자료: 커널 메모리 관리 문서, LWN의 memory management 시리즈, Documentation/mm/
메모리 할당자 심화
할당 컨텍스트별 제약사항
커널 메모리 할당은 호출 컨텍스트에 따라 사용 가능한 GFP 플래그가 엄격히 제한됩니다.
| 컨텍스트 | 허용 GFP | 슬립 가능 | 설명 |
|---|---|---|---|
| 프로세스 컨텍스트 | GFP_KERNEL | 예 | 가장 일반적. 직접 회수, I/O, 스왑 모두 가능 |
| 인터럽트 컨텍스트 (hardirq) | GFP_ATOMIC | 아니오 | 비상 예비 풀 사용. 실패 가능성 높음 |
| 소프트IRQ / 타이머 | GFP_ATOMIC | 아니오 | hardirq와 동일한 제약 |
| 스핀락 보유 중 | GFP_ATOMIC | 아니오 | 슬립 시 데드락 발생 |
| 프로세스 (I/O 불가) | GFP_NOIO | 예 | 블록 I/O 재귀 방지 (블록 드라이버 내부) |
| 프로세스 (FS 불가) | GFP_NOFS | 예 | 파일시스템 재귀 방지 (VFS 코드 내부) |
| 사용자 공간 대신 | GFP_USER | 예 | 사용자 프로세스 대신 할당, OOM killer 대상 |
| DMA 영역 필요 | GFP_DMA / GFP_DMA32 | 예 | ISA DMA(16MB 이하) 또는 32비트 DMA 장치 |
치명적 실수: 인터럽트 컨텍스트에서 GFP_KERNEL 사용 시 슬립이 발생하여 BUG: scheduling while atomic 패닉이 발생합니다. in_interrupt(), in_atomic() 등으로 컨텍스트를 확인하고, 컨텍스트에 맞는 GFP 플래그를 선택하십시오.
/* 올바른 컨텍스트별 할당 패턴 */
/* 인터럽트 핸들러에서 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_data *data;
data = kmalloc(sizeof(*data), GFP_ATOMIC);
if (!data)
return IRQ_HANDLED; /* GFP_ATOMIC 실패는 흔함 */
/* ... */
}
/* 블록 I/O 경로에서 */
static int my_block_submit(struct bio *bio)
{
/* GFP_NOIO: submit_bio → 할당 → submit_bio 무한 재귀 방지 */
buf = kmalloc(BUF_SIZE, GFP_NOIO);
/* ... */
}
/* 파일시스템 코드에서 */
static int my_fs_write(struct inode *inode)
{
/* GFP_NOFS: writeback → 할당 → writeback 무한 재귀 방지 */
page = alloc_page(GFP_NOFS);
/* ... */
}
kmalloc 내부 구현과 크기 클래스
kmalloc은 내부적으로 미리 생성된 SLUB 캐시(kmalloc-8, kmalloc-16, ..., kmalloc-8192)에서 오브젝트를 할당합니다. 요청 크기는 가장 가까운 2의 거듭제곱으로 반올림됩니다.
| 요청 크기 | 실제 할당 | 내부 단편화 | 사용 캐시 |
|---|---|---|---|
| 1~8 bytes | 8 bytes | 최대 87.5% | kmalloc-8 |
| 9~16 bytes | 16 bytes | 최대 43.75% | kmalloc-16 |
| 17~32 bytes | 32 bytes | 최대 46.875% | kmalloc-32 |
| 33~64 bytes | 64 bytes | 최대 48.44% | kmalloc-64 |
| 65~96 bytes | 96 bytes | 최대 32.29% | kmalloc-96 |
| 97~128 bytes | 128 bytes | 최대 24.22% | kmalloc-128 |
| 129~192 bytes | 192 bytes | 최대 32.81% | kmalloc-192 |
| 193~256 bytes | 256 bytes | 최대 24.61% | kmalloc-256 |
| ... | ... | ... | ... |
| 4097~8192 bytes | 8192 bytes | 최대 49.99% | kmalloc-8192 |
크기 최적화: 구조체 크기를 kmalloc 크기 클래스 경계에 맞추면 내부 단편화를 줄일 수 있습니다. cat /proc/slabinfo에서 kmalloc-* 캐시의 사용량을 모니터링하십시오. KMALLOC_MAX_SIZE는 아키텍처마다 다르며, x86-64에서 기본 SLUB_MAX_ORDER=3이므로 32KB(8 pages)입니다.
/* kmalloc 크기 확인 */
size_t actual = ksize(kmalloc(100, GFP_KERNEL));
/* ksize() 반환: 128 (실제 할당된 usable 크기) */
/* krealloc: 재할당 (가능하면 in-place) */
ptr = krealloc(ptr, new_size, GFP_KERNEL);
if (!ptr)
return -ENOMEM;
/* kcalloc: 배열 할당 (오버플로 검사 포함) */
arr = kcalloc(n_elements, sizeof(*arr), GFP_KERNEL);
/* n_elements * sizeof(*arr) 오버플로 시 NULL 반환 */
/* kmalloc_array: kcalloc과 동일하지만 zero-init 안 함 */
arr = kmalloc_array(n_elements, sizeof(*arr), GFP_KERNEL);
/* 큰 할당에 kvmalloc (kmalloc 시도 → vmalloc fallback) */
buf = kvmalloc(large_size, GFP_KERNEL);
kvfree(buf); /* kmalloc/vmalloc 자동 판별 해제 */
SLUB 디버깅과 튜닝
SLUB allocator는 다양한 디버깅 옵션과 런타임 튜닝 파라미터를 제공합니다.
# SLUB 디버깅 부트 파라미터
slub_debug=FZPU # 모든 캐시에 대해 디버깅 활성화
slub_debug=FZ,kmalloc-128 # 특정 캐시에만 적용
# 디버깅 플래그:
# F - Sanity checks (free 검증)
# Z - Red zone (오브젝트 경계 오버런 탐지)
# P - Poisoning (use-after-free 탐지)
# U - User tracking (할당/해제 호출자 기록)
# T - Trace (할당/해제 시 로그 출력)
# 런타임 슬랩 정보 확인
cat /proc/slabinfo
# name
# kmalloc-256 1024 1280 256 32 2
# slabinfo 도구로 상세 분석
slabinfo -T # Top slabs by size
slabinfo -S # Sort by slab size
slabinfo -A # Activity report
# slabtop: 실시간 모니터링
slabtop -s c # 캐시 크기 기준 정렬
# SLUB sysfs 인터페이스
ls /sys/kernel/slab/kmalloc-256/
# align, alloc_fastpath, cache_dma, cpu_slabs, hwcache_align,
# min_partial, object_size, objects, objs_per_slab, order,
# partial, red_zone, sanity_checks, shrink, slab_size, ...
# 특정 캐시의 통계
cat /sys/kernel/slab/kmalloc-256/alloc_fastpath
cat /sys/kernel/slab/kmalloc-256/alloc_slowpath
cat /sys/kernel/slab/kmalloc-256/free_fastpath
성능 영향: slub_debug는 할당/해제마다 검증을 수행하므로 성능이 크게 저하됩니다. 프로덕션 환경에서는 CONFIG_KFENCE=y를 사용하면 샘플링 기반으로 메모리 오류를 낮은 오버헤드로 탐지할 수 있습니다.
Per-CPU 할당자
Per-CPU 변수는 각 CPU마다 독립된 복사본을 유지하여 락 없이 데이터에 접근할 수 있는 메커니즘입니다. 멀티프로세서 환경에서 공유 데이터에 대한 캐시 라인 바운싱(cache line bouncing)을 방지하고, 동기화 오버헤드 없이 높은 성능을 달성합니다.
왜 Per-CPU인가?: 여러 CPU가 동일한 캐시 라인에 속한 변수를 수정하면, MESI 프로토콜에 의해 해당 캐시 라인이 끊임없이 무효화(invalidate)됩니다. 이를 false sharing 또는 cache line bouncing이라 합니다. Per-CPU 변수는 각 CPU에 독립된 메모리 영역을 할당하여 이 문제를 원천적으로 제거합니다. 통계 카운터, 참조 카운트, 임시 버퍼 등에서 spinlock 대비 수십 배의 성능 향상을 얻을 수 있습니다.
Per-CPU 메모리 레이아웃
커널의 Per-CPU 데이터는 정적 영역과 동적 영역으로 나뉩니다. 정적 Per-CPU 변수는 컴파일 시 .data..percpu ELF 섹션에 배치되고, 부팅 시 CPU 수만큼 복제됩니다.
/* include/asm-generic/percpu.h — 주소 변환 핵심 */
extern unsigned long __per_cpu_offset[NR_CPUS];
#define per_cpu_offset(cpu) (__per_cpu_offset[cpu])
/* Per-CPU 포인터 → 특정 CPU의 실제 주소 */
#define per_cpu_ptr(ptr, cpu) \
(typeof(ptr))(((unsigned long)(ptr)) + per_cpu_offset(cpu))
/* arch/x86/kernel/setup_percpu.c — 부팅 시 Per-CPU 영역 초기화 */
void __init setup_per_cpu_areas(void)
{
unsigned long delta;
int cpu;
/* pcpu_alloc_alloc 함수로 각 CPU의 Per-CPU 메모리 영역 할당 */
rc = pcpu_embed_first_chunk(PERCPU_MODULE_RESERVE,
PERCPU_DYNAMIC_RESERVE, PAGE_SIZE, NULL,
pcpu_fc_alloc, pcpu_fc_free);
/* 각 CPU에 대해 offset 설정 */
for_each_possible_cpu(cpu) {
per_cpu_offset(cpu) = delta + pcpu_unit_offsets[cpu];
/* 주요 Per-CPU 변수 초기화 */
per_cpu(this_cpu_off, cpu) = per_cpu_offset(cpu);
per_cpu(cpu_number, cpu) = cpu;
setup_percpu_segment(cpu); /* x86: GS/FS 세그먼트 설정 */
}
}
x86에서의 최적화: x86_64에서는 GS 세그먼트 레지스터가 현재 CPU의 Per-CPU 베이스를 가리킵니다. this_cpu_read(var)는 단일 mov %gs:offset, %reg 명령어로 컴파일되어, offset 배열 참조 없이 O(1)으로 접근합니다. ARM64에서는 TPIDR_EL1 레지스터가 동일한 역할을 합니다.
정적 Per-CPU 변수 선언
DEFINE_PER_CPU 매크로 패밀리로 정적 Per-CPU 변수를 선언합니다. 각 변형은 ELF 섹션 배치와 접근 특성이 다릅니다.
/* include/linux/percpu-defs.h — Per-CPU 선언 매크로 */
/* 기본 — .data..percpu 섹션, 캐시라인 정렬 없음 */
DEFINE_PER_CPU(unsigned long, my_counter);
/* 공유 정렬 — 캐시라인 경계에 정렬, false sharing 방지 */
DEFINE_PER_CPU_SHARED_ALIGNED(struct my_data, shared_data);
/* → SMP에서 ____cacheline_aligned_in_smp 적용 */
/* 첫 번째 청크 — 부트 시 사용, 성능 최적화 */
DEFINE_PER_CPU_FIRST(struct my_data, first_data);
/* → .data..percpu..first 섹션, 고정 오프셋 보장 */
/* 페이지 정렬 — 큰 구조체에 사용 */
DEFINE_PER_CPU_PAGE_ALIGNED(struct big_struct, page_data);
/* → .data..percpu..page_aligned 섹션, PAGE_SIZE 정렬 */
/* 읽기 전용 — 초기화 후 변경 없음 */
DEFINE_PER_CPU_READ_MOSTLY(int, cpu_constant);
/* → .data..percpu..read_mostly 섹션 */
/* → 쓰기 빈도가 낮아 다른 Per-CPU 변수와 캐시라인 분리 */
/* 선언만 (extern) — 다른 파일에서 정의된 변수 참조 */
DECLARE_PER_CPU(unsigned long, my_counter);
DECLARE_PER_CPU_SHARED_ALIGNED(struct my_data, shared_data);
DECLARE_PER_CPU_READ_MOSTLY(int, cpu_constant);
| 매크로 | ELF 섹션 | 정렬 | 용도 |
|---|---|---|---|
DEFINE_PER_CPU | .data..percpu | 기본 | 범용 Per-CPU 변수 |
DEFINE_PER_CPU_SHARED_ALIGNED | .data..percpu..shared_aligned | 캐시라인 | 자주 접근, false sharing 방지 |
DEFINE_PER_CPU_FIRST | .data..percpu..first | 페이지 | 고정 오프셋 필요 (irq stack 등) |
DEFINE_PER_CPU_PAGE_ALIGNED | .data..percpu..page_aligned | 페이지 | GDT, TSS 등 큰 구조체 |
DEFINE_PER_CPU_READ_MOSTLY | .data..percpu..read_mostly | 기본 | 초기화 후 거의 읽기만 |
Per-CPU 접근 API
Per-CPU 변수 접근에는 여러 API가 있으며, preemption 상태와 원자성 요구사항에 따라 선택합니다.
/* ============================================ */
/* 1. this_cpu_* 매크로 (현재 CPU, preemption disabled 필요) */
/* ============================================ */
preempt_disable();
/* 읽기/쓰기 */
val = this_cpu_read(my_counter);
this_cpu_write(my_counter, 42);
/* 산술 연산 */
this_cpu_inc(my_counter); /* ++ */
this_cpu_dec(my_counter); /* -- */
this_cpu_add(my_counter, 5); /* += */
this_cpu_sub(my_counter, 3); /* -= */
/* 비트 연산 */
this_cpu_and(my_flags, ~FLAG_MASK); /* &= */
this_cpu_or(my_flags, FLAG_MASK); /* |= */
/* 원자적 교환 (RMW) */
old = this_cpu_xchg(my_counter, new_val);
old = this_cpu_cmpxchg(my_counter, expected, new_val);
preempt_enable();
/* ============================================ */
/* 2. __this_cpu_* (preemption 검증 없는 고속 버전) */
/* ============================================ */
/* 이미 preemption disabled인 것이 보장된 컨텍스트 */
/* (인터럽트 핸들러, softirq, preempt_disable 구간 등) */
__this_cpu_inc(my_counter); /* preempt count 검증 생략 */
__this_cpu_add(my_counter, 5);
/* ============================================ */
/* 3. raw_cpu_* (인스트루먼테이션 없는 최저수준 접근) */
/* ============================================ */
/* NMI, 매우 이른 부팅 경로 등 특수 상황 */
raw_cpu_inc(my_counter);
/* ============================================ */
/* 4. get_cpu_var / put_cpu_var (자동 preemption 관리) */
/* ============================================ */
get_cpu_var(my_counter)++; /* preempt_disable + 현재 CPU 값 참조 */
put_cpu_var(my_counter); /* preempt_enable */
/* 포인터 형태 */
struct my_data *p = &get_cpu_var(my_data);
p->field = value;
put_cpu_var(my_data);
/* ============================================ */
/* 5. per_cpu / per_cpu_ptr (특정 CPU 접근) */
/* ============================================ */
/* 정적 Per-CPU 변수 — 특정 CPU의 값 */
val = per_cpu(my_counter, cpu_id);
/* 동적 Per-CPU 포인터 — 특정 CPU의 포인터 */
struct my_data *p = per_cpu_ptr(dyn_ptr, cpu_id);
/* 모든 CPU 순회 합산 (읽기 전용, preemption 불필요) */
unsigned long total = 0;
int cpu;
for_each_possible_cpu(cpu)
total += per_cpu(my_counter, cpu);
/* for_each_online_cpu: 현재 온라인 CPU만 (hotplug 안전) */
for_each_online_cpu(cpu)
total += *per_cpu_ptr(dyn_ptr, cpu);
| API 패밀리 | Preemption | 검증 | 대상 CPU | 사용 컨텍스트 |
|---|---|---|---|---|
this_cpu_* | 직접 관리 | O (debug) | 현재 CPU | 일반 커널 코드 |
__this_cpu_* | 이미 disabled | X | 현재 CPU | IRQ, softirq, preempt off 구간 |
raw_cpu_* | 없음 | X | 현재 CPU | NMI, early boot |
get/put_cpu_var | 자동 | O | 현재 CPU | 간단한 접근 |
per_cpu() | 불필요 | - | 지정 CPU | 다른 CPU 값 읽기 |
per_cpu_ptr() | 불필요 | - | 지정 CPU | 동적 Per-CPU 포인터 |
Preemption 주의: this_cpu_* 매크로는 preemption이 비활성화된 상태에서만 사용해야 합니다. preemption이 활성화된 상태에서 사용하면, 연산 도중 다른 CPU로 마이그레이션되어 잘못된 CPU의 데이터를 수정할 수 있습니다. get_cpu_var()/put_cpu_var() 쌍이 자동으로 관리하지만, 성능이 중요한 경로에서는 명시적 preempt_disable()/preempt_enable()을 사용하십시오.
동적 Per-CPU 할당
모듈이나 런타임에 Per-CPU 데이터가 필요한 경우 동적 할당을 사용합니다. 내부적으로 Chunk 기반 할당자가 Per-CPU 영역의 빈 공간을 관리합니다.
/* include/linux/percpu.h — 동적 Per-CPU 할당 API */
/* 기본 할당 — 자연 정렬 */
void __percpu *ptr = alloc_percpu(struct my_data);
/* → __alloc_percpu(sizeof(struct my_data), __alignof__(struct my_data)) */
/* 명시적 크기/정렬 지정 */
void __percpu *ptr = __alloc_percpu(size, align);
/* GFP 플래그 지정 (커널 5.15+) */
void __percpu *ptr = __alloc_percpu_gfp(size, align, GFP_KERNEL);
/* 해제 */
free_percpu(ptr);
/* ---- 사용 예제 ---- */
struct net_stats {
u64 rx_packets;
u64 tx_packets;
u64 rx_bytes;
u64 tx_bytes;
};
struct my_device {
struct net_stats __percpu *stats; /* __percpu 어노테이션 */
/* ... */
};
static int my_dev_init(struct my_device *dev)
{
dev->stats = alloc_percpu(struct net_stats);
if (!dev->stats)
return -ENOMEM;
return 0;
}
/* 패킷 수신 경로 (softirq 컨텍스트, preemption 이미 disabled) */
static void my_dev_rx(struct my_device *dev, struct sk_buff *skb)
{
struct net_stats *stats = this_cpu_ptr(dev->stats);
stats->rx_packets++;
stats->rx_bytes += skb->len;
}
/* 통계 읽기 (user context, 모든 CPU 합산) */
static void my_dev_get_stats(struct my_device *dev,
struct net_stats *total)
{
int cpu;
memset(total, 0, sizeof(*total));
for_each_possible_cpu(cpu) {
struct net_stats *s = per_cpu_ptr(dev->stats, cpu);
total->rx_packets += s->rx_packets;
total->tx_packets += s->tx_packets;
total->rx_bytes += s->rx_bytes;
total->tx_bytes += s->tx_bytes;
}
}
static void my_dev_cleanup(struct my_device *dev)
{
free_percpu(dev->stats);
}
Per-CPU 할당자 내부 구현
동적 Per-CPU 할당자는 mm/percpu.c에 구현되어 있으며, Chunk 단위로 메모리를 관리합니다. 각 Chunk는 모든 CPU에 대해 동일 크기의 Unit을 포함하며, Unit 내에서 first-fit 방식으로 할당합니다.
/* mm/percpu-internal.h — Chunk 핵심 구조체 */
struct pcpu_chunk {
struct list_head list; /* pcpu_slot[] 리스트 */
int free_bytes; /* 총 여유 바이트 */
int contig_bits; /* 최대 연속 여유 블록 (비트) */
int first_bit; /* 첫 여유 비트 위치 */
int nr_pages; /* 매핑된 페이지 수 */
unsigned long *alloc_map; /* 사용 중 비트맵 */
unsigned long *bound_map; /* 할당 경계 비트맵 */
struct pcpu_block_md *md_blocks; /* 블록별 메타데이터 */
void *base_addr; /* Unit 0 시작 주소 */
struct page **pages; /* 물리 페이지 배열 */
};
/* mm/percpu.c — 할당 핵심 경로 (단순화) */
static void __percpu *pcpu_alloc(size_t size, size_t align,
bool reserved, gfp_t gfp)
{
struct pcpu_chunk *chunk;
int off;
/* 1단계: 기존 chunk에서 여유 공간 탐색 (pcpu_slot[] 순회) */
list_for_each_entry(chunk, &pcpu_slot[slot], list) {
off = pcpu_find_block_fit(chunk, bits, bit_align);
if (off >= 0)
goto area_found;
}
/* 2단계: 기존 chunk에 공간 없음 → 새 chunk 생성 */
chunk = pcpu_create_chunk(pcpu_gfp);
area_found:
/* 3단계: 비트맵 갱신, 물리 페이지 매핑 (필요시) */
pcpu_alloc_area(chunk, bits, bit_align, off);
pcpu_populate_chunk(chunk, rs, re, gfp); /* 페이지 할당/매핑 */
/* 반환: base_addr + offset (모든 CPU에 동일 offset 적용) */
return __addr_to_pcpu_ptr(chunk->base_addr + off);
}
percpu_counter — 정밀 Per-CPU 카운터
단순 Per-CPU 변수 합산은 모든 CPU를 순회해야 하므로 비용이 높습니다. percpu_counter는 중앙 카운트(count)와 CPU별 로컬 카운트(counters)를 결합하여, 읽기 시 근사값을 O(1)로 제공하고, 정확한 값이 필요할 때만 전체 합산합니다.
/* include/linux/percpu_counter.h */
struct percpu_counter {
raw_spinlock_t lock;
s64 count; /* 중앙 카운트 */
s32 __percpu *counters; /* CPU별 로컬 카운트 */
};
/* 초기화/해제 */
percpu_counter_init(&pcnt, initial_value, GFP_KERNEL);
percpu_counter_destroy(&pcnt);
/* 더하기 — 로컬 카운트에 누적 */
percpu_counter_add(&pcnt, 1);
/* → 로컬 |count| > batch이면 중앙으로 flush */
/* → batch 기본값 = max(32, nr_cpus * 2) */
/* 빼기 */
percpu_counter_sub(&pcnt, 5);
/* batch 크기 직접 지정 */
percpu_counter_add_batch(&pcnt, delta, my_batch);
/* 근사값 읽기 — O(1), 오차 ±(nr_cpus × batch) */
s64 approx = percpu_counter_read_positive(&pcnt);
/* 정확한 합산 읽기 — 모든 CPU 순회, 비용 높음 */
s64 exact = percpu_counter_sum(&pcnt);
/* 0 이상인지 근사 판정 */
bool positive = percpu_counter_positive(&pcnt);
/* 임계값 비교 (정확도와 성능 균형) */
bool over = percpu_counter_compare(&pcnt, threshold) > 0;
/* → |count - threshold| > batch*nr_cpus이면 근사값만으로 판정 */
/* → 아니면 sum()으로 정확히 비교 */
커널 내 사용 사례: ext4의 free block/inode 카운트(ext4_sb_info.s_freeclusters_counter), 네트워크의 소켓 메모리 카운팅, VM의 vm_committed_as(가상 메모리 커밋 추적) 등이 percpu_counter를 사용합니다. 파일시스템에서 여유 블록 수를 매 할당마다 정확히 계산하면 병목이 되므로, 근사값으로 판단하고 임계치에 가까울 때만 정확히 합산합니다.
percpu_ref — Per-CPU 참조 카운트
percpu_ref는 참조 카운팅의 빠른 경로(Per-CPU)와 정확한 경로(atomic)를 모드 전환으로 결합합니다. 일반적으로 Per-CPU 모드로 동작하여 락프리 성능을 제공하고, 리소스 해제 시 atomic 모드로 전환하여 참조가 0이 되는 순간을 정확히 감지합니다.
/* include/linux/percpu-refcount.h */
struct percpu_ref {
atomic_long_t count; /* atomic 모드 카운트 */
unsigned long __percpu *percpu_count_ptr; /* Per-CPU 카운트 */
percpu_ref_func_t *release; /* count=0 시 호출 */
percpu_ref_func_t *confirm_switch; /* 모드 전환 완료 콜백 */
unsigned long flags;
};
/* 생명주기: Per-CPU 모드 → kill → atomic 모드 → count=0 → release */
/* 초기화 — Per-CPU 모드로 시작, release 콜백 등록 */
percpu_ref_init(&ref, my_release_fn, 0, GFP_KERNEL);
/* 참조 획득/해제 — Per-CPU 모드: 매우 빠름 (preempt_disable + 로컬 inc/dec) */
percpu_ref_get(&ref);
percpu_ref_put(&ref);
/* tryget — 이미 kill된 상태이면 실패 */
if (!percpu_ref_tryget_live(&ref))
return -ENODEV;
/* kill — atomic 모드로 전환, 초기 참조 1 제거 */
/* → 이후 모든 get/put은 atomic_long_inc/dec */
/* → Per-CPU 카운트를 atomic으로 합산 (call_rcu 기반) */
percpu_ref_kill(&ref);
/* 모든 참조가 해제되면 release 콜백 자동 호출 */
static void my_release_fn(struct percpu_ref *ref)
{
struct my_obj *obj = container_of(ref, struct my_obj, ref);
complete(&obj->done); /* 또는 kfree, workqueue 등 */
}
사용 사례: block I/O의 request_queue 참조 카운팅, cgroup 서브시스템의 참조 관리, MD/RAID의 I/O 활성 추적 등에서 percpu_ref를 사용합니다. 일반 운영 시 수백만 회/초의 get/put이 락 없이 수행되고, 장치 제거나 cgroup 삭제 시에만 atomic 모드로 전환하여 정확한 참조 추적을 수행합니다.
Per-CPU 사용 시 주의사항
| 실수 유형 | 증상 | 올바른 해결책 |
|---|---|---|
| Preemption 미비활성화 | 다른 CPU 데이터 수정 (데이터 손상) | preempt_disable() 또는 get_cpu_var() 사용 |
this_cpu_*를 슬립 가능 구간에서 사용 | BUG: scheduling while atomic | 변수를 로컬에 복사 후 작업 |
per_cpu_ptr 포인터를 preempt 구간 밖에서 역참조 | 잘못된 CPU 데이터 접근 | 포인터 역참조를 preempt off 내에서 수행 |
| CPU 오프라인 미고려 | 오프라인 CPU의 데이터 누락/유실 | CPU hotplug 노티파이어에서 데이터 마이그레이션 |
for_each_possible_cpu 대신 for_each_online_cpu 사용 | 나중에 온라인된 CPU 데이터 누락 | 초기화 순회: possible, 합산 순회: 목적에 따라 선택 |
| Per-CPU 변수에 포인터 저장 후 다른 CPU에서 역참조 | cache coherency 문제 | 포인터 대상 데이터도 Per-CPU이거나 적절히 동기화 |
alloc_percpu 반환값 직접 역참조 | 컴파일 경고 또는 잘못된 값 | 반드시 per_cpu_ptr() 또는 this_cpu_ptr()로 접근 |
모듈 언로드 시 free_percpu 누락 | Per-CPU 메모리 누수 | cleanup 경로에서 반드시 해제 |
/* ❌ 잘못된 패턴: preemption 없이 this_cpu 사용 */
this_cpu_inc(my_counter); /* BUG: 마이그레이션 가능! */
/* ✅ 올바른 패턴 1: 명시적 preemption 관리 */
preempt_disable();
this_cpu_inc(my_counter);
preempt_enable();
/* ✅ 올바른 패턴 2: get/put 사용 */
get_cpu_var(my_counter)++;
put_cpu_var(my_counter);
/* ❌ 잘못된 패턴: preempt 구간 밖에서 포인터 사용 */
struct my_data *p = this_cpu_ptr(percpu_ptr);
preempt_enable();
p->field = val; /* BUG: 이미 다른 CPU로 이동했을 수 있음 */
/* ✅ 올바른 패턴: 포인터 역참조를 preempt off 내에서 수행 */
preempt_disable();
struct my_data *p = this_cpu_ptr(percpu_ptr);
p->field = val;
preempt_enable();
/* CPU hotplug 안전한 합산 패턴 */
static int percpu_hotplug_dead(unsigned int dead_cpu)
{
struct my_data *dead_stats = per_cpu_ptr(global_stats, dead_cpu);
struct my_data *my_stats = this_cpu_ptr(global_stats);
/* 오프라인된 CPU의 데이터를 현재 CPU로 병합 */
my_stats->count += dead_stats->count;
dead_stats->count = 0;
return 0;
}
/* cpuhp_setup_state로 hotplug 콜백 등록 */
cpuhp_setup_state_nocalls(CPUHP_AP_ONLINE_DYN,
"my_module:online", NULL, percpu_hotplug_dead);
Per-CPU 디버깅과 모니터링
# Per-CPU 할당자 상태 확인
cat /proc/percpu_stats
# Percpu Memory Statistics
# Chunk Info:
# nr_alloc nr_max_alloc free_bytes contig_bytes sum_frag ...
# 152 208 28672 16384 4096
# 전체 Per-CPU 메모리 사용량
grep -i percpu /proc/meminfo
# Percpu: 4352 kB ← 전체 Per-CPU 할당 크기
# CPU별 메모리 사용 상세 (vmstat)
cat /proc/vmstat | grep percpu
# nr_percpu_alloc 152 ← 동적 Per-CPU 할당 횟수
# nr_percpu_free 23 ← 동적 Per-CPU 해제 횟수
# 부팅 로그에서 Per-CPU 초기화 확인
dmesg | grep -i percpu
# percpu: Embedded 45 pages/cpu s143360 r8192 d28672 u524288
# s=정적영역 r=예약영역 d=동적영역 u=단위크기
# percpu: max_distance=0x3fe00000000 too large for vmalloc space 0x0
# percpu: 15132 allocators registered
# Per-CPU 할당 추적 (ftrace)
echo 1 > /sys/kernel/debug/tracing/events/percpu/percpu_alloc_percpu/enable
echo 1 > /sys/kernel/debug/tracing/events/percpu/percpu_free_percpu/enable
cat /sys/kernel/debug/tracing/trace
# my_module-1234 [002] percpu_alloc_percpu: size=64 align=8 ptr=0x...
성능 고려사항: Per-CPU 메모리는 nr_cpu_ids × 크기만큼 소비됩니다. 256 CPU 시스템에서 64바이트 Per-CPU 변수 하나는 16KB를 사용합니다. 대량의 Per-CPU 할당이 필요한 경우, percpu_counter처럼 작은 로컬 카운트 + 중앙 집계 방식을 검토하거나, CPU가 많은 NUMA 시스템에서는 노드 단위 집계를 고려하십시오. 또한 alloc_percpu는 GFP_KERNEL으로 슬립할 수 있으므로 atomic 컨텍스트에서 호출할 수 없습니다.
mempool (메모리 풀)
mempool은 메모리 부족 상황에서도 최소한의 할당을 보장하는 예비 풀입니다. 블록 I/O, 스토리지 드라이버 등 메모리 할당 실패가 허용되지 않는 경로에서 사용합니다.
/* mempool 생성 */
struct kmem_cache *my_cache;
mempool_t *my_pool;
my_cache = kmem_cache_create("my_obj", sizeof(struct my_obj),
0, SLAB_HWCACHE_ALIGN, NULL);
/* 최소 16개 오브젝트를 예비로 확보 */
my_pool = mempool_create_slab_pool(16, my_cache);
if (!my_pool) {
kmem_cache_destroy(my_cache);
return -ENOMEM;
}
/* mempool에서 할당 (GFP_NOIO 경로에서도 안전) */
struct my_obj *obj = mempool_alloc(my_pool, GFP_NOIO);
/* mempool_alloc은 예비 풀이 있으므로 NULL 반환하지 않음 */
/* 해제 → 예비 풀이 부족하면 풀로 반환 */
mempool_free(obj, my_pool);
/* 풀 크기 조정 */
mempool_resize(my_pool, 32); /* 예비 풀을 32개로 확장 */
/* 정리 */
mempool_destroy(my_pool);
kmem_cache_destroy(my_cache);
/* 페이지 기반 mempool */
mempool_t *page_pool = mempool_create_page_pool(8, 0);
/* 최소 8개 order-0 페이지 예비 확보 */
메모리 회수 (Reclaim) 메커니즘
커널이 메모리 부족 시 페이지를 회수하는 과정은 kswapd(백그라운드)와 direct reclaim(동기적)으로 나뉩니다.
# 워터마크 확인
cat /proc/zoneinfo | grep -A 5 "Normal"
# pages free 65432
# min 4096
# low 5120
# high 6144
# 워터마크 조정 (낮출수록 OOM 위험 증가)
sysctl vm.min_free_kbytes=65536 # MIN 워터마크 기준값
sysctl vm.watermark_boost_factor=15000 # 단편화 방지 부스트
sysctl vm.watermark_scale_factor=10 # LOW-HIGH 간격 (0.1% 단위)
# 페이지 회수 통계
cat /proc/vmstat | grep -E "pgscan|pgsteal|pswp|pgfault"
# pgscan_kswapd: kswapd가 스캔한 페이지 수
# pgscan_direct: direct reclaim 스캔 수
# pgsteal_kswapd: kswapd가 회수한 페이지 수
# vmscan 이벤트 추적
echo 1 > /sys/kernel/debug/tracing/events/vmscan/mm_vmscan_direct_reclaim_begin/enable
echo 1 > /sys/kernel/debug/tracing/events/vmscan/mm_vmscan_direct_reclaim_end/enable
cat /sys/kernel/debug/tracing/trace
메모리 할당 주의사항과 흔한 실수
| 실수 유형 | 증상 | 올바른 해결책 |
|---|---|---|
인터럽트에서 GFP_KERNEL 사용 | BUG: scheduling while atomic | GFP_ATOMIC 사용 또는 workqueue로 지연 처리 |
| NULL 반환값 미검사 | NULL pointer dereference Oops | 모든 할당 후 반드시 NULL 체크 |
| kmalloc으로 큰 메모리 할당 | order-too-high 할당 실패 | kvmalloc() 또는 vmalloc() 사용 |
| double free | SLUB 검증 실패, 커널 패닉 | 해제 후 포인터를 NULL로 설정 |
| use-after-free | 데이터 손상, 간헐적 크래시 | KASAN/KFENCE로 탐지, 참조 카운팅 사용 |
| 메모리 누수 | 시간이 지나며 메모리 부족 | kmemleak으로 탐지 (CONFIG_DEBUG_KMEMLEAK) |
__GFP_NOFAIL 남용 | OOM 상황에서 무한 루프 | 에러 경로를 올바르게 구현 |
| slab cache 미해제 | 모듈 언로드 시 메모리 누수 | kmem_cache_destroy() 호출 확인 |
| GFP 플래그 혼합 | 예측 불가능한 동작 | 하나의 기본 플래그 + 수정자 조합 |
| order > MAX_ORDER 할당 | 즉시 실패 | vmalloc 또는 CMA 사용 |
/* 안전한 할당 패턴들 */
/* 1. 구조체 할당 시 sizeof(*ptr) 패턴 */
struct my_data *p = kzalloc(sizeof(*p), GFP_KERNEL);
/* sizeof(struct my_data) 대신 sizeof(*p) → 타입 변경 시 자동 반영 */
/* 2. 배열 할당 시 오버플로 방지 */
/* BAD: kmalloc(n * sizeof(elem)) → 오버플로 가능 */
/* GOOD: */
arr = kcalloc(n, sizeof(*arr), GFP_KERNEL);
/* 또는 flexible array member: */
p = kzalloc(struct_size(p, items, n), GFP_KERNEL);
/* 3. 오류 경로에서 메모리 해제 (goto cleanup 패턴) */
static int my_init(void)
{
a = kmalloc(SIZE_A, GFP_KERNEL);
if (!a) return -ENOMEM;
b = kmalloc(SIZE_B, GFP_KERNEL);
if (!b) goto err_free_a;
c = kmalloc(SIZE_C, GFP_KERNEL);
if (!c) goto err_free_b;
return 0;
err_free_b:
kfree(b);
err_free_a:
kfree(a);
return -ENOMEM;
}
할당자 선택 전략
할당자 선택 결정 트리:
- 크기 < 페이지 크기? →
kmalloc()/kzalloc() - 동일 타입 오브젝트 반복 할당? →
kmem_cache_create() - 크기 > 페이지 크기 + 물리 연속 필요? →
alloc_pages()+ CMA - 크기 > 페이지 크기 + 물리 연속 불필요? →
vmalloc() - 크기 불확정? →
kvmalloc()(kmalloc 시도 후 vmalloc fallback) - DMA 장치 버퍼? →
dma_alloc_coherent() - 할당 실패 불허? →
mempool - Per-CPU 통계/카운터? →
alloc_percpu() - 수명이 짧은 임시 버퍼? → 스택 변수 또는 Per-CPU 버퍼
| 할당자 | 최적 용도 | 오버헤드 | 주의점 |
|---|---|---|---|
kmalloc | 작은 범용 할당 (<8KB) | 최소 | 내부 단편화, order 높으면 실패 |
kmem_cache | 동일 크기 반복 할당 | 최소 | 캐시 해제 누락 주의 |
vmalloc | 큰 비연속 버퍼 | 페이지 테이블 | DMA 불가, TLB 미스 증가 |
kvmalloc | 크기 가변 할당 | 가변 | 해제 시 kvfree 사용 |
alloc_pages | low-level 페이지 제어 | 없음 | 수동 가상 주소 매핑 필요 |
mempool | 할당 보장 필요 | 예비 메모리 | 예비 풀이 메모리 낭비 |
per_cpu | Per-CPU 데이터 | CPU 수 × 크기 | preemption disable 필요 |
CMA | 큰 연속 블록 | 예약 영역 | migratable 페이지만 공존 |
DMA 메모리 매핑 심화
DMA 메모리 존(Zone)과 주소 제한
| Zone | x86_64 범위 | 용도 | 비고 |
|---|---|---|---|
ZONE_DMA |
0 ~ 16MB | ISA DMA (24비트 주소) | 레거시 ISA 디바이스 전용, 현대 시스템에서는 거의 불필요 |
ZONE_DMA32 |
0 ~ 4GB | 32비트 DMA 디바이스 | PCI 디바이스의 기본 DMA 가능 영역 |
ZONE_NORMAL |
4GB ~ 끝 | 일반 커널 메모리 | 64비트 DMA 가능 디바이스만 접근 가능 |
- DMA 주소 마스크에 따른 zone 선택 흐름 */
- dma_set_mask(dev, DMA_BIT_MASK(32))
- → ZONE_DMA32에서 할당 시도
- → IOMMU가 있으면 ZONE_NORMAL에서도 할당 가능 (리매핑)
- dma_set_mask(dev, DMA_BIT_MASK(64))
- → 모든 zone에서 할당 가능
- → SWIOTLB bounce buffer 불필요
- SWIOTLB bounce buffer가 사용되는 조건 */
- IOMMU가 없는 시스템에서 */
- 디바이스의 DMA mask가 물리 메모리 주소를 커버하지 못할 때 */
- 커널이 swiotlb=force 부트 파라미터로 시작했을 때 */
- Bounce buffer 성능 영향 확인 */
- # cat /sys/kernel/debug/swiotlb/io_tlb_used */
- 값이 계속 증가하면 bounce buffer 사용 중 → 64비트 DMA 마스크 설정 필요 */
DMA 캐시 일관성 아키텍처별 차이
| 아키텍처 | DMA coherent 구현 | Streaming DMA sync 비용 | 비고 |
|---|---|---|---|
| x86/x86_64 | HW cache snoop (DMA coherent) | 거의 없음 (noop) | x86은 기본적으로 DMA coherent — sync가 실질적 비용 없음 |
| ARM (non-coherent) | Uncached 매핑 | 캐시 flush/invalidate (비용 큼) | 대부분의 ARM SoC. dma_alloc_coherent는 uncached → CPU 접근 느림 |
| ARM (CCI/CCN) | HW coherency unit | 낮음 | Cache Coherent Interconnect 지원 SoC (고가 서버급) |
| RISC-V | 구현 의존 | 구현 의존 | Sv39/48 + 벤더별 coherency 구현 다양 |
dma_alloc_noncoherent() + 명시적 sync를 사용하여 cached 매핑의 이점을 얻을 수 있습니다.
단, sync 시점을 정확히 관리해야 하므로 코드 복잡도가 증가합니다.
mmap 심화 — 가상 메모리 매핑
mmap() 시스템 콜은 파일이나 익명 메모리를 프로세스 주소 공간에 매핑하는 강력한 메커니즘입니다. 공유 메모리, 메모리 맵 파일 I/O, 디바이스 메모리 접근 등 다양한 용도로 활용됩니다.
상세 문서: VMA/mmap 메커니즘의 심층 분석은 VMA / mmap 심화 페이지에서 확인할 수 있습니다. mmap 시스템 콜 인터페이스, VMA 구조, 페이지 폴트 처리, MAP_SHARED/PRIVATE, mremap/mprotect/madvise, userfaultfd, 프로세스 주소 공간 레이아웃 등 전체 내용을 다룹니다.
주요 개념
- VMA (vm_area_struct): 가상 메모리 영역 관리 자료구조
- MAP_SHARED: 다중 프로세스 간 메모리 공유, 파일 변경 동기화
- MAP_PRIVATE: Copy-on-Write 방식의 프라이빗 매핑
- MAP_ANONYMOUS: 파일 없는 익명 메모리 매핑 (힙 대안)
- 페이지 폴트: lazy allocation, demand paging
- userfaultfd: 유저스페이스에서 페이지 폴트 처리
자세한 내용은 VMA / mmap 심화 페이지를 참고하세요.
역사적 메모리 관련 주요 버그 사례
리눅스 커널의 메모리 관리 서브시스템에서 발견된 주요 버그 사례들을 분석합니다. 이 사례들은 복잡한 메모리 관리 코드에서 발생할 수 있는 미묘한 결함과 그로 인한 보안 영향을 이해하는 데 핵심적인 교훈을 제공합니다.
1. Dirty COW (CVE-2016-5195) — Copy-on-Write 경쟁 조건
madvise(MADV_DONTNEED)와 write() 사이의 race condition을 악용하여
읽기 전용 파일(예: /etc/passwd)에 쓰기가 가능합니다.
근본 원인: get_user_pages()에서 COW(Copy-on-Write) 처리 도중,
write 권한 없이도 dirty 플래그가 설정될 수 있는 경쟁 조건이 존재했습니다.
두 스레드가 동시에 동작할 때 발생하는 TOCTOU(Time-of-Check to Time-of-Use) 문제입니다.
/* Dirty COW 공격 흐름 (개념적 설명) */
/* Thread 1: /proc/self/mem을 통해 읽기 전용 매핑에 쓰기 시도 */
lseek(fd_mem, map_addr, SEEK_SET);
write(fd_mem, payload, payload_len);
/* 내부적으로 get_user_pages(FOLL_WRITE) 호출 */
/* → COW break 발생 → 사본 페이지 할당 */
/* → 사본에 쓰기 수행 */
/* Thread 2: madvise()로 페이지 무효화 */
madvise(map_addr, page_size, MADV_DONTNEED);
/* → COW 사본 폐기 → 원본 페이지로 복귀 */
/* → Thread 1이 원본 페이지에 직접 쓰기하게 됨! */
/* 경쟁 조건 타이밍:
* Thread 1: get_user_pages() → [COW break] → follow_page_mask()
* Thread 2: madvise(MADV_DONTNEED) ← 이 사이에 실행
* Thread 1: → 원본 페이지에 dirty 플래그 설정 → 쓰기 완료
*/
커널 수정 패치 핵심:
/* mm/gup.c - 수정된 COW 처리 */
static inline bool can_follow_write_pte(pte_t pte, unsigned int flags)
{
/* FOLL_COW 플래그 도입: COW break 완료 여부를 명시적으로 확인 */
if (flags & FOLL_COW) {
/* COW break가 실제로 완료되었고, 페이지가 dirty인 경우만 허용 */
return pte_dirty(pte);
}
return pte_write(pte);
}
/* faultin_page()에서 COW 처리 후 FOLL_COW 플래그 설정 */
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags |= FOLL_COW; /* 새로 도입된 플래그 */
get_user_pages()의 FOLL_WRITE → fault → retry 과정에서 페이지 테이블 상태가 변경될 수 있다는 점을
항상 고려해야 합니다. 이 버그는 TOCTOU 패턴의 전형적 사례로, 검증(check)과 사용(use) 사이에
상태가 변경되면 보안 경계가 무너질 수 있음을 보여줍니다.
2. KASAN이 발견한 Use-After-Free 패턴 (CVE-2016-8655)
packet_set_ring()과 packet_setsockopt() 사이의 경쟁 조건으로
해제된 타이머 구조체에 접근하여 로컬 권한 상승이 가능합니다.
Use-After-Free의 일반적 패턴: SLAB allocator에서 object가 해제된 후에도 dangling pointer를 통해 접근하는 경우 발생합니다.
/* Use-After-Free 일반 패턴 */
struct my_object *obj;
/* 1. 할당 */
obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
/* 2. 사용 */
obj->data = 42;
/* 3. 해제 */
kfree(obj);
/* 4. 다른 할당이 같은 메모리를 재사용 */
another = kmem_cache_alloc(my_cache, GFP_KERNEL);
/* 5. Dangling pointer 접근 → Use-After-Free! */
printk("data = %d\\n", obj->data); /* 이미 해제된 메모리 */
CVE-2016-8655 구체적 흐름:
/* net/packet/af_packet.c - 경쟁 조건 */
/* Thread A: packet_set_ring() */
packet_set_ring(sk, &req) {
/* ring buffer 설정 중 타이머 초기화 */
init_prb_bdqc(po, ...); /* retire_blk_timer 초기화 */
}
/* Thread B: 소켓 버전 변경 */
packet_setsockopt(sk, PACKET_VERSION, ...) {
/* ring buffer 해제 */
packet_set_ring(sk, &req_u.req); /* ring 해제 */
/* → retire_blk_timer가 여전히 pending 상태! */
}
/* Timer fires: 해제된 구조체 접근 → Use-After-Free */
prb_retire_rx_blk_timer_expired(...) {
/* 해제된 ring buffer의 메모리에 접근 */
}
KASAN 탐지 메커니즘:
/* KASAN(Kernel Address SANitizer) 동작 원리 */
/* 1. Shadow memory: 8바이트 실제 메모리 당 1바이트 shadow */
/* shadow 값: 0 = 전체 접근 가능, N(1-7) = 처음 N바이트만 접근 가능 */
/* 음수 = 접근 불가 (red zone, freed 등) */
/* 2. kfree() 시 shadow memory를 KASAN_FREE_SHADOW(0xFB)로 마킹 */
static void kasan_poison_slab_free(struct kmem_cache *cache, void *object)
{
kasan_poison_shadow(object, round_up(cache->object_size,
KASAN_SHADOW_SCALE_SIZE), KASAN_KMALLOC_FREE);
}
/* 3. 이후 접근 시 shadow 값 확인 → BUG 리포트 출력 */
/* BUG: KASAN: use-after-free in prb_retire_rx_blk_timer_expired */
/* Read of size 8 at addr ffff8800XXXXXXXX by task swapper/0 */
/* 4. Quarantine: 해제된 object를 즉시 재사용하지 않고 격리 */
/* 일정 크기가 쌓이면 실제로 slab에 반환 */
/* → UAF 탐지 윈도우 확대 */
CONFIG_KASAN=y)하면
이러한 버그를 조기에 탐지할 수 있습니다.
해제 전 타이머(del_timer_sync())와 워크큐(cancel_work_sync())를
반드시 정리하고, RCU 기반 수명 관리로 안전한 해제 시점을 보장하는 것이 핵심입니다.
3. Stack Guard Page 우회 — Stack Clash (CVE-2017-1000364)
alloca()나 VLA(Variable Length Array)로
guard page를 한 번에 넘어 인접 메모리를 덮어쓸 수 있습니다.
Guard Page 우회 원리:
커널 수정 사항:
/* 수정 1: Guard page 크기를 1MB로 확대 */
/* include/linux/mm.h */
#define STACK_GUARD_GAP 256 /* 256 pages = 1MB (4KB pages 기준) */
/* 수정 2: stack_guard_gap 커널 파라미터 도입 */
/* 부트 파라미터로 guard gap 크기 조정 가능 */
/* stack_guard_gap=N (페이지 단위) */
/* 수정 3: expand_stack()에서 guard gap 검증 강화 */
int expand_stack(struct vm_area_struct *vma, unsigned long address)
{
struct vm_area_struct *prev;
/* 인접 VMA와의 거리가 stack_guard_gap 이상인지 확인 */
prev = find_vma_prev(vma->vm_mm, address, &prev);
if (prev && prev->vm_end + stack_guard_gap > address)
return -ENOMEM; /* guard gap 침범 → 확장 거부 */
/* ... */
}
| 항목 | 수정 전 (취약) | 수정 후 (안전) |
|---|---|---|
| Guard Gap 크기 | 4KB (1 page) | 1MB (256 pages, 조정 가능) |
| VMA 확장 검증 | 인접 VMA 거리 미확인 | stack_guard_gap 이상 거리 강제 |
| 커널 파라미터 | 없음 | stack_guard_gap=N |
| rlimit 연동 | RLIMIT_STACK만 확인 | guard gap + RLIMIT_STACK 동시 확인 |
alloca()는 커널 코드에서 사용을 지양하며,
Linux 커널은 -Wvla 컴파일 경고를 활성화하여 VLA 사용을 금지하고 있습니다.
4. Transparent Huge Pages (THP) OOM 문제
khugepaged 커널 스레드가 과도한 CPU 시간을 소모하여
프로덕션 서버 성능이 심각하게 저하되는 문제입니다. 보안 취약점은 아니지만
실제 서비스 장애를 유발한 대표적인 메모리 관리 이슈입니다.
문제 발생 메커니즘:
/* THP compaction 문제 흐름 */
/*
* 1. 프로세스가 페이지 폴트 발생
* → THP가 enabled(always)면 2MB huge page 할당 시도
*
* 2. 연속 2MB 물리 메모리가 없으면 compaction 시작
* → 페이지 이동(migration)으로 연속 공간 확보 시도
*
* 3. 메모리 단편화가 심한 경우:
* → compaction이 반복적으로 실패
* → khugepaged가 CPU 100% 점유
* → 실제 워크로드에 CPU 자원 부족
* → 응답 지연 → OOM killer 발동 가능
*/
/* khugepaged 동작 (mm/khugepaged.c) */
static int khugepaged(void *none)
{
while (!kthread_should_stop()) {
/* 모든 프로세스의 VMA를 스캔하며 huge page 병합 시도 */
khugepaged_do_scan();
/* → collapse_huge_page() → 연속 512개 4KB 페이지를 2MB로 병합 */
/* → 실패 시 재시도 → 단편화 심하면 무한 루프에 가까워짐 */
wait_event_freezable_timeout(khugepaged_wait,
..., msecs_to_jiffies(khugepaged_scan_sleep_millisecs));
}
return 0;
}
영향을 받는 주요 애플리케이션과 해결 방법:
| 애플리케이션 | THP 영향 | 권장 설정 |
|---|---|---|
| Redis | fork() 기반 RDB/AOF 저장 시 COW로 인한 메모리 2배 사용, latency spike | THP 비활성화 필수 |
| MongoDB | WiredTiger 엔진의 메모리 매핑과 충돌, 성능 불안정 | THP 비활성화 권장 |
| Oracle DB | HugePages(명시적)와 THP 혼용 시 예측 불가능한 메모리 사용 | THP 비활성화, 명시적 HugePages 사용 |
| Java (JVM) | GC pause 시간 증가, 메모리 사용량 비예측적 증가 | madvise 모드 또는 비활성화 |
/* THP 제어 방법 */
/* 1. 시스템 전역 설정 */
# 현재 설정 확인
$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
# madvise 모드로 변경 (명시적 요청만 THP 사용)
$ echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
# 완전 비활성화
$ echo never > /sys/kernel/mm/transparent_hugepage/enabled
/* 2. defrag 설정 (compaction 동작 제어) */
$ echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag
/* 3. khugepaged 스캔 간격 조정 */
# 기본값: 10000ms → 더 긴 간격으로 CPU 부하 완화
$ echo 60000 > /sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs
/* 4. 프로세스별 제어 (madvise 모드일 때) */
madvise(addr, length, MADV_HUGEPAGE); /* 이 영역에 THP 사용 */
madvise(addr, length, MADV_NOHUGEPAGE); /* 이 영역에 THP 미사용 */
madvise 모드를 사용하여 애플리케이션이 명시적으로
huge page 사용 여부를 결정하도록 하는 것이 안전한 기본 전략입니다.
버그 사례 비교 요약
| 버그 | 유형 | 잠복 기간 | 영향 | 핵심 교훈 |
|---|---|---|---|---|
| Dirty COW | Race Condition (TOCTOU) | 9년 (2007~2016) | 로컬 권한 상승 | COW 경로의 원자성 보장 필수 |
| AF_PACKET UAF | Use-After-Free | 수개월 | 로컬 권한 상승 | KASAN 활용, 해제 전 타이머/워크큐 정리 |
| Stack Clash | Guard Page 우회 | 수년 | 로컬 권한 상승 | 방어 메커니즘의 크기 가정 재검토 |
| THP OOM | 성능 결함 | 지속적 | 서비스 장애 | 워크로드별 최적화 전략 필요 |
Android 메모리 관리 특화
Android는 메모리 제약이 큰 모바일 환경에서 다수의 앱을 동시에 관리해야 하므로, 커널 메모리 관리에 여러 특화 기법을 적용한다.
ashmem → memfd_create 전환: 초기 Android의 ashmem(Anonymous Shared Memory)은 커널 5.18에서 제거되었으며, 메인라인의 memfd_create()로 대체되었다. memfd_create()는 파일 디스크립터 기반 공유 메모리를 제공하고, F_SEAL_* 플래그로 크기 변경 방지가 가능하다.
lowmemorykiller → lmkd: 커널 내 lowmemorykiller 드라이버는 OOM killer와 충돌 문제로 제거되었으며(4.12+), 유저스페이스 lmkd 데몬이 /proc/pressure/memory(PSI)를 모니터링하여 메모리 부족 시 적절한 프로세스를 종료한다. PSI 기반이므로 CONFIG_PSI=y가 필수다.
MGLRU와 Android Go: MGLRU(Multi-Gen LRU, 커널 6.1+)는 Android 계열 저사양 워크로드에서도 페이지 에이징 정확도와 재클레임 효율 개선에 활용된다. 구체 수치와 기본 활성화 정책은 제품/브랜치별로 달라질 수 있으므로 해당 릴리스 문서를 함께 확인해야 한다. 관련 내용은 Android 커널 — 메모리 관리를 참고하라.
페이지 폴트 핸들러
페이지 폴트는 가상 메모리의 핵심 메커니즘으로, Demand Paging·Copy-On-Write·Swap-in 등 대부분의 메모리 이벤트를 처리합니다. 64비트 시스템에서 프로세스마다 128TB 가상 공간을 사용하지만 실제 RAM은 유한하기 때문에, 필요한 페이지만 물리 메모리에 로드(Demand Paging)하여 효율을 극대화합니다.
페이지 폴트 종류
| 종류 | 발생 원인 | 처리 시간 | 예시 |
|---|---|---|---|
| Minor Fault | 페이지는 RAM에 있지만 매핑 안 됨 | ~1µs | COW, Page Cache hit, 처음 mmap 접근 |
| Major Fault | 디스크/스왑에서 읽어야 함 | ~1ms | mmap 파일 최초 로드, Swap-in |
| Invalid Fault | 잘못된 주소 또는 권한 위반 | N/A | NULL 역참조 → SIGSEGV 발생 |
핸들러 경로 (x86_64)
/* arch/x86/mm/fault.c — CPU 예외 진입점 */
static void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
unsigned long address = read_cr2(); /* CR2 = 폴트 발생 가상 주소 */
if (user_mode(regs))
do_user_addr_fault(regs, error_code, address);
else
do_kern_addr_fault(regs, error_code, address);
}
/* mm/memory.c — 공통 폴트 처리 */
vm_fault_t handle_mm_fault(struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
/* PGD → P4D → PUD → PMD → PTE 페이지 테이블 워크 */
pgd_t *pgd = pgd_offset(vma->vm_mm, address);
pte_t *pte = pte_offset_map(pmd_offset(...), address);
if (!pte_present(*pte))
return do_anonymous_page(vma, address, pte); /* Demand Paging */
else if (flags & FAULT_FLAG_WRITE && !pte_write(*pte))
return do_wp_page(vma, address, pte); /* COW */
return 0;
}
주요 시나리오
① Demand Paging (Anonymous 페이지 최초 접근):
static vm_fault_t do_anonymous_page(struct vm_area_struct *vma,
unsigned long address, pte_t *pte)
{
struct page *page = alloc_zeroed_user_highpage_movable(vma, address);
set_pte_at(vma->vm_mm, address, pte, mk_pte(page, vma->vm_page_prot));
return 0; /* Minor Fault — 디스크 I/O 없음 */
}
② COW (Copy-On-Write — fork 후 쓰기):
static vm_fault_t do_wp_page(struct vm_area_struct *vma,
unsigned long address, pte_t *pte)
{
struct page *old_page = vm_normal_page(vma, address, *pte);
struct page *new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);
copy_user_highpage(new_page, old_page, address, vma); /* 내용 복사 */
set_pte_at(vma->vm_mm, address, pte, mk_pte(new_page, vma->vm_page_prot));
return 0; /* Minor Fault — 공유 해제만 발생 */
}
③ File-backed Page (mmap 파일 최초 접근 → Major Fault):
static vm_fault_t do_read_fault(struct vm_fault *vmf)
{
struct page *page = find_get_page(mapping, offset); /* Page Cache 확인 */
if (!page)
page = __do_fault(vmf); /* Major Fault: 디스크에서 읽기 */
set_pte_at(vmf->vma->vm_mm, vmf->address, vmf->pte,
mk_pte(page, vmf->vma->vm_page_prot));
return 0;
}
모니터링
# 프로세스별 Major/Minor Fault 카운트 (/proc/stat 필드 10, 12)
awk '{print "Minor:", $10, "Major:", $12}' /proc/self/stat
# vmstat: si/so = Major Fault 지표 (Swap In/Out)
vmstat 1
# r b swpd free buff cache si so bi bo in cs
# 2 0 0 123456 78901 234567 0 0 0 0 1234 5678
# perf: 폴트 유형별 카운트
perf stat -e page-faults,minor-faults,major-faults ./myapp
# ftrace: 페이지 폴트 이벤트 추적
echo 1 > /sys/kernel/debug/tracing/events/exceptions/page_fault_user/enable
echo 1 > /sys/kernel/debug/tracing/tracing_on
./myapp
cat /sys/kernel/debug/tracing/trace | head -20
성능 최적화 기법
| 기법 | 설명 | 효과 |
|---|---|---|
| Huge Pages | 2MB/1GB 페이지 사용 | 폴트 수 512× 감소 (TLB 효율↑) |
mlock() |
페이지를 RAM에 고정 (swap-out 방지) | Major Fault 완전 제거 |
madvise(MADV_WILLNEED) |
프리페치 힌트 — readahead 유도 | 접근 전 페이지 미리 로드 |
MAP_POPULATE |
mmap() 시점에 즉시 폴트 처리 |
첫 접근 지연 제거 |
madvise(MADV_HUGEPAGE) |
THP(Transparent Huge Page) 활성화 | 자동 2MB 병합으로 폴트 감소 |
#include <sys/mman.h>
/* mlock: 중요 데이터 구조를 RAM에 고정 */
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
mlock(addr, size); /* RLIMIT_MEMLOCK 이내에서 동작 */
/* MAP_POPULATE: mmap 시점에 모든 페이지를 즉시 폴트 처리 */
void *buf = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
일반적인 문제
Thrashing (Major Fault 폭증): 메모리 부족으로 Swap-in/out이 반복되면 CPU가 대부분 swap I/O 대기에 소비됩니다. vmstat 1에서 si/so 값이 지속적으로 높으면 Thrashing 상태입니다. memory.high throttling이나 earlyoom으로 사전 대응하거나, 메모리를 증설해야 합니다.
vmstat 1
# si so 값이 수천~수만: Thrashing 징후
# 5000 5000 ← 즉각 조치 필요
Segmentation Fault (Invalid Fault): NULL 포인터 역참조나 범위 밖 접근 시 SIGSEGV가 발생합니다. coredumpctl debug 또는 gdb ./myapp core로 폴트 위치를 확인하세요.
고급 메모리 문제 대응 플레이북
심화 메모리 이슈(THP, compaction, reclaim, page fault storm)는 서로 연쇄적으로 영향을 줍니다. 한 기능만 튜닝하지 말고 fault/reclaim/compaction 지표를 동시에 관찰해야 합니다.
| 관찰 축 | 주요 지표 | 해석 |
|---|---|---|
| fault pressure | minor/major fault 비율 | working set 불일치 여부 |
| reclaim pressure | kswapd 활동, si/so | 메모리 압박 지속 여부 |
| compaction | compaction success/fail | 고차 할당 가능성 |
| hugepage | THP fault/collapse 통계 | TLB 최적화 실효성 |
# 고급 메모리 지표 수집
cat /proc/vmstat | grep -E "thp|compact|pgscan|pgsteal|pgfault"
cat /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null || true
vmstat 1 10
관련 문서
메모리 관리 심화와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.