Swapping 서브시스템
Linux 커널 Swapping 메커니즘: swap 공간 설정, swap cache, swap out/in 경로, swappiness 튜닝, zswap/zram 압축 스왑, 성능 모니터링 종합 가이드.
핵심 요약
- Swap 공간 — 물리 메모리 부족 시 익명 페이지를 임시 저장하는 디스크 영역입니다.
- Swap Cache — swap out된 페이지를 RAM에 캐싱하여, 다시 접근 시 디스크 I/O 없이 빠르게 복원합니다.
- kswapd — 백그라운드에서 메모리 부족을 미리 감지하고 페이지를 회수하는 커널 스레드입니다.
- swappiness — 익명 페이지(swap)와 파일 페이지(page cache) 중 어느 쪽을 먼저 회수할지 비율을 조정하는 커널 파라미터입니다.
- zswap / zram — 디스크 대신 압축된 RAM 영역을 swap으로 사용하여 성능을 크게 개선합니다.
단계별 이해
- Swap의 필요성 — 익명 페이지(힙, 스택, mmap anonymous)는 backing store가 없어 swap 없이는 회수 불가능합니다.
파일 페이지는 원본 파일에서 다시 읽을 수 있지만, 익명 페이지는 swap이 유일한 저장소입니다.
- Swap Out/In 과정 — kswapd가 메모리 압력을 감지하면 LRU 리스트에서 오래 사용되지 않은 페이지를 선택하여 디스크에 기록하고, 물리 페이지를 회수합니다.
나중에 해당 페이지에 접근하면 page fault가 발생하고, 커널이 swap 영역에서 다시 읽어옵니다(swap in).
- Swap Cache 최적화 — swap out된 페이지가 아직 RAM에 남아 있으면, 다시 접근 시 디스크 I/O 없이 즉시 복원됩니다.
이를 통해 반복적으로 접근되는 페이지의 swap in 비용을 크게 줄입니다.
- zswap/zram 압축 스왑 — 디스크 대신 압축된 메모리를 사용하여 swap 성능을 10배 이상 개선할 수 있습니다.
zswap은 기존 swap의 캐시 역할, zram은 독립적인 블록 디바이스로 동작하며 모바일/임베디드 환경에서 필수입니다.
Swapping 서브시스템
Swapping은 물리 메모리(RAM) 부족 시 익명 페이지(anonymous page)를 디스크의 스왑 영역으로 내보내고, 다시 접근할 때 복원하는 메커니즘입니다. 파일 기반 페이지는 원본 파일에서 다시 읽을 수 있지만, 힙·스택·mmap(MAP_ANONYMOUS) 등 backing store가 없는 익명 페이지는 스왑 영역이 유일한 저장소입니다.
Swap vs Page Cache 회수: 커널의 메모리 회수(reclaim)는 두 가지 경로로 동작합니다. 파일 페이지(page cache)는 clean이면 즉시 버리고 dirty면 원본 파일에 writeback 후 회수합니다. 익명 페이지는 스왑 영역에 기록(swap out)해야만 회수할 수 있습니다. vm.swappiness로 이 두 경로의 비율을 조정합니다.
Swap 공간 설정
스왑 영역은 전용 파티션 또는 스왑 파일로 구성할 수 있습니다. 여러 스왑 영역을 동시에 사용할 수 있으며, 우선순위(priority)로 사용 순서를 제어합니다.
# === 스왑 파티션 설정 ===
mkswap /dev/sda2 # 파티션을 스왑으로 포맷
swapon /dev/sda2 # 스왑 활성화
swapon -p 10 /dev/sda2 # 우선순위 10으로 활성화
# === 스왑 파일 설정 ===
fallocate -l 4G /swapfile # 4GB 파일 생성
chmod 600 /swapfile # 권한 제한 (필수)
mkswap /swapfile # 스왑 포맷
swapon /swapfile # 활성화
# === /etc/fstab 영구 설정 ===
# /dev/sda2 none swap sw,pri=10 0 0
# /swapfile none swap sw,pri=5 0 0
# === 스왑 상태 확인 ===
swapon --show # 활성 스왑 영역 목록
cat /proc/swaps # 동일 정보 (proc 인터페이스)
free -h # 스왑 사용량 요약
# === 스왑 비활성화 ===
swapoff /dev/sda2 # 스왑 인 후 비활성화 (시간 소요)
swapoff -a # 모든 스왑 비활성화
스왑 파일 주의사항: Btrfs에서 스왑 파일을 사용하려면 chattr +C로 COW를 비활성화하고 별도 서브볼륨에 생성해야 합니다. ext4에서 fallocate 대신 dd를 사용해야 하는 오래된 커널(< 5.0)도 있으므로 주의하십시오. 스왑 파일은 반드시 chmod 600으로 권한을 제한해야 합니다.
우선순위(Priority) 동작 방식: 동일한 우선순위를 가진 스왑 영역들은 라운드 로빈으로 사용되어 I/O가 분산됩니다(RAID-0과 유사). 우선순위가 다르면 높은 우선순위의 영역을 먼저 사용하고, 가득 차면 낮은 우선순위로 넘어갑니다.
# 우선순위 기반 스왑 계층 구성 예시
swapon -p 100 /dev/zram0 # 1순위: zram (압축 메모리, 가장 빠름)
swapon -p 10 /dev/nvme0n1p2 # 2순위: NVMe SSD
swapon -p 1 /swapfile # 3순위: HDD 스왑 파일 (가장 느림)
# 결과 확인
swapon --show
# NAME TYPE SIZE USED PRIO
# /dev/zram0 partition 2G 0B 100
# /dev/nvme0n1p2 partition 8G 0B 10
# /swapfile file 4G 0B 1
Swap 핵심 자료구조
스왑 서브시스템은 세 가지 핵심 자료구조로 구성됩니다.
/* include/linux/swap.h — 스왑 영역 정보 */
struct swap_info_struct {
unsigned long flags; /* SWP_USED | SWP_WRITEOK 등 */
signed short prio; /* 스왑 우선순위 */
struct plist_node list; /* 우선순위 정렬 리스트 */
signed char type; /* 스왑 영역 인덱스 (0~MAX_SWAPFILES-1) */
unsigned int max; /* 최대 스왑 슬롯 수 */
unsigned char *swap_map; /* 슬롯별 참조 카운트 배열 */
struct swap_cluster_info *cluster_info; /* 클러스터별 정보 */
struct swap_cluster_list free_clusters; /* 빈 클러스터 리스트 */
unsigned int lowest_bit; /* 빈 슬롯 탐색 힌트 (시작) */
unsigned int highest_bit; /* 빈 슬롯 탐색 힌트 (끝) */
unsigned int pages; /* 사용 가능 총 페이지 수 */
unsigned int inuse_pages; /* 사용 중인 페이지 수 */
unsigned int cluster_next; /* 다음 할당 위치 힌트 */
unsigned int cluster_nr; /* 현재 클러스터 내 위치 */
struct percpu_cluster __percpu *percpu_cluster; /* Per-CPU 할당 */
struct block_device *bdev; /* 스왑 블록 디바이스 */
struct file *swap_file; /* 스왑 파일 (파일 기반 시) */
unsigned int old_block_size; /* 이전 블록 크기 */
};
/* include/linux/swapops.h — 스왑 엔트리 인코딩 */
/*
* swp_entry_t: PTE에 저장되는 스왑 위치 정보
* 페이지가 스왑 아웃되면, PTE의 present 비트가 0이 되고
* 나머지 비트에 스왑 영역 인덱스(type)와 오프셋(offset)이 인코딩됩니다.
*
* x86_64 레이아웃 (64비트 PTE):
* ┌──────────────────────────────────────────────────────┐
* │ bit 63..58 │ bit 57..5 │ bit 4..1 │ bit 0 │
* │ (unused) │ offset (53 bits) │ type (4b) │ P=0 │
* └──────────────────────────────────────────────────────┘
* P=0이므로 MMU는 page fault 발생 → do_swap_page() 호출
*/
typedef struct {
unsigned long val;
} swp_entry_t;
/* swp_entry_t 조작 매크로/함수 */
swp_type(entry) /* 스왑 영역 인덱스 추출 (0~MAX_SWAPFILES-1) */
swp_offset(entry) /* 스왑 영역 내 슬롯 오프셋 추출 */
swp_entry(type, off) /* type + offset → swp_entry_t 생성 */
/* PTE ↔ swp_entry_t 변환 */
pte_to_swp_entry(pte) /* non-present PTE → swp_entry_t */
swp_entry_to_pte(ent) /* swp_entry_t → non-present PTE */
/* mm/swap_state.c — swap_map: 슬롯별 참조 카운트 */
/*
* swap_map[offset] 값의 의미:
* 0 : 빈 슬롯 (할당 가능)
* 1~SWAP_MAP_MAX : 참조 카운트 (해당 슬롯을 참조하는 PTE 수)
* SWAP_MAP_BAD : 불량 슬롯 (사용 불가)
* SWAP_HAS_CACHE : 스왑 캐시에 존재 (비트 OR)
*
* 참조 카운트가 여러 개인 경우:
* fork() 시 CoW로 공유된 익명 페이지가 스왑 아웃되면
* 부모와 자식 프로세스의 PTE가 동일한 swap entry를 가리킴
*/
#define SWAP_HAS_CACHE 0x40 /* 스왑 캐시에 페이지 존재 */
#define SWAP_MAP_MAX 0x3e /* 최대 참조 카운트 (62) */
#define SWAP_MAP_BAD 0x3f /* 불량 슬롯 표시 */
#define SWAP_MAP_SHMEM 0x20 /* shmem/tmpfs 전용 참조 */
Swap Cache
Swap Cache는 스왑 영역과 메모리 사이의 중간 캐시 계층입니다. 페이지가 스왑 아웃/인될 때 일시적으로 스왑 캐시에 존재하며, 동일 페이지에 대한 중복 I/O를 방지하고 fork된 프로세스 간 일관성을 보장합니다.
/* mm/swap_state.c — Swap Cache 핵심 함수 */
/*
* swapper_spaces[]: 스왑 영역별 address_space 배열
* 각 address_space의 XArray에 swap offset → struct page 매핑 저장
* 일반 파일의 page cache와 동일한 인터페이스(find_get_page 등) 사용
*/
struct address_space *swapper_spaces[MAX_SWAPFILES];
/* 페이지를 Swap Cache에 추가 (swap out 시) */
int add_to_swap_cache(struct page *page, swp_entry_t entry,
gfp_t gfp, void **shadowp)
{
struct address_space *address_space = swap_address_space(entry);
pgoff_t idx = swp_offset(entry);
SetPageSwapCache(page); /* PG_swapcache 플래그 설정 */
set_page_private(page, entry.val); /* page->private에 swap entry 저장 */
/* XArray에 page 삽입 (page cache와 동일한 방식) */
xa_store(&address_space->i_pages, idx, page, gfp);
...
}
/* Swap Cache에서 페이지 검색 (swap in 시) */
struct page *lookup_swap_cache(swp_entry_t entry,
struct vm_area_struct *vma,
unsigned long addr)
{
struct page *page;
page = find_get_page(swap_address_space(entry),
swp_offset(entry));
if (page) {
/* Swap Cache 히트 — 디스크 I/O 없이 즉시 반환 */
mark_page_accessed(page);
}
return page;
}
Swap Cache vs Page Cache: Swap Cache는 사실상 Page Cache의 특수한 형태입니다. 일반 파일 페이지의 page->mapping이 파일의 address_space를 가리키듯, 스왑 캐시 페이지의 page->mapping은 swapper_spaces[]의 address_space를 가리킵니다. /proc/meminfo의 SwapCached 항목이 현재 스왑 캐시 크기를 나타냅니다.
페이지 Swap Out 경로
메모리 회수(reclaim) 과정에서 익명 페이지를 스왑 영역에 기록하는 전체 흐름입니다.
/*
* Swap Out 전체 경로 (간략화):
*
* kswapd / direct_reclaim
* → shrink_node()
* → shrink_lruvec()
* → shrink_list() ← inactive anon LRU 리스트 순회
* → shrink_folio_list()
* → add_to_swap() ← 1. 스왑 슬롯 할당 + 스왑 캐시 등록
* → pageout()
* → swap_writepage() ← 2. 디스크에 기록
* → try_to_unmap() ← 3. 모든 PTE에서 매핑 제거
* → rmap walk
* → try_to_unmap_one() ← PTE를 swap entry로 교체
* → free the page ← 4. 페이지 프레임 해제
*/
/* mm/vmscan.c — add_to_swap(): 스왑 슬롯 할당 핵심 */
bool add_to_swap(struct folio *folio)
{
swp_entry_t entry;
/* 1. 빈 스왑 슬롯 할당 (우선순위 기반) */
entry = folio_alloc_swap(folio);
if (!entry.val)
return false; /* 스왑 공간 부족 */
/* 2. Swap Cache에 등록 */
if (add_to_swap_cache(folio, entry, ...))
return true;
/* 실패 시 슬롯 반환 */
put_swap_folio(folio, entry);
return false;
}
/* mm/page_io.c — swap_writepage(): 실제 디스크 기록 */
int swap_writepage(struct page *page, struct writeback_control *wbc)
{
/* zswap이 활성화되어 있으면 압축 저장 시도 */
if (zswap_store(folio)) {
count_vm_event(ZSWPOUT);
return 0; /* zswap에 저장 성공 → 디스크 I/O 회피 */
}
/* 블록 디바이스에 비동기 기록 */
__swap_writepage(page, wbc);
...
}
/* mm/rmap.c — try_to_unmap_one(): PTE를 swap entry로 교체 */
static bool try_to_unmap_one(struct folio *folio,
struct vm_area_struct *vma, unsigned long address, ...)
{
pte_t pteval;
swp_entry_t entry;
/* 현재 PTE 값 읽기 및 unmap */
pteval = ptep_clear_flush(vma, address, pvmw.pte);
/* page->private에서 swap entry 추출 */
entry = make_readable_migration_entry(page_to_pfn(page));
if (PageSwapCache(page)) {
entry.val = page_private(page); /* swap entry */
}
/* PTE를 swap entry로 교체 (present=0) */
set_pte_at(mm, address, pvmw.pte,
swp_entry_to_pte(entry));
...
}
페이지 Swap In 경로
프로세스가 스왑 아웃된 페이지에 접근하면 page fault가 발생하고, do_swap_page()가 호출되어 페이지를 복원합니다.
/*
* Swap In 전체 경로:
*
* CPU가 PTE 접근 → present=0 → page fault
* → handle_pte_fault()
* → 비어있지 않은 non-present PTE → do_swap_page()
*
* do_swap_page() 내부:
* 1. PTE에서 swp_entry_t 추출
* 2. Swap Cache에서 페이지 검색 (hit이면 I/O 불필요)
* 3. Cache miss → swap_readpage()로 디스크에서 읽기
* 4. 읽은 페이지를 Swap Cache에 등록
* 5. PTE를 유효한 매핑으로 복원 (present=1)
* 6. swap_map 참조 카운트 감소
* 7. 참조 카운트가 0이면 → Swap Cache에서 제거 + 슬롯 해제
*/
/* mm/memory.c — do_swap_page() 핵심 로직 (간략화) */
vm_fault_t do_swap_page(struct vm_fault *vmf)
{
swp_entry_t entry;
struct page *page;
pte_t pte;
/* 1. PTE에서 swap entry 추출 */
entry = pte_to_swp_entry(vmf->orig_pte);
/* 2. Swap Cache 검색 */
page = lookup_swap_cache(entry, vma, vmf->address);
if (!page) {
/* 3. Cache miss → 디스크에서 읽기 */
page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE,
vmf);
if (!page)
return VM_FAULT_OOM;
}
/* 4. 페이지 잠금 및 유효성 검증 */
lock_page(page);
/* 5. PTE를 유효한 매핑으로 복원 */
pte = mk_pte(page, vma->vm_page_prot);
if (vmf->flags & FAULT_FLAG_WRITE)
pte = maybe_mkwrite(pte_mkdirty(pte), vma);
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);
/* 6. swap_map 참조 카운트 감소 */
swap_free(entry);
/* 참조 카운트가 0이면 Swap Cache에서도 제거됨 */
return 0;
}
/* mm/page_io.c — swap_readpage(): 디스크에서 읽기 */
int swap_readpage(struct page *page, bool synchronous,
struct swap_iocb **plug)
{
/* zswap에서 먼저 검색 (압축 저장된 경우) */
if (zswap_load(folio)) {
count_vm_event(ZSWPIN);
SetPageUptodate(page);
return 0; /* zswap에서 복원 성공 */
}
/* 블록 디바이스에서 비동기/동기 읽기 */
submit_bio(bio);
...
}
Swap Readahead
스왑 인 시 인접 페이지를 미리 읽어 성능을 개선합니다. 커널은 두 가지 readahead 전략을 사용합니다.
| 전략 | 방식 | 적합한 상황 | 설정 |
|---|---|---|---|
| 클러스터 readahead | 스왑 영역에서 물리적으로 인접한 슬롯들을 함께 읽기 | 순차 접근 패턴, HDD | /proc/sys/vm/page-cluster (2^n 페이지, 기본=3 → 8페이지) |
| VMA readahead | 가상 주소 공간에서 인접한 스왑 엔트리들을 함께 읽기 | 연속적 가상 메모리 접근, SSD | 자동 (swap in 패턴 분석) |
# Swap readahead 크기 설정
# page-cluster: 2^N 페이지를 한 번에 readahead
cat /proc/sys/vm/page-cluster # 기본값: 3 (2^3 = 8 페이지 = 32KB)
# SSD에서는 줄이는 것이 유리 (랜덤 읽기 비용이 낮음)
echo 0 > /proc/sys/vm/page-cluster # readahead 비활성화
echo 1 > /proc/sys/vm/page-cluster # 2페이지만 readahead
# HDD에서는 높은 값이 유리 (순차 읽기가 빠름)
echo 4 > /proc/sys/vm/page-cluster # 16페이지 readahead
Swappiness 튜닝
vm.swappiness는 커널의 메모리 회수 시 익명 페이지(swap out)와 파일 페이지(page cache 회수)의 상대적 비율을 조절합니다.
| 값 | 동작 | 적합한 워크로드 |
|---|---|---|
0 |
가능한 한 스왑 안 함 (파일 캐시를 우선 회수, 메모리 극히 부족할 때만 스왑) | 데이터베이스, 실시간 시스템 |
10 |
스왑을 최소화하되 필요 시 약간 허용 | 데스크톱, 일반 서버 |
60 |
기본값 — 파일 캐시와 익명 페이지를 균형 있게 회수 | 범용 서버 |
100 |
익명 페이지와 파일 페이지를 동일 비율로 회수 | 대용량 파일 캐시 유지가 중요한 경우 |
200 |
익명 페이지를 적극적으로 스왑 (cgroup v2 전용, 6.1+) | 메모리 오버커밋, 컨테이너 환경 |
# 전역 swappiness 설정
sysctl vm.swappiness=10 # 스왑 최소화
sysctl -w vm.swappiness=60 # 기본값으로 복원
# 영구 설정 (/etc/sysctl.conf)
# vm.swappiness = 10
# cgroup v2: 그룹별 독립 swappiness 설정
echo 0 > /sys/fs/cgroup/mydb/memory.swap.max # 해당 cgroup 스왑 금지
echo 10 > /sys/fs/cgroup/myapp/memory.swappiness # 그룹별 swappiness (커널/배포판별 지원 범위 확인)
/* mm/vmscan.c — swappiness가 reclaim 비율에 미치는 영향 (간략화) */
static void get_scan_count(struct lruvec *lruvec,
struct scan_control *sc,
unsigned long *nr)
{
unsigned long ap, fp; /* anon pressure, file pressure */
unsigned long swappiness = mem_cgroup_swappiness(memcg);
/* swappiness가 0이면 → 메모리 극히 부족할 때만 anon 회수 */
if (!swappiness) {
/* 파일 캐시를 우선 회수, free가 극히 낮으면 anon도 회수 */
fraction[0] = 0; /* anon scan = 0 */
fraction[1] = 1; /* file scan = 전체 */
return;
}
/*
* anon과 file의 스캔 비율 결정:
* ap = swappiness * (최근 anon 참조 빈도의 역수)
* fp = (200 - swappiness) * (최근 file 참조 빈도의 역수)
* → swappiness가 높을수록 anon 스캔 비율 증가
*/
ap = swappiness * (total_cost + 1);
ap /= anon_cost + 1;
fp = (200 - swappiness) * (total_cost + 1);
fp /= file_cost + 1;
fraction[0] = ap; /* anon LRU 스캔 비율 */
fraction[1] = fp; /* file LRU 스캔 비율 */
}
swappiness=0은 스왑 완전 비활성이 아닙니다. vm.swappiness=0으로 설정해도, 시스템 전체 free 메모리가 zone의 high watermark + file cache보다 낮아지면 커널은 여전히 익명 페이지를 스왑 아웃합니다. 스왑을 완전히 금지하려면 swapoff -a로 스왑 영역을 비활성화하거나, cgroup v2에서 memory.swap.max=0으로 설정해야 합니다.
Multi-Gen LRU (MGLRU)
커널 6.1에 도입된 MGLRU는 기존의 active/inactive 2-리스트 LRU를 다중 세대(generation)로 확장하여, 페이지의 접근 빈도를 더 정밀하게 추적합니다. 이를 통해 스왑 아웃/페이지 회수 결정의 정확도가 크게 향상되어, 특히 메모리 부족 시 성능 저하가 줄어듭니다.
# MGLRU 활성화 상태 확인 (CONFIG_LRU_GEN 필요)
cat /sys/kernel/mm/lru_gen/enabled
# 0x0007 = 모든 기능 활성화 (Y+Y+Y)
# 비트 0: lru_gen 코어 활성화
# 비트 1: lru_gen에 의한 reclaim 활성화
# 비트 2: mm_walk(페이지 테이블 스캔)으로 세대 결정 활성화
# MGLRU 활성화/비활성화
echo 7 > /sys/kernel/mm/lru_gen/enabled # 전체 활성화
echo 0 > /sys/kernel/mm/lru_gen/enabled # 비활성화 (기존 LRU로 복귀)
# 세대별 페이지 분포 확인
cat /sys/kernel/mm/lru_gen/memcg_path
# memcg nid gen anon_pages file_pages birth_time
MGLRU 성능 효과: Google의 벤치마크에서 MGLRU는 기존 LRU 대비 메모리 부족 워크로드에서 최대 40%의 성능 향상을 보였습니다. 특히 대규모 서버, Android, ChromeOS 등에서 메모리 압박 시 OOM 발생률이 감소하고, swap thrashing으로 인한 성능 저하가 크게 줄어듭니다.
Swap 모니터링과 디버깅
# === /proc/meminfo — 스왑 관련 항목 ===
cat /proc/meminfo | grep -i swap
# SwapCached: 10240 kB ← Swap Cache 크기 (RAM에 캐시된 스왑 페이지)
# SwapTotal: 8388604 kB ← 전체 스왑 공간
# SwapFree: 7340032 kB ← 미사용 스왑 공간
# === /proc/vmstat — 스왑 I/O 통계 ===
cat /proc/vmstat | grep -E "pswp|swap"
# pswpin 123456 ← 스왑 인 된 총 페이지 수 (누적)
# pswpout 234567 ← 스왑 아웃 된 총 페이지 수 (누적)
# === vmstat 명령으로 실시간 스왑 활동 모니터링 ===
vmstat 1
# procs ---memory--- ---swap-- -----io---- ...
# r b swpd free si so bi bo ...
# 1 0 10240 65432 0 0 12 8 ...
# swpd: 사용 중인 스왑 (KB)
# si: 초당 스왑 인 (KB/s) — 높으면 스왑 thrashing 의심
# so: 초당 스왑 아웃 (KB/s)
# === 프로세스별 스왑 사용량 ===
cat /proc/<pid>/status | grep -i swap
# VmSwap: 1024 kB ← 해당 프로세스의 스왑 사용량
# 스왑 사용량이 큰 프로세스 상위 10개
for f in /proc/[0-9]*/status; do
awk '/^(Name|VmSwap)/{printf "%s ", $2}' "$f" 2>/dev/null
echo
done | sort -k2 -n -r | head -10
# === /proc/<pid>/smaps — 상세 VMA별 스왑 정보 ===
cat /proc/<pid>/smaps | grep -A 20 "heap" | grep Swap
# Swap: 1024 kB ← 해당 VMA의 스왑 사용량
# SwapPss: 512 kB ← PSS 비례 스왑 (공유 시 분할)
# === ftrace로 스왑 이벤트 추적 ===
echo 1 > /sys/kernel/debug/tracing/events/vmscan/mm_vmscan_lru_shrink_inactive/enable
echo 1 > /sys/kernel/debug/tracing/events/swap/swap_readpage/enable
echo 1 > /sys/kernel/debug/tracing/events/swap/swap_writepage/enable
cat /sys/kernel/debug/tracing/trace_pipe
Swap Thrashing 감지: vmstat의 si/so 값이 지속적으로 높으면(수 MB/s 이상) 스왑 thrashing 상태입니다. 이는 물리 메모리가 워크로드에 비해 크게 부족하다는 신호이며, 시스템 전체 성능이 급격히 저하됩니다. 해결 방법: 메모리 증설, 불필요한 프로세스 종료, vm.swappiness 조정, zswap/zram 도입, 또는 cgroup memory.high로 throttling 적용.
Swap 관련 커널 설정 요약
| 설정 | 기본값 | 설명 |
|---|---|---|
CONFIG_SWAP |
y | 스왑 서브시스템 활성화. 비활성 시 익명 페이지 회수 불가 |
CONFIG_SWAP_STATS |
y | 스왑 통계 수집 (/proc/vmstat의 pswpin/pswpout) |
CONFIG_ZSWAP |
y | zswap 압축 스왑 캐시 활성화 |
CONFIG_ZRAM |
m | zram 압축 블록 디바이스 (보통 모듈) |
CONFIG_LRU_GEN |
y (6.1+) | MGLRU 활성화 — 페이지 회수 정확도 향상 |
vm.swappiness |
60 | anon vs file 회수 비율 (0~200, cgroup v2) |
vm.page-cluster |
3 | swap readahead 크기: 2^N 페이지 |
vm.min_free_kbytes |
자동 | MIN watermark 기준 — 스왑/회수 트리거에 간접 영향 |
vm.watermark_boost_factor |
15000 | 단편화 방지 워터마크 부스트 팩터 |
MAX_SWAPFILES |
32 | 동시에 활성화 가능한 최대 스왑 영역 수 |
zswap과 zram
zswap과 zram은 압축을 활용하여 스왑 성능을 크게 개선하는 메커니즘입니다. 디스크 I/O를 줄이고 스왑 영역의 실질적 용량을 확장합니다.
zswap — 압축 스왑 캐시
zswap은 스왑 아웃될 페이지를 압축하여 RAM의 동적 풀에 캐시하는 커널 기능입니다. 실제 디스크/SSD 스왑 I/O가 발생하기 전에 RAM에서 압축 저장을 시도하므로, 디스크 I/O를 극적으로 줄입니다. zswap은 기존 스왑 영역 위에서 동작하는 write-back 캐시이며, 풀이 가득 차면 가장 오래된 페이지를 실제 스왑 영역으로 writeback합니다.
# === zswap 설정 ===
# 런타임 활성화
echo Y > /sys/module/zswap/parameters/enabled
# 압축 알고리즘 선택
echo lz4 > /sys/module/zswap/parameters/compressor
# 선택지: lzo (기본, 균형), lz4 (빠름), zstd (높은 압축률)
# 메모리 풀 할당자 선택
echo z3fold > /sys/module/zswap/parameters/zpool
# zbud: 2:1 압축 비율, 간단하고 예측 가능
# z3fold: 3:1 압축 비율, zbud보다 효율적 (권장)
# zsmalloc: 최고 압축 효율, 약간의 CPU 오버헤드
# 최대 풀 크기 (전체 RAM 대비 퍼센트)
echo 20 > /sys/module/zswap/parameters/max_pool_percent
# same-filled page 최적화 (0으로 채워진 페이지 특수 처리)
echo Y > /sys/module/zswap/parameters/same_filled_pages_enabled
# 커널 부트 파라미터 (권장 설정)
# zswap.enabled=1 zswap.compressor=lz4 zswap.zpool=z3fold zswap.max_pool_percent=25
# === zswap 상태 모니터링 ===
grep -r . /sys/kernel/debug/zswap/ 2>/dev/null
# pool_total_size: 압축 데이터가 차지하는 메모리 (바이트)
# stored_pages: 현재 저장된 페이지 수
# pool_limit_hit: 풀 크기 제한에 도달한 횟수
# reject_reclaim_fail: writeback 실패로 거절된 횟수
# reject_compress_poor: 압축 효율 낮아 거절된 횟수
# written_back_pages: 디스크로 writeback된 페이지 수
# same_filled_pages: same-filled로 최적화된 페이지 수
# 압축 비율 계산
# 원본 크기 = stored_pages × 4096
# 압축 크기 = pool_total_size
# 압축 비율 = 원본 / 압축
zswap 압축 알고리즘 선택 가이드: lz4는 압축/해제 속도가 가장 빨라 CPU 오버헤드가 적으며 대부분의 환경에서 권장됩니다. zstd는 압축률이 높아 메모리 절약이 최우선인 서버에 적합하지만 CPU 사용량이 증가합니다. lzo는 기본값으로 lz4와 유사한 성능을 보입니다. Android에서는 lz4가 표준입니다.
zram — 압축 RAM 블록 디바이스
zram은 RAM의 일부를 압축 블록 디바이스로 만들어 스왑 영역으로 사용하는 모듈입니다. zswap과 달리 독립적인 스왑 디바이스로 동작하며, 실제 디스크 스왑 영역이 없어도 사용할 수 있습니다. 디스크 없는 임베디드 시스템이나 SSD 수명을 보호하려는 환경에서 유용합니다.
# === zram 설정 ===
# 모듈 로드 (디바이스 수 지정)
modprobe zram num_devices=2
# 압축 알고리즘 설정 (디바이스 생성 전에 설정)
echo lz4 > /sys/block/zram0/comp_algorithm
# 지원 알고리즘 확인:
cat /sys/block/zram0/comp_algorithm
# lzo lzo-rle lz4 [lz4hc] zstd (대괄호=현재 선택)
# 디스크 크기 설정 (압축 전 논리적 크기)
echo 4G > /sys/block/zram0/disksize
# 메모리 사용량 제한 (선택사항)
echo 1G > /sys/block/zram0/mem_limit # 실제 RAM 사용 상한
# 스왑으로 활성화
mkswap /dev/zram0
swapon -p 100 /dev/zram0 # 높은 우선순위 (디스크보다 먼저 사용)
# === zram 상태 모니터링 ===
cat /sys/block/zram0/mm_stat
# orig_data_size compr_data_size mem_used_total mem_limit ...
# 4096000000 1024000000 1073741824 1073741824 ...
# ↑ 원본 크기 ↑ 압축 크기 ↑ 실제 메모리 ↑ 메모리 제한
zramctl # zram 디바이스 상태 요약
# NAME ALGORITHM DISKSIZE DATA COMPR TOTAL STREAMS MOUNTPOINT
# /dev/zram0 lz4 4G 1.2G 320M 340M 4 [SWAP]
# === zram 비활성화 및 리셋 ===
swapoff /dev/zram0
echo 1 > /sys/block/zram0/reset # 디바이스 초기화
zswap vs zram 비교
| 특성 | zswap | zram |
|---|---|---|
| 동작 방식 | 기존 스왑 영역의 write-back 캐시 | 독립적인 스왑 블록 디바이스 |
| 디스크 스왑 필요 | 필수 (디스크 스왑 위에서 동작) | 불필요 (RAM만으로 동작 가능) |
| 풀 가득 참 시 | 디스크 스왑으로 writeback | 스왑 공간 부족 처리 (할당 실패) |
| 메모리 풀 | zbud/z3fold/zsmalloc (동적) | 자체 메모리 할당자 (zsmalloc) |
| 투명성 | 완전 투명 (기존 스왑 앞단에 삽입) | 별도 스왑 디바이스로 명시적 설정 |
| 주요 용도 | 디스크 스왑 I/O 감소, 서버 | 디스크 없는 시스템, SSD 보호, Android |
| 병용 가능 | 가능하지만 비권장 — 이중 압축 오버헤드 발생. 보통 하나만 선택 | |
권장 구성: SSD 기반 서버에서는 zswap + SSD 스왑 조합이 효과적입니다 (디스크 I/O 감소 + writeback 안전망). 디스크 없는 임베디드/IoT에서는 zram 단독이 유일한 선택입니다. 데스크톱/Android에서는 zram이 일반적입니다. 대규모 서버 환경에서 메모리 절약이 최우선이면 zswap + zstd 조합을 고려하십시오.
압축 알고리즘 비교
| 알고리즘 | 압축률 | 속도 | CPU 사용 | 권장 환경 |
|---|---|---|---|---|
lz4 |
2.0× | 매우 빠름 | 낮음 | 일반 서버/데스크톱 — CPU 비용 최소화 |
zstd |
2.5× | 빠름 | 중간 | 메모리 절약 우선, 여유 CPU 있을 때 |
lzo |
1.8× | 빠름 | 낮음 | 레거시 커널 (lz4 없을 때) |
lz4hc |
2.3× | 느림 | 높음 | 압축률 우선, CPU 여유 충분한 경우 |
lzo-rle |
1.9× | 빠름 | 낮음 | 희소(sparse) 페이지 많은 환경 |
권장: 대부분의 경우 lz4가 최선입니다. 메모리 절약이 최우선이고 CPU 여유가 있다면 zstd를 고려하세요. ARM/저전력 장치에서는 lzo-rle도 좋은 선택입니다.
성능 벤치마크
| 시나리오 | 일반 스왑 (SSD) | Zswap (lz4) | Zram (lz4) |
|---|---|---|---|
| 읽기 지연 (µs) | 500 | 50 | 30 |
| 쓰기 지연 (µs) | 1,000 | 80 | 60 |
| 처리량 (MB/s) | 500 | 3,000 | 5,000 |
| 메모리 절약 | 없음 | 15~20% (RAM의 20~25%로 제한) | 40~60% (전체 RAM 활용 가능) |
| CPU 오버헤드 | 없음 | 낮음 (~1% 단일 코어) | 낮음 (~1% 단일 코어) |
최적 설정 예시
# === 8GB RAM 서버 — Zswap + SSD 스왑 ===
# /etc/default/grub
GRUB_CMDLINE_LINUX="zswap.enabled=1 zswap.compressor=lz4 zswap.max_pool_percent=25 zswap.zpool=z3fold"
# swappiness (높여도 OK — RAM 내 압축이 먼저)
echo 80 > /proc/sys/vm/swappiness
# === 4GB RAM 임베디드/데스크톱 — Zram 단독 ===
modprobe zram
echo lz4 > /sys/block/zram0/comp_algorithm
echo 2G > /sys/block/zram0/disksize # RAM의 50%
mkswap /dev/zram0
swapon /dev/zram0 -p 10
# swappiness 높여서 Zram 적극 활용
echo 100 > /proc/sys/vm/swappiness
# === systemd-zram-setup (Fedora/Ubuntu 22.04+) ===
# /etc/systemd/zram-generator.conf
# [zram0]
# zram-size = ram / 2
# compression-algorithm = lz4
모니터링
# === Zswap 모니터링 ===
grep -r "" /sys/kernel/debug/zswap/
# pool_total_size 524288000 # 풀 사용 (Bytes)
# stored_pages 128000 # 압축 저장된 페이지 수
# written_back_pages 1024 # 디스크로 writeback된 페이지
# reject_compress_poor 512 # 압축률 불량으로 거부된 페이지
# duplicate_entry 0 # 중복 엔트리
# === Zram 모니터링 ===
zramctl
# NAME ALGORITHM DISKSIZE DATA COMPR TOTAL STREAMS MOUNTPOINT
# /dev/zram0 lz4 4G 1.2G 400M 450M 4 [SWAP]
cat /sys/block/zram0/mm_stat
# orig_data_size compr_data_size mem_used_total ...
# 1073741824 357564928 367001600
# 압축률 = orig / compr = 3.0x
# /proc/meminfo 관련 항목
grep -E "Swap|Zswap" /proc/meminfo
# SwapTotal: 4194304 kB
# SwapFree: 3145728 kB
# SwapCached: 8192 kB
알려진 문제
- CPU 오버헤드: 압축/해제는 CPU 사이클을 소모합니다. CPU가 이미 포화 상태라면 지연이 더 커질 수 있습니다.
lz4선택 및 CPU 사용률 모니터링으로 완화 가능합니다. - 비압축성 데이터: 이미 압축된 데이터(JPEG·MP4·암호화 파일 등)는 압축률이 1.0배 미만으로,
reject_compress_poor카운터가 증가하며 곧장 디스크 스왑으로 넘어갑니다. - Zswap 풀 고갈:
max_pool_percent에 도달하면 기존 항목을 디스크 스왑으로 writeback합니다. SSD가 느리다면 일시적으로 성능이 떨어질 수 있습니다. - Zram + Zswap 병용: 이중 압축 오버헤드가 발생하므로 비권장입니다. 하나만 선택하십시오.
- 메모리 부족 시 Zram OOM: Zram만 사용 중이고 압축 풀이 가득 찬 경우 백업 스왑이 없어 OOM Killer가 동작할 수 있습니다. 중요 프로세스에는
oom_score_adj = -1000을 설정하십시오.
Swap 튜닝 플레이북
Swap 성능 문제는 저장장치 속도, 압축 정책, 워크로드 working set 크기가 동시에 영향을 줍니다. 단순 swappiness 변경보다 먼저 병목 위치를 분리하세요.
| 상황 | 우선 점검 | 권장 조치 |
|---|---|---|
| 응답 지연 급증 | vmstat si/so, iowait | zswap/zram 적용, hot set 보호 |
| CPU 과부하 | 압축 알고리즘 비용 | lz4/zstd 압축 정책 재검토 |
| 지속적 swap-out | 메모리 과할당 | 메모리 상한 조정, workload 분리 |
# swap 상태 핵심 지표
free -h
vmstat 1 10
grep -E "Swap|Zswap" /proc/meminfo
cat /sys/module/zswap/parameters/enabled 2>/dev/null || true
Swap 클러스터 할당 심화
실제 swap I/O 성능은 빈 슬롯을 "어떻게" 찾고 묶어서 쓰는지에 크게 좌우됩니다. 최신 커널은 단일 비트 탐색보다 클러스터 단위 할당과 Per-CPU 힌트를 활용해 락 경합과 탐색 비용을 줄입니다.
/* mm/swapfile.c - 클러스터 기반 할당 개념 (간략화) */
#define SWAPFILE_CLUSTER 256 /* 256 페이지 단위 (아키텍처/설정에 따라 다를 수 있음) */
struct swap_cluster_info {
unsigned int flags;
unsigned int count; /* 사용 중 슬롯 수 */
};
struct percpu_cluster {
unsigned int index; /* 현재 CPU가 소비 중인 클러스터 */
unsigned int next; /* 다음 슬롯 힌트 */
};
/* 핵심 흐름: 클러스터 우선, 실패 시 글로벌 탐색 */
swp_entry_t get_swap_page(struct folio *folio)
{
swp_entry_t entry;
/* 1) 현재 CPU 클러스터에서 빠른 할당 시도 */
entry = scan_swap_map_try_ssd_cluster(si, cpu);
if (entry.val)
return entry;
/* 2) free_clusters 리스트에서 새 클러스터 획득 */
entry = alloc_swap_scan_cluster(si);
if (entry.val)
return entry;
/* 3) 최후: lowest_bit~highest_bit 선형 탐색 */
return scan_swap_map_slots(si);
}
cgroup v2 Swap 제어 심화
현대 운영 환경에서 swap 정책은 전역 `vm.swappiness`보다 cgroup 단위 제어가 더 중요합니다. 컨테이너별 상한과 보호 정책으로 swap thrashing 전파를 차단할 수 있습니다.
| 파일 | 의미 | 실전 활용 |
|---|---|---|
memory.swap.max | 해당 cgroup swap 사용 상한 | 0이면 swap 금지, 장애 전파 차단 |
memory.high | 소프트 상한 (초과 시 reclaim/스로틀) | OOM 전에 완충 구간 형성 |
memory.max | 하드 상한 | 초과 시 강제 회수/킬 |
memory.events | high/max/oom 이벤트 카운터 | 자동 스케일링/알람 트리거 |
memory.pressure | PSI 지표 | stall 기반 조기 경보 |
# cgroup v2 기준 예시
CG=/sys/fs/cgroup/workloads/api
mkdir -p $CG
# 메모리 8GB, swap 2GB 상한
echo $((8*1024*1024*1024)) > $CG/memory.max
echo $((2*1024*1024*1024)) > $CG/memory.swap.max
# high를 먼저 설정해 스로틀+완충
echo $((7*1024*1024*1024)) > $CG/memory.high
# 이벤트/압력 모니터링
watch -n 1 "cat $CG/memory.events; echo; cat $CG/memory.pressure"
memory.swap.max=0, 배치/비동기 워크로드는 제한된 swap 허용이 안정적입니다. 같은 호스트에서 두 유형을 분리하지 않으면 swap 지연이 서비스 전반으로 전파됩니다.
Swap 지연시간 분해
swap fault 한 번의 비용은 단일 값이 아닙니다. lock 대기, 압축 해제, 블록 I/O, 페이지 테이블 복원 비용이 합산됩니다. 병목 구간을 분리 측정해야 튜닝이 정확해집니다.
swap-subsystem과 zswap 페이지 중복 검토
두 문서를 비교하면 아래 주제가 중복됩니다. 중복 자체는 탐색 편의에는 유리하지만, 심화 관점에서는 역할 분리가 더 명확해야 합니다.
- 중복 영역 — zswap/zram 비교표, 압축 알고리즘 소개, 기본 모니터링 명령
- 이 페이지의 책임 — 전체 swap 정책, reclaim/swap out/in 경로, swappiness/cgroup/PSI 기반 운영
- zswap 페이지의 책임 — zswap 내부 구조(RB-tree, zpool), reject 원인, writeback/shrinker, 디버깅
실무에서는 먼저 이 페이지에서 시스템 정책을 고정하고, zswap 세부 튜닝은 zswap 심화 페이지에서 별도로 최적화하는 순서가 가장 안정적입니다.
| 주제 | 이 페이지 (swap-subsystem) | zswap 페이지 | 중복 처리 원칙 |
|---|---|---|---|
| 회수 정책 | 주관: LRU/kswapd/direct reclaim, swappiness, PSI | 참조만 유지 | 정책 결정은 이 페이지에서 1차 고정 |
| zswap 내부 | 개요만 유지 | 주관: zpool, reject, writeback, shrinker | 내부 구조 설명은 zswap으로 단일화 |
| 튜닝 순서 | 주관: 시스템 한계선과 보호 규칙 | 주관: 파라미터 미세 조정 | 정책 이후 파라미터 조정 순서 고정 |
| 모니터링 지표 | PSI, vmstat, cgroup 이벤트 | zswap debugfs, reject/writeback 지표 | 대시보드 계층 분리(시스템/기능) |
| 장애 대응 | 주관: 시스템 스로틀/격리 | 주관: zswap 병목 원인 분기 | 공통 증상이라도 진단 관문을 분리 |
vmscan과 kswapd 내부 동작
kswapd는 각 NUMA 노드마다 하나씩 존재하는 커널 스레드로, 백그라운드에서 메모리 워터마크를 모니터링하며 페이지 회수를 수행합니다. vmscan 경로의 핵심 함수들을 정확히 이해해야 swap 정책의 실질적 동작을 파악할 수 있습니다.
kswapd 깨우기 조건
페이지 할당자(__alloc_pages())가 zone의 free pages가 low watermark 아래로 떨어진 것을 감지하면 wakeup_kswapd()를 호출합니다. kswapd는 free pages가 high watermark에 도달할 때까지 회수를 계속한 뒤 다시 sleep합니다.
/* mm/vmscan.c - kswapd main loop (간략화) */
static int kswapd(void *p)
{
struct pglist_data *pgdat = (struct pglist_data *)p;
for ( ; ; ) {
/* 워터마크 충족 시 sleep */
prepare_kswapd_sleep(pgdat);
schedule();
/* 깨어나면 balance_pgdat() 실행 */
balance_pgdat(pgdat, order, highest_zoneidx);
}
}
balance_pgdat() 메인 루프
balance_pgdat()는 scan priority를 DEF_PRIORITY(12)에서 시작하여 0까지 감소시키며 점점 더 공격적으로 페이지를 스캔합니다. priority가 낮을수록 스캔 범위가 넓어지며, LRU 리스트의 더 많은 비율을 검사합니다.
/* mm/vmscan.c - balance_pgdat 핵심 흐름 */
#define DEF_PRIORITY 12 /* 초기 스캔 우선순위 */
static int balance_pgdat(struct pglist_data *pgdat,
int order, int highest_zoneidx)
{
int priority;
struct scan_control sc = {
.gfp_mask = GFP_KERNEL,
.order = order,
.may_unmap = 1,
.may_swap = 1,
};
for (priority = DEF_PRIORITY; priority >= 0; priority--) {
sc.priority = priority;
/* 각 zone을 순회하며 shrink_node() 호출 */
kswapd_shrink_node(pgdat, &sc);
/* 워터마크 도달 시 조기 종료 */
if (kswapd_watermark_ok(pgdat, highest_zoneidx))
break;
}
}
lru_pages >> priority입니다. priority 12는 전체의 1/4096, priority 0은 전체 LRU를 스캔합니다. 이 설계로 경미한 메모리 압력에서는 최소 비용으로 회수하고, 심각한 상황에서만 전체 탐색을 수행합니다.
shrink_node() 내부
shrink_node()는 노드 내 모든 memcg를 순회하며, 각 memcg의 LRU 리스트에서 페이지를 회수합니다. 파일 페이지와 익명 페이지의 회수 비율은 swappiness 값에 의해 결정됩니다.
/* mm/vmscan.c - shrink_node 핵심 경로 */
static void shrink_node(struct pglist_data *pgdat,
struct scan_control *sc)
{
struct mem_cgroup *memcg;
/* 각 memcg를 순회 */
memcg = mem_cgroup_iter(NULL, NULL, NULL);
do {
/* LRU 리스트에서 페이지 회수 */
shrink_node_memcgs(pgdat, sc);
/* file vs anon 비율 = f(swappiness) */
get_scan_count(lruvec, sc, nr);
} while ((memcg = mem_cgroup_iter(NULL, memcg, NULL)));
}
| 함수 | 위치 | 역할 |
|---|---|---|
kswapd() | mm/vmscan.c | 메인 루프: sleep → balance_pgdat → sleep |
balance_pgdat() | mm/vmscan.c | priority 감소 루프, 노드 단위 회수 조율 |
shrink_node() | mm/vmscan.c | 노드 내 memcg 순회, LRU 리스트 회수 |
get_scan_count() | mm/vmscan.c | swappiness 기반 file/anon 스캔 비율 결정 |
shrink_lruvec() | mm/vmscan.c | 실제 LRU 페이지 isolate → reclaim |
shrink_folio_list() | mm/vmscan.c | 개별 folio의 writeback/swap/free 결정 |
get_scan_count()와 swappiness의 관계
get_scan_count()는 swappiness 값을 입력으로 file 페이지와 anon 페이지의 스캔 비율을 결정합니다. 이 함수가 swap 정책의 핵심입니다.
/* mm/vmscan.c - get_scan_count 핵심 로직 (간략화) */
static void get_scan_count(struct lruvec *lruvec,
struct scan_control *sc,
unsigned long *nr)
{
unsigned long ap, fp; /* anon/file pressure */
unsigned long swappiness = mem_cgroup_swappiness(memcg);
/* swappiness를 anon/file 비율로 변환 */
ap = swappiness; /* anon pressure */
fp = 200 - swappiness; /* file pressure */
/* 각 LRU의 refault rate도 고려 */
ap *= lruvec_page_state(lruvec, NR_INACTIVE_ANON);
fp *= lruvec_page_state(lruvec, NR_INACTIVE_FILE);
/* 최종 스캔 수: priority로 조절 */
nr[LRU_INACTIVE_ANON] = ap >> sc->priority;
nr[LRU_INACTIVE_FILE] = fp >> sc->priority;
}
| swappiness | anon:file 비율 | 적합한 워크로드 |
|---|---|---|
| 0 | 0:200 (파일만 회수) | swap 완전 차단 (OOM 위험 증가) |
| 10 | 10:190 | 데이터베이스 (anon 보호 강조) |
| 60 (기본) | 60:140 | 범용 서버 |
| 100 | 100:100 (동등) | zram 사용 시 권장 |
| 200 | 200:0 (anon만 회수) | 특수: file cache 최우선 보호 |
kswapd 워터마크 트리거 다이어그램
MGLRU (Multi-Gen LRU)와 vmscan
커널 6.1+에서 도입된 MGLRU는 기존 active/inactive 2단 LRU를 세대(generation) 기반으로 확장합니다. 이를 통해 vmscan의 페이지 에이징 정확도가 크게 향상됩니다.
/* mm/vmscan.c - MGLRU 핵심 구조 (간략화) */
struct lru_gen_folio {
unsigned long max_seq; /* 최신 세대 번호 */
unsigned long min_seq[ANON_AND_FILE]; /* 가장 오래된 세대 */
struct list_head folios[MAX_NR_GENS][ANON_AND_FILE][MAX_NR_ZONES];
};
/* 에이징: 접근되지 않은 folio는 다음 세대로 강등 */
static void inc_max_seq(struct lruvec *lruvec)
{
/* PTE의 accessed bit 확인 후 세대 갱신 */
walk_pmd_range(...); /* 페이지 테이블 워킹 */
/* 접근된 folio → 최신 세대, 미접근 → 유지 */
}
| 항목 | 기존 LRU | MGLRU |
|---|---|---|
| 에이징 정확도 | 2단계 (active/inactive) | 4+ 세대 (configurable) |
| 스캔 방식 | LRU 끝에서 역방향 | 페이지 테이블 워킹 기반 |
| CPU 오버헤드 | 높음 (불필요한 스캔) | 낮음 (targeted walk) |
| working set 보호 | refault distance 기반 | 세대 기반 자연 보호 |
# MGLRU 활성화 상태 확인
cat /sys/kernel/mm/lru_gen/enabled
# 출력: 0x0007 (모든 기능 활성화)
# MGLRU 통계
cat /sys/kernel/debug/lru_gen
# 워터마크 확인
grep -E "^(Node| pages free| min| low| high)" /proc/zoneinfo
# 현재 min_free_kbytes
sysctl vm.min_free_kbytes
# kswapd 활동 모니터링
vmstat 1 | awk 'NR==1||NR==2||$7>0||$8>0'
scan_control 주요 플래그
struct scan_control은 vmscan 경로의 동작을 제어하는 플래그 모음입니다. 호출 경로에 따라 다른 플래그가 설정됩니다.
| 필드 | 기본값 | 의미 |
|---|---|---|
may_writepage | 1 (kswapd), 0 (direct) | dirty page writeback 허용 여부 |
may_unmap | 1 | 매핑된 페이지 unmap 허용 여부 |
may_swap | 1 | swap out 허용 여부 |
gfp_mask | GFP_KERNEL | 할당 플래그 (회수 범위 결정) |
priority | 12 → 0 | 스캔 범위 (낮을수록 공격적) |
nr_to_reclaim | SWAP_CLUSTER_MAX (32) | 목표 회수 페이지 수 |
kswapd 문제 진단 체크리스트
| 증상 | 확인 명령 | 원인 | 대응 |
|---|---|---|---|
| kswapd CPU 100% | top -p $(pgrep kswapd) | 회수 불가 페이지가 LRU 점유 | mlocked/unevictable 페이지 점검 |
| scan priority 0 도달 | ftrace: vmscan/mm_vmscan_lru_shrink_inactive | 극심한 메모리 압력 | 워크로드 축소, 메모리 증설 |
| kswapd 깨어나지 않음 | /proc/zoneinfo free vs watermark | 워터마크 설정 오류 | vm.min_free_kbytes 재설정 |
| NUMA 불균형 회수 | numastat -m | 특정 노드만 압력 | vm.zone_reclaim_mode 조정 |
페이지 회수 경로 (Direct Reclaim vs kswapd vs kcompactd)
리눅스 커널에서 메모리가 부족할 때 페이지를 회수하는 경로는 크게 세 가지입니다. 각 경로의 트리거 조건과 성능 영향을 정확히 이해해야 메모리 압력 상황에서의 시스템 동작을 예측할 수 있습니다.
Direct Reclaim 경로
프로세스가 __alloc_pages()에서 메모리 할당을 요청했으나 free pages가 MIN watermark 아래인 경우, 할당자가 직접 페이지 회수를 수행합니다. 이 경로는 프로세스를 차단하므로 사용자 관점에서 가장 심각한 지연을 유발합니다.
/* mm/page_alloc.c - direct reclaim 진입 경로 (간략화) */
static struct page *__alloc_pages_slowpath(gfp_t gfp, unsigned int order,
struct alloc_context *ac)
{
/* 1단계: kswapd 깨우기 */
if (gfp_kswapd_allowed(gfp))
wake_all_kswapds(order, gfp, ac);
/* 2단계: 워터마크 재확인 후 direct reclaim */
page = __perform_reclaim(gfp, order, ac);
/* 3단계: 여전히 실패 시 OOM killer 호출 */
if (!page)
page = __alloc_pages_may_oom(gfp, order, ac);
return page;
}
kswapd 백그라운드 회수
kswapd는 앞 섹션에서 설명한 대로 LOW watermark 이하에서 깨어나 HIGH watermark까지 비동기적으로 페이지를 회수합니다. 정상적인 시스템에서는 대부분의 회수가 이 경로에서 처리됩니다.
| 경로 | 트리거 | 차단 여부 | 성능 영향 |
|---|---|---|---|
| kswapd | free < LOW | 비차단 (백그라운드) | 정상 — 사용자 지연 없음 |
| Direct Reclaim | free < MIN | 차단 | 심각 — 프로세스 stall |
| kcompactd | high-order 할당 실패 | 비차단 (백그라운드) | 경미 — 연속 메모리 확보 |
| OOM Killer | 모든 회수 실패 | 프로세스 종료 | 치명적 — 서비스 중단 |
워터마크 계산 공식
MIN/LOW/HIGH 워터마크는 vm.min_free_kbytes를 기준으로 자동 계산됩니다. 각 zone의 크기에 비례하여 분배됩니다.
/* mm/page_alloc.c - 워터마크 계산 (간략화) */
/* MIN = vm.min_free_kbytes를 zone 크기 비례 분배 */
/* LOW = MIN + MIN/4 (기본, vm.watermark_scale_factor로 조정) */
/* HIGH = MIN + MIN/2 (기본, vm.watermark_scale_factor로 조정) */
/* watermark_scale_factor 사용 시 (기본 10 = 0.1%) */
/* LOW = MIN + (zone_managed_pages * scale_factor / 10000) */
/* HIGH = MIN + (zone_managed_pages * scale_factor / 10000) * 3/2 */
# 워터마크 상세 확인 (zone별)
awk '/Node.*zone/{zone=$0} /pages free|min |low |high /{print zone, $0}' /proc/zoneinfo
# watermark_scale_factor 조정 (대형 메모리 서버에서 유용)
# 기본 10 (0.1%) → 150 (1.5%)로 상향하면 kswapd가 더 일찍 깨어남
sysctl -w vm.watermark_scale_factor=150
# 워터마크 boost (일시적으로 HIGH를 올려 kswapd 회수량 증가)
sysctl -w vm.watermark_boost_factor=15000
| 파라미터 | 기본값 | 영향 | 조정 가이드 |
|---|---|---|---|
vm.min_free_kbytes | 자동 (RAM 기반) | MIN 워터마크 직접 결정 | 64GB RAM → ~180MB, 너무 높이면 낭비 |
vm.watermark_scale_factor | 10 (0.1%) | LOW-MIN, HIGH-MIN 간격 | 150~500: direct reclaim 빈도 감소 |
vm.watermark_boost_factor | 15000 | fragmentation 시 HIGH 임시 상향 | 0: boost 비활성화 |
kcompactd와 메모리 압축
kcompactd는 high-order(연속 물리 페이지) 할당 요청이 반복 실패할 때 깨어나 메모리 압축(compaction)을 수행합니다. 이는 페이지 회수와는 다른 개념으로, 기존 페이지를 물리적으로 재배치하여 연속 공간을 만듭니다.
# direct reclaim 발생 횟수 확인
grep "pgsteal_direct" /proc/vmstat
# kswapd 회수 횟수
grep "pgsteal_kswapd" /proc/vmstat
# compaction 이벤트
grep "compact_" /proc/vmstat | head -8
# 프로세스별 direct reclaim 지연
grep -E "allocstall_(movable|normal|dma)" /proc/vmstat
zone_watermark_ok() 판정 로직
/* mm/page_alloc.c - 워터마크 판정 (간략화) */
bool zone_watermark_ok(struct zone *z, int order,
unsigned long mark, int highest_zoneidx,
unsigned int alloc_flags)
{
long free_pages = zone_page_state(z, NR_FREE_PAGES);
/* 예약 페이지를 감안한 실제 사용 가능 페이지 */
free_pages -= (1 << order) - 1;
/* lowmem_reserve 보정 */
for (i = highest_zoneidx; i >= 0; i--)
free_pages -= z->lowmem_reserve[i];
return free_pages > mark;
}
페이지 회수 결정 플로차트
pgsteal_kswapd가 pgsteal_direct보다 압도적으로 많은 것이 건강한 상태입니다. direct reclaim이 빈번하면 vm.min_free_kbytes를 높이거나 메모리 과할당을 줄여야 합니다.
Reclaim 경로 ftrace 추적
vmscan 관련 tracepoint를 활용하면 실시간으로 페이지 회수 경로를 추적할 수 있습니다.
# vmscan tracepoint 활성화
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
echo 1 > /sys/kernel/debug/tracing/events/vmscan/mm_vmscan_kswapd_wake/enable
echo 1 > /sys/kernel/debug/tracing/events/vmscan/mm_vmscan_lru_shrink_inactive/enable
# 추적 결과 확인
cat /sys/kernel/debug/tracing/trace_pipe | head -50
# 또는 perf로 집계
perf stat -e 'vmscan:*' -a -- sleep 10
# bpftrace로 direct reclaim 지연 히스토그램
bpftrace -e '
tracepoint:vmscan:mm_vmscan_direct_reclaim_begin { @start[tid] = nsecs; }
tracepoint:vmscan:mm_vmscan_direct_reclaim_end /@start[tid]/ {
@usecs = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
Reclaim 경로별 지연 특성
| 경로 | 평균 지연 | 최악 지연 | 지표 |
|---|---|---|---|
| kswapd (clean page) | < 1 μs | ~10 μs | pgsteal_kswapd |
| kswapd (dirty writeback) | ~50 μs (SSD) | ~5 ms (HDD) | pgsteal_kswapd + pageoutrun |
| Direct reclaim (clean) | ~10 μs | ~100 μs | pgsteal_direct |
| Direct reclaim (dirty) | ~1 ms (SSD) | ~50 ms (HDD) | pgsteal_direct + allocstall_* |
| kcompactd | ~100 μs | ~10 ms | compact_success |
Writeback과 Reclaim의 상호작용
dirty page를 회수하려면 먼저 디스크에 기록(writeback)해야 합니다. kswapd는 dirty page를 만나면 writeback을 시작하고, writeback 완료 후에야 페이지를 회수할 수 있습니다. 이 때문에 dirty ratio가 높은 시스템에서는 회수 효율이 크게 떨어집니다.
/* mm/vmscan.c - dirty page 회수 결정 (간략화) */
static unsigned int shrink_folio_list(...)
{
if (folio_test_dirty(folio)) {
/* kswapd: writeback 시작 후 다음 스캔에서 회수 */
if (current_is_kswapd()) {
folio_start_writeback(folio);
mapping_writeback(mapping, folio);
goto keep_locked; /* 이번에는 회수 안 함 */
}
/* direct reclaim: 가능하면 skip (성능) */
if (sc->may_writepage)
folio_start_writeback(folio);
else
goto keep_locked;
}
}
vm.dirty_ratio와 vm.dirty_background_ratio를 낮추면 dirty page 비율이 줄어 회수 효율이 개선됩니다. 특히 swap이 빈번한 환경에서는 dirty_background_ratio=5, dirty_ratio=15 정도로 제한하는 것을 권장합니다.
Throttle과 공정성 제어
커널 5.x+에서는 direct reclaim이 과도하게 발생할 때 reclaim_throttle()로 태스크를 일시 대기시켜 kswapd에게 회수 기회를 양보합니다. 이는 direct reclaim 폭주(thundering herd)를 방지합니다.
/* mm/vmscan.c - reclaim throttle (간략화) */
static void reclaim_throttle(struct pglist_data *pgdat,
enum vmscan_throttle_state reason)
{
/* VMSCAN_THROTTLE_WRITEBACK: dirty page writeback 대기 */
/* VMSCAN_THROTTLE_ISOLATED: isolation 실패 시 대기 */
/* VMSCAN_THROTTLE_NOPROGRESS: 회수 진전 없음 시 대기 */
wait_event_interruptible_timeout(
pgdat->reclaim_wait[reason],
kswapd_watermark_ok(pgdat),
HZ/10); /* 최대 100ms 대기 */
}
PSI (Pressure Stall Information) 메모리
PSI는 커널 4.20+에서 도입된 메모리 압력 측정 프레임워크입니다. 기존의 vmstat 지표보다 정확한 "실제 작업이 지연된 시간 비율"을 제공하며, 사전 대응적 메모리 관리의 핵심 도구입니다.
/proc/pressure/memory 형식
# PSI 메모리 지표 읽기
cat /proc/pressure/memory
# 출력 예시:
# some avg10=0.42 avg60=1.20 avg300=0.85 total=328842
# full avg10=0.08 avg60=0.35 avg300=0.22 total=87523
| 필드 | 의미 | 임계값 가이드 |
|---|---|---|
some | 하나 이상의 태스크가 메모리 대기로 stall된 시간 비율 | > 10% → 경고 |
full | 모든 non-idle 태스크가 동시에 stall된 시간 비율 | > 5% → 심각 |
avg10 | 최근 10초 이동 평균 (%) | 즉각 반응 지표 |
avg60 | 최근 60초 이동 평균 (%) | 단기 추세 |
avg300 | 최근 300초 이동 평균 (%) | 장기 추세 |
total | 누적 stall 시간 (μs) | 절대량 비교용 |
PSI 트리거 설정
PSI 트리거를 사용하면 메모리 압력이 특정 임계값을 초과할 때 poll()/epoll()로 알림을 받을 수 있습니다. 이는 사전 대응적 메모리 관리의 핵심입니다.
# PSI 트리거 설정: some 150ms/1s 초과 시 알림
# (사용자 공간에서 fd에 write)
echo "some 150000 1000000" > /proc/pressure/memory
# 이후 poll(fd, ...)로 대기 → 임계값 초과 시 이벤트 수신
/* PSI 트리거를 활용한 사전 대응 예제 (간략화) */
int fd = open("/proc/pressure/memory", O_RDWR | O_NONBLOCK);
/* "some 150000 1000000" = some ≥ 150ms/1s 일 때 트리거 */
write(fd, "some 150000 1000000", 20);
struct pollfd fds = { .fd = fd, .events = POLLPRI };
while (1) {
int n = poll(&fds, 1, -1);
if (n > 0) {
/* 메모리 압력 감지 → 캐시 드롭, 프로세스 중지 등 대응 */
handle_memory_pressure();
}
}
cgroup PSI: memory.pressure
cgroup v2에서는 그룹별 PSI를 /sys/fs/cgroup/<group>/memory.pressure에서 읽을 수 있습니다. 이를 통해 컨테이너 단위의 메모리 압력을 개별 모니터링할 수 있습니다.
# 특정 cgroup의 메모리 PSI 확인
cat /sys/fs/cgroup/myapp/memory.pressure
# some avg10=5.23 avg60=3.40 avg300=1.89 total=1284523
# full avg10=1.02 avg60=0.78 avg300=0.45 total=342185
# cgroup PSI 트리거 설정
echo "some 100000 1000000" > /sys/fs/cgroup/myapp/memory.pressure
PSI 기반 사전 대응 전략
| PSI 상태 | 조건 | 권장 대응 |
|---|---|---|
| 정상 | some avg10 < 1% | 모니터링 유지 |
| 경고 | some avg10 1~10% | 비필수 캐시 드롭, swap 정책 검토 |
| 위험 | some avg10 > 10% 또는 full avg10 > 2% | 워크로드 축소, 메모리 한도 상향 |
| 위기 | full avg10 > 10% | 즉각 부하 분산, OOM 사전 방지 |
lmkd(Low Memory Killer Daemon)는 PSI 트리거를 핵심 입력으로 사용하여, 메모리 압력에 따라 백그라운드 앱을 사전적으로 종료합니다. 기존 커널 OOM killer보다 훨씬 세밀한 제어가 가능합니다.
PSI 내부 구현: task state 추적
PSI는 각 CPU에서 태스크 상태 변화를 추적하여 stall 시간을 계산합니다. 메모리 관련 stall은 direct reclaim, swap-in, thrashing(refault) 대기가 포함됩니다.
/* kernel/sched/psi.c - PSI 상태 추적 (간략화) */
enum psi_task_count {
NR_IOWAIT, /* I/O 대기 */
NR_MEMSTALL, /* 메모리 stall (reclaim, swap-in, thrashing) */
NR_RUNNING, /* 실행 가능 */
NR_MEMSTALL_RUNNING,/* stall 중이지만 CPU 사용 가능 (some) */
};
/* 태스크 상태 전환 시 호출 */
void psi_task_switch(struct task_struct *prev,
struct task_struct *next, bool sleep)
{
/* prev의 stall 상태 해제, next의 상태 갱신 */
psi_group_change(prev->psi_group, cpu, clear, set);
}
| stall 유형 | PSI 카운터 | 발생 경로 |
|---|---|---|
| Direct Reclaim | NR_MEMSTALL | __perform_reclaim() → shrink_node() |
| Swap-in I/O | NR_MEMSTALL | do_swap_page() → swap_readpage() |
| Thrashing (refault) | NR_MEMSTALL | workingset_refault() |
| THP split | NR_MEMSTALL | split_huge_page() (간접) |
PSI vs 기존 모니터링 방식 비교
| 방식 | 지표 | 장점 | 한계 |
|---|---|---|---|
| vmstat si/so | swap in/out 초당 블록 수 | 간단, 오래된 커널 지원 | stall 영향 크기 모름 |
| /proc/meminfo | SwapTotal, SwapFree 등 | 현재 상태 확인 | 추세/속도 정보 없음 |
| cgroup memory.events | low, high, max, oom | cgroup 단위 이벤트 | 시간 비율 정보 없음 |
| PSI memory | some/full, avg10/60/300 | 실제 지연 비율, 트리거 지원 | 커널 4.20+ 필요 |
PSI 모니터링 실전 스크립트
# PSI 연속 모니터링 (1초 간격)
while true; do
echo "=== $(date +%H:%M:%S) ==="
cat /proc/pressure/memory
echo "---"
grep -E "pswpin|pswpout|pgsteal|allocstall" /proc/vmstat | head -6
sleep 1
done
# PSI와 swap 지표를 조합한 원라이너
paste <(awk '/some/{print "PSI_some="$2}' /proc/pressure/memory) \
<(awk '/pswpin/{print "swin="$2} /pswpout/{print "swout="$2}' /proc/vmstat)
# systemd 서비스에서 PSI 기반 보호
# /etc/systemd/system/myapp.service
# [Service]
# MemoryPressureWatch=yes
# MemoryPressureThresholdSec=200ms
Swap Readahead 최적화
Swap readahead는 swap-in 시 인접 슬롯도 함께 읽어 후속 page fault 비용을 줄이는 최적화입니다. 커널은 두 가지 readahead 전략을 접근 패턴에 따라 동적으로 선택합니다.
Cluster Readahead
스왑 디바이스 상에서 물리적으로 인접한 슬롯을 함께 읽는 방식입니다. 순차적 swap-out으로 인해 인접 슬롯이 함께 swap-in될 가능성이 높을 때 효과적입니다.
/* mm/swap_state.c - cluster readahead (간략화) */
struct page *swap_cluster_readahead(swp_entry_t entry,
gfp_t gfp, struct vm_fault *vmf)
{
unsigned long offset = swp_offset(entry);
unsigned int nr_pages = 1 << page_cluster; /* 기본 8 pages */
/* offset을 클러스터 경계로 정렬 */
unsigned long start = offset & ~(nr_pages - 1);
unsigned long end = start + nr_pages;
/* 인접 슬롯을 일괄 읽기 */
for (i = start; i < end; i++)
__read_swap_cache_async(swp_entry(type, i), gfp, ...);
}
VMA-based Readahead
프로세스의 가상 주소 공간(VMA)에서 인접한 페이지를 함께 읽는 방식입니다. 동일 VMA 내 가까운 가상 주소의 페이지가 함께 swap-in될 가능성이 높을 때 사용됩니다.
/* mm/swap_state.c - VMA readahead (간략화) */
struct page *swap_vma_readahead(swp_entry_t entry,
gfp_t gfp, struct vm_fault *vmf)
{
/* 현재 fault 주소 근처 PTE를 순회 */
unsigned long start = max(vmf->address - window, vma->vm_start);
unsigned long end = min(vmf->address + window, vma->vm_end);
/* swap entry가 있는 PTE만 readahead */
for (addr = start; addr < end; addr += PAGE_SIZE) {
pte = pte_offset_map(pmd, addr);
if (is_swap_pte(*pte))
__read_swap_cache_async(pte_to_swp_entry(*pte), gfp, ...);
}
}
swap_readahead_detect() 알고리즘
커널은 swap_readahead_detect()에서 최근 swap-in 패턴을 분석하여 cluster 또는 VMA readahead를 자동 선택합니다. 순차적 패턴이 감지되면 cluster를, 아니면 VMA를 사용합니다.
Readahead 효과 측정
readahead가 효과적인지 측정하려면 swap-in 횟수 대비 실제 사용(major fault)의 비율을 확인해야 합니다.
# readahead 효율 측정
# 1) 전체 swap-in 횟수
awk '/pswpin/{print "swap-in pages:", $2}' /proc/vmstat
# 2) major fault (실제 필요한 swap-in)
awk '/pgmajfault/{print "major faults:", $2}' /proc/vmstat
# 비율: major_fault/pswpin 이 1에 가까우면 readahead 효과 없음
# major_fault/pswpin 이 0.1~0.3이면 readahead가 효과적
# perf로 swap readahead 경로 프로파일링
perf record -g -e 'vmscan:mm_vmscan_lru_shrink_inactive' -- sleep 30
perf report --sort=symbol
# bpftrace로 readahead 크기 히스토그램
bpftrace -e '
kprobe:swap_cluster_readahead {
@cluster = count();
}
kprobe:swap_vma_readahead {
@vma = count();
}
interval:s:10 { print(@cluster); print(@vma); clear(@cluster); clear(@vma); }'
page_cluster sysctl 튜닝
| 파라미터 | 기본값 | 의미 | 튜닝 가이드 |
|---|---|---|---|
vm.page-cluster | 3 | readahead 크기 = 2^N 페이지 | SSD: 2~3, HDD: 3~4, 랜덤: 0 |
# 현재 page_cluster 값 확인 (기본 3 = 8 pages = 32KB)
sysctl vm.page-cluster
# SSD에서 readahead 축소 (4 pages = 16KB)
sysctl -w vm.page-cluster=2
# 완전 랜덤 워크로드: readahead 비활성화
sysctl -w vm.page-cluster=0
# swap readahead 효과 확인
grep "pswpin\|pswpout" /proc/vmstat
page-cluster=0으로 불필요한 readahead를 차단하면 swap 대역폭이 개선됩니다. 반대로 순차 스캔 워크로드에서는 기본값(3) 이상이 유리합니다.
Readahead 전략 선택 알고리즘
swap_readahead_detect()는 per-CPU 카운터를 사용하여 최근 swap-in 패턴을 분석합니다. 연속된 swap offset 접근이 감지되면 cluster readahead를, 그렇지 않으면 VMA readahead를 선택합니다.
/* mm/swap_state.c - readahead 전략 선택 (간략화) */
static struct page *swap_readahead_detect(struct vm_fault *vmf,
struct vma_swap_readahead *ra_info)
{
struct swap_ra_info *ra = this_cpu_ptr(&swap_ra);
/* 최근 접근 패턴 분석 */
if (swap_offset(entry) == ra->last_offset + 1) {
ra->sequential++;
/* 연속 접근 → cluster readahead */
return swap_cluster_readahead(entry, gfp, vmf);
}
ra->sequential = 0;
/* 비연속 접근 → VMA readahead */
return swap_vma_readahead(entry, gfp, vmf);
}
| 전략 | 적합한 워크로드 | 장점 | 단점 |
|---|---|---|---|
| Cluster Readahead | 순차 메모리 접근, 대량 데이터 처리 | 디스크 순차 I/O 최적화 | 랜덤 접근 시 불필요한 I/O |
| VMA Readahead | 랜덤 접근, 다수 프로세스 | 실제 사용될 페이지 예측 | PTE 순회 오버헤드 |
| Disabled (page-cluster=0) | 극단적 랜덤, 데이터베이스 | 불필요한 I/O 완전 제거 | 순차 접근 시 비효율 |
SWP_SYNCHRONOUS_IO와 readahead
zram처럼 SWP_SYNCHRONOUS_IO 플래그가 설정된 디바이스에서는 readahead가 비활성화됩니다. 동기 I/O 디바이스는 지연이 매우 작으므로 readahead 오버헤드가 이득보다 큽니다.
/* mm/memory.c - SWP_SYNCHRONOUS_IO 경로 */
if (data_race(si->flags & SWP_SYNCHRONOUS_IO) &&
__swap_count(entry) == 1) {
/* 빠른 경로: readahead 없이 직접 읽기 */
/* swap cache에도 추가하지 않음 (바이패스) */
folio = vma_alloc_folio(gfp, 0, vma, vmf->address);
swap_read_folio(folio, true); /* synchronous */
}
Swap Entry 내부 구조
스왑 아웃된 페이지의 위치는 swp_entry_t 값으로 PTE(페이지 테이블 엔트리)에 저장됩니다. 이 값은 swap 디바이스 번호(type)와 디바이스 내 오프셋(offset)을 단일 정수에 인코딩합니다.
swp_entry_t 인코딩
/* include/linux/swapops.h */
typedef struct {
unsigned long val;
} swp_entry_t;
/* 비트 레이아웃 (x86_64 기준) */
/* bit 0 : present = 0 (not present → swap entry) */
/* bit 1-5 : swap type (최대 32개 디바이스) */
/* bit 6-63 : swap offset (디바이스 내 슬롯 번호) */
#define SWP_TYPE_SHIFT 1
#define SWP_OFFSET_SHIFT (SWP_TYPE_SHIFT + 5) /* = 6 */
#define MAX_SWAPFILES (1 << 5) /* = 32 */
static inline swp_entry_t swp_entry(unsigned long type,
unsigned long offset)
{
swp_entry_t ret;
ret.val = (type << SWP_TYPE_SHIFT) | (offset << SWP_OFFSET_SHIFT);
return ret;
}
static inline unsigned swp_type(swp_entry_t entry)
{
return (entry.val >> SWP_TYPE_SHIFT) & 0x1f;
}
static inline pgoff_t swp_offset(swp_entry_t entry)
{
return entry.val >> SWP_OFFSET_SHIFT;
}
Swap Entry 비트 레이아웃 다이어그램
swap_info_struct 핵심 필드
| 필드 | 타입 | 역할 |
|---|---|---|
swap_map | unsigned char * | 슬롯별 참조 카운트 (0=free, 1+=사용중) |
cluster_info | struct swap_cluster_info * | 클러스터별 free/사용 상태 |
swap_file | struct file * | swap 파일/파티션의 file 객체 |
bdev | struct block_device * | 블록 디바이스 참조 |
pages | unsigned long | 총 슬롯 수 |
inuse_pages | unsigned int | 현재 사용 중인 슬롯 수 |
prio | int | swap 우선순위 (높을수록 먼저 사용) |
flags | unsigned long | SWP_USED, SWP_WRITEOK, SWP_DISCARDABLE 등 |
Swap Extent Tree
swap 파일이 디스크에서 연속적이지 않을 수 있으므로, 커널은 swap_extent 구조로 논리 오프셋 → 물리 블록 매핑을 관리합니다. swap 파티션은 1:1 매핑이므로 단일 extent만 필요하지만, 조각난 swap 파일은 다수의 extent가 생성됩니다.
swapon() 시 커널은 swap 파일의 모든 extent를 스캔하여 red-black tree를 구성합니다. swap I/O 시 논리 오프셋을 이 트리에서 검색하여 물리 블록 주소를 결정합니다.
/* include/linux/swap.h */
struct swap_extent {
struct rb_node rb_node; /* red-black tree 노드 */
pgoff_t start_page; /* 시작 swap 오프셋 */
pgoff_t nr_pages; /* 연속 페이지 수 */
sector_t start_block; /* 디스크 시작 섹터 */
};
filefrag -v /swapfile로 extent 수를 확인하세요. extent가 수백 개 이상이면 성능이 저하됩니다. swap 파일 생성 시 fallocate로 연속 할당하거나, swap 파티션 사용을 권장합니다.
swap_map 참조 카운팅
swap_map[] 배열은 각 swap 슬롯의 참조 카운트를 관리합니다. fork()에 의한 COW(Copy-on-Write) 페이지가 같은 swap 슬롯을 공유할 수 있으므로, 정확한 참조 카운팅이 필수적입니다.
/* mm/swapfile.c - swap_map 참조 카운트 값 */
#define SWAP_HAS_CACHE 0x40 /* swap cache에 존재 */
#define SWAP_MAP_MAX 0x3e /* 최대 참조 카운트 (62) */
#define SWAP_MAP_BAD 0x3f /* 불량 슬롯 표시 */
#define SWAP_MAP_SHMEM 0x80 /* shmem/tmpfs 페이지 */
/* swap 슬롯 참조 획득 */
int swap_duplicate(swp_entry_t entry)
{
struct swap_info_struct *si = swp_swap_info(entry);
unsigned long offset = swp_offset(entry);
unsigned char count;
count = si->swap_map[offset];
if (count < SWAP_MAP_MAX)
si->swap_map[offset] = count + 1;
return 0;
}
/* swap 슬롯 참조 해제 */
struct swap_info_struct *swap_entry_free(struct swap_info_struct *si,
swp_entry_t entry)
{
unsigned long offset = swp_offset(entry);
unsigned char usage = si->swap_map[offset];
if (usage == 1) {
/* 마지막 참조: 슬롯 해제, free_clusters에 반환 */
si->swap_map[offset] = 0;
inc_cluster_info_page(si, offset);
} else {
si->swap_map[offset] = usage - 1;
}
}
| swap_map 값 | 의미 | 발생 상황 |
|---|---|---|
| 0 | 미사용 (free slot) | 한 번도 할당 안 됨 or 모든 참조 해제 |
| 1 | 단일 참조 | 일반적인 swap-out 페이지 |
| 2~62 | 다중 참조 | fork() COW 공유, shmem 공유 |
| 0x3f (BAD) | 불량 슬롯 | 디스크 I/O 오류 발생 슬롯 |
| 0x40 (HAS_CACHE) | swap cache 존재 | swap cache에 아직 보관 중 |
| 0x80 (SHMEM) | shmem 페이지 | tmpfs/shmem에서 swap-out된 페이지 |
Swap 슬롯 DISCARD (TRIM) 지원
SSD에서 swap 슬롯이 해제되면 DISCARD 명령으로 SSD에 알려 가비지 컬렉션을 최적화할 수 있습니다. 커널은 두 가지 DISCARD 모드를 지원합니다.
# swap DISCARD 활성화 (swapon 시)
swapon --discard /dev/nvme0n1p2
# 또는 fstab에서
# /dev/nvme0n1p2 none swap sw,discard 0 0
# discard 세분화: 단일/클러스터 단위
# --discard=once → swapon 시 전체 DISCARD (한 번)
# --discard=pages → 슬롯 해제 시마다 DISCARD (실시간)
swapon --discard=pages /dev/nvme0n1p2
--discard=pages)는 각 swap free마다 DISCARD 명령을 발행하므로 오버헤드가 있습니다. 고빈도 swap 환경에서는 --discard=once로 제한하거나, 주기적인 fstrim으로 대체하는 것을 고려하세요.
다중 디바이스 Swap 전략
리눅스 커널은 최대 32개의 swap 디바이스를 동시에 사용할 수 있습니다. 우선순위 기반 분배와 동일 우선순위 라운드 로빈을 통해 다양한 저장 계층을 효율적으로 활용할 수 있습니다.
Priority 기반 분배
높은 우선순위의 swap 디바이스가 가득 찰 때까지 먼저 사용되며, 가득 차면 다음 우선순위로 내려갑니다. 같은 우선순위의 디바이스 간에는 라운드 로빈으로 분배됩니다.
# 다중 swap 설정 예시
# zram (최고 우선순위: 메모리 압축, 가장 빠름)
swapon -p 100 /dev/zram0
# NVMe SSD (중간 우선순위)
swapon -p 50 /dev/nvme0n1p2
# HDD (최저 우선순위: 최후의 수단)
swapon -p 10 /dev/sda2
# 현재 swap 상태 확인
swapon --show=NAME,TYPE,SIZE,USED,PRIO
cat /proc/swaps
| 디바이스 유형 | 권장 우선순위 | 용도 | 지연시간 |
|---|---|---|---|
| zram | 100 (최고) | 압축 swap, 1차 흡수 | ~1 μs |
| NVMe SSD | 50 | 고속 swap, 2차 흡수 | ~50 μs |
| SATA SSD | 30 | 중속 swap | ~200 μs |
| HDD | 10 (최저) | 대용량 swap, 최후의 수단 | ~5 ms |
| NFS/NBD | 5 | 네트워크 swap (특수 용도) | ~1~100 ms |
라운드 로빈 동작
/* mm/swapfile.c - 동일 우선순위 라운드 로빈 (개념) */
/* 같은 priority의 디바이스가 여러 개일 때: */
/* plist에서 순서대로 각 디바이스에 클러스터 단위 할당 */
/* avail_lists[node] → NUMA 노드별 로컬 디바이스 우선 */
NFS/NBD Swap 고려사항
네트워크 기반 swap은 특수한 환경(디스크리스 부팅, 씬 클라이언트 등)에서 사용됩니다. 커널은 swap 경로에서 메모리 할당이 필요할 때 교착(deadlock)을 방지하기 위해 특별한 메모리 예약 메커니즘을 사용합니다.
/* net/core/sock.c - swap over NFS 메모리 예약 */
/* PF_MEMALLOC: 네트워크 스택이 emergency reserves 사용 가능 */
/* sk_set_memalloc(): swap I/O를 위한 소켓에 플래그 설정 */
/* 이 없으면: swap-in → 메모리 필요 → reclaim → swap-in 교착 */
SWP_SYNCHRONOUS_IO 플래그 미지원으로 성능도 불리합니다.
| 항목 | 로컬 디바이스 Swap | NFS/NBD Swap |
|---|---|---|
| 지연시간 | ~50 μs (SSD) | ~1-100 ms (네트워크 의존) |
| 신뢰성 | 높음 | 네트워크 장애 시 hang 위험 |
| SWP_SYNCHRONOUS_IO | 지원 (zram, brd) | 미지원 |
| readahead | cluster + VMA | VMA 주로 |
| 메모리 예약 | 불필요 | PF_MEMALLOC 필수 |
| 교착 방지 | 단순 | 복잡 (네트워크 스택 내 예약) |
파일 vs 파티션 성능 비교
| 항목 | Swap 파티션 | Swap 파일 |
|---|---|---|
| 성능 | 최적 (1:1 매핑) | 약간 불리 (extent 탐색 오버헤드) |
| 크기 조정 | 파티션 리사이즈 필요 | swapoff → resize → swapon 유연 |
| 조각화 | 해당 없음 | 가능 (fallocate로 최소화) |
| TRIM/DISCARD | 직접 지원 | 파일시스템 의존 |
| 설정 복잡도 | 파티션 생성 필요 | 파일 생성만으로 가능 |
/etc/fstab 기반 다중 swap 구성
# /etc/fstab 예시: 다중 swap 영구 설정
# 디바이스 마운트 타입 옵션 덤프 패스
# /dev/zram0 none swap sw,pri=100 0 0
# /dev/nvme0n1p2 none swap sw,discard,pri=50 0 0
# /swapfile none swap sw,pri=10 0 0
# systemd 기반 zram 자동 설정 (/etc/systemd/zram-generator.conf)
# [zram0]
# zram-size = ram / 4
# compression-algorithm = lz4
# swap-priority = 100
zram + 디스크 하이브리드 구성
# zram + SSD 하이브리드 swap 구성 예시
# 1) zram: RAM의 25%를 압축 swap으로 (최우선)
modprobe zram num_devices=1
echo lz4 > /sys/block/zram0/comp_algorithm
echo $(($(free -b | awk '/Mem:/{print $2}') / 4)) > /sys/block/zram0/disksize
mkswap /dev/zram0
swapon -p 100 /dev/zram0
# 2) SSD swap 파일: overflow 대비
fallocate -l 4G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon -p 10 /swapfile
# 검증
swapon --show
# NAME TYPE SIZE USED PRIO
# /dev/zram0 partition 3.9G 0B 100
# /swapfile file 4G 0B 10
NUMA 노드별 Swap 배치
NUMA 시스템에서는 swap 디바이스의 물리적 위치가 성능에 영향을 줍니다. 커널은 avail_lists[node]를 통해 각 NUMA 노드에 로컬 swap 디바이스를 우선 할당합니다.
# NUMA 토폴로지 확인
numactl --hardware
# available: 2 nodes (0-1)
# node 0: 0-15 cpus, 64GB
# node 1: 16-31 cpus, 64GB
# 노드별 swap 사용량 확인
numastat -m | grep -i swap
# NUMA 노드에 로컬 swap 할당 권장
# Node 0에 연결된 NVMe → 높은 priority
# Node 1에 연결된 NVMe → 높은 priority
# cross-node swap access = 추가 ~50ns 지연
swap_avail_heads 우선순위 리스트
커널은 활성 swap 디바이스를 priority 순서대로 정렬된 plist(priority list)로 관리합니다. get_swap_pages() 호출 시 이 리스트를 순회하며 가장 높은 우선순위 디바이스에서 먼저 슬롯을 할당합니다.
/* mm/swapfile.c - swap 디바이스 우선순위 관리 */
static PLIST_HEAD(swap_avail_heads);
/* swapon 시 우선순위에 따라 리스트에 삽입 */
static void _enable_swap_info(struct swap_info_struct *si)
{
si->flags |= SWP_WRITEOK;
/* priority 순서대로 정렬 삽입 */
plist_add(&si->avail_lists[nid], &swap_avail_heads);
}
/* 할당 시 순회 */
plist_for_each_entry_safe(si, next, &swap_avail_heads, avail_lists[node]) {
entry = scan_swap_map_slots(si, ...);
if (entry.val)
break; /* 높은 우선순위에서 성공 */
}
Swap 디바이스 핫 추가/제거
# 런타임 swap 추가
fallocate -l 2G /tmp/emergency-swap
chmod 600 /tmp/emergency-swap
mkswap /tmp/emergency-swap
swapon -p 5 /tmp/emergency-swap # 낮은 우선순위로 긴급 추가
# 안전한 swap 제거 (사용 중인 페이지를 다른 swap/RAM으로 이동)
swapoff /tmp/emergency-swap # 주의: 사용량이 크면 오래 걸림
# swapoff 진행 상황 모니터링
while swapon --show | grep emergency; do
swapon --show=NAME,USED
sleep 1
done
THP (Transparent Huge Pages) Swap
THP(Transparent Huge Pages)는 2MB 크기의 대형 페이지를 자동으로 사용하여 TLB 효율을 높이는 기능입니다. THP의 swap 동작은 커널 버전에 따라 크게 달라지며, 성능에 중요한 영향을 미칩니다.
THP Swap의 진화
| 커널 버전 | 동작 | 성능 영향 |
|---|---|---|
| ~4.x | swap 전 반드시 split → 512개 4KB 페이지로 개별 swap | split 비용 + 512회 I/O |
5.0+ (CONFIG_THP_SWAP) | 2MB 단위로 통째로 swap out 가능 | 단일 I/O, split 비용 제거 |
| 6.x+ (folio 기반) | folio 단위 swap, 다양한 크기 지원 | 유연한 대형 페이지 swap |
CONFIG_THP_SWAP 동작
/* mm/huge_memory.c - THP swap out (간략화) */
bool can_split_folio(struct folio *folio,
int *pextra_pins)
{
/* THP swap 지원 시 split 불필요 */
#ifdef CONFIG_THP_SWAP
if (folio_test_anon(folio) && folio_test_large(folio))
return true; /* 통째로 swap 가능 */
#endif
return false;
}
/* swap 슬롯 할당: THP는 연속 512개 슬롯 필요 */
swp_entry_t folio_alloc_swap(struct folio *folio)
{
int nr = folio_nr_pages(folio);
/* nr=512일 때 연속 512개 슬롯을 클러스터에서 할당 */
return get_swap_pages(nr);
}
THP Split-on-Swap 비용
THP split은 다음 작업을 수반합니다:
- 페이지 테이블 분할: PMD 엔트리 하나를 512개 PTE로 전환
- rmap 업데이트: reverse mapping을 512개로 분리
- LRU 재등록: 대형 folio를 512개 base 페이지로 개별 등록
- TLB flush: 모든 CPU에 TLB shootdown IPI 발생
# THP swap 관련 통계
grep -E "thp_swpout|thp_split" /proc/vmstat
# thp_swpout ← THP 통째로 swap out 횟수
# thp_swpout_fallback ← split 후 swap 횟수 (실패 지표)
# THP 현재 상태
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never
CONFIG_THP_SWAP=y (최근 커널 기본값)로 THP split을 피하는 것이 유리합니다. 다만 swap 공간이 부족하면 연속 512개 슬롯 할당 실패로 인한 fallback split이 발생할 수 있으므로, swap 용량에 여유를 두세요.
THP와 Swap 슬롯 할당
THP를 통째로 swap-out하려면 2MB = 512개의 연속 swap 슬롯이 필요합니다. swap 디바이스가 단편화되면 연속 슬롯 확보에 실패하여 fallback split이 발생합니다. 이를 최소화하려면 swap 공간에 여유를 유지해야 합니다.
# THP swap 실패 횟수 모니터링
grep thp_swpout_fallback /proc/vmstat
# thp_swpout_fallback이 증가 → swap 공간 부족 또는 단편화
# swap 사용률 확인 (70% 이상이면 THP swap 실패 급증)
swapon --show=NAME,SIZE,USED,PRIO
# 권장: swap 사용률 50% 이하 유지
THP Swap-in 경로
THP로 swap-out된 페이지를 다시 읽어올 때도 가능하면 통째로 swap-in하여 THP 상태를 유지합니다.
/* mm/memory.c - THP swap-in (간략화) */
static vm_fault_t do_swap_page(struct vm_fault *vmf)
{
swp_entry_t entry = pte_to_swp_entry(vmf->orig_pte);
struct folio *folio;
/* 1) swap cache에서 folio 검색 */
folio = swap_cache_get_folio(entry, vma, vmf->address);
if (!folio) {
/* 2) cache miss: swap 디바이스에서 읽기 */
/* THP 슬롯이면 2MB 단위로 읽기 시도 */
folio = swapin_readahead(entry, gfp, vmf);
}
/* 3) PTE 복원 (THP면 PMD로 복원) */
if (folio_test_large(folio))
do_set_pmd(vmf, folio); /* PMD 매핑 복원 */
else
set_pte_at(...); /* PTE 매핑 복원 */
}
THP Swap 성능 비교
| 시나리오 | Split + 개별 Swap | THP Swap (통째로) | 개선 비율 |
|---|---|---|---|
| Swap-out 지연 | ~5 ms (512회 I/O) | ~50 μs (1회 I/O) | ~100x |
| Swap-in 지연 | ~5 ms (512회 fault) | ~50 μs (1회 fault) | ~100x |
| TLB miss 후 비용 | 높음 (PTE 레벨) | 낮음 (PMD 레벨) | ~3-5x |
| swap 슬롯 단편화 | 심각 (산발적) | 최소 (연속 할당) | 순차 I/O 유지 |
Folio 기반 Swap (커널 6.x)
커널 5.16+에서 도입된 folio 추상화는 메모리 관리의 기본 단위를 struct page에서 struct folio로 전환합니다. Swap 경로에서도 folio 기반 API가 점진적으로 적용되어 코드 명확성과 성능이 모두 개선되고 있습니다.
Folio란?
Folio는 하나 이상의 연속 물리 페이지를 나타내는 구조체입니다. 기존 struct page에서 "compound page의 head page인지 tail page인지" 판별하는 복잡한 로직을 제거하고, 항상 논리적 단위의 첫 페이지를 가리킵니다.
/* include/linux/mm_types.h - folio 핵심 (간략화) */
struct folio {
union {
struct {
unsigned long flags; /* PG_locked, PG_dirty 등 */
struct list_head lru; /* LRU 리스트 연결 */
struct address_space *mapping;
pgoff_t index;
unsigned long private;
atomic_t _mapcount;
atomic_t _refcount;
};
struct page page; /* 하위 호환 */
};
unsigned char _folio_order; /* 2^order 페이지 포함 */
};
Folio Swap 핵심 함수
| 함수 | 대체 대상 | 역할 |
|---|---|---|
folio_alloc_swap() | get_swap_page() | folio 크기에 맞는 연속 swap 슬롯 할당 |
folio_start_writeback() | set_page_writeback() | folio 단위 writeback 시작 표시 |
swap_write_folio() | swap_writepage() | folio 전체를 swap 디바이스에 기록 |
swap_read_folio() | swap_readpage() | swap 디바이스에서 folio 읽기 |
folio_free_swap() | try_to_free_swap() | folio의 swap 슬롯 해제 |
filemap_get_folio() | find_get_page() | swap cache에서 folio 검색 |
Folio Swap 흐름
Folio Swap의 장점
- 락 경합 감소: 대형 folio는 하나의 락으로 여러 페이지를 관리하므로, 512개 개별 페이지 락 대비 경합이 크게 줄어듭니다.
- 배치 연산: LRU 조작, writeback 상태 변경, swap cache 등록이 folio 단위로 한 번에 수행됩니다.
- 타입 안전성:
struct folio *는 항상 head page를 보장하므로, tail page를 실수로 조작하는 버그가 원천 차단됩니다. - 통계 정확성:
folio_nr_pages()로 항상 정확한 페이지 수를 얻을 수 있어, compound page 처리 관련 off-by-one 버그가 사라집니다.
Large Folio Swapout
커널 6.x에서는 2MB THP뿐 아니라 다양한 크기의 large folio (16KB, 32KB, 64KB 등)가 지원됩니다. swap 경로에서도 이러한 중간 크기 folio를 split 없이 처리할 수 있어, 워크로드 특성에 맞는 최적 크기를 자동 선택합니다.
| Folio 크기 | 페이지 수 | Swap 슬롯 | 적합한 워크로드 |
|---|---|---|---|
| 4 KB (base) | 1 | 1 | 소규모 할당, 세밀한 관리 |
| 16 KB | 4 | 4 (연속) | ARM64 기본 페이지 크기 |
| 64 KB | 16 | 16 (연속) | 중간 규모 할당 |
| 2 MB (THP) | 512 | 512 (연속) | 대규모 메모리 워크로드 |
# folio 관련 통계 확인
grep -E "folio|thp_swpout" /proc/vmstat
# large folio 할당 현황
cat /proc/buddyinfo
# large folio swap 지원 확인
grep CONFIG_THP_SWAP /boot/config-$(uname -r)
struct page 기반입니다. swap_writepage() → swap_write_folio() 전환이 완료되면 모든 swap I/O가 folio 단위로 통합될 예정입니다.
Folio Swap-in 경로
folio 기반 swap-in은 기존 page 기반 대비 경로가 단순화됩니다. swap cache에서 folio를 조회하고, 없으면 swap 디바이스에서 folio 단위로 읽어옵니다.
/* mm/memory.c - folio swap-in (간략화) */
static vm_fault_t do_swap_page(struct vm_fault *vmf)
{
swp_entry_t entry = pte_to_swp_entry(vmf->orig_pte);
struct folio *folio;
/* 1) swap cache에서 folio 검색 */
folio = swap_cache_get_folio(entry, vma, vmf->address);
if (!folio) {
/* 2) SWP_SYNCHRONOUS_IO (zram): 빠른 경로 */
if (data_race(si->flags & SWP_SYNCHRONOUS_IO)) {
folio = vma_alloc_folio(gfp, 0, vma, addr);
swap_read_folio(folio, true);
} else {
/* 3) 일반 디바이스: readahead 적용 */
folio = swapin_readahead(entry, gfp, vmf);
}
}
/* 4) folio → PTE/PMD 매핑 복원 */
swap_free(entry);
set_pte_at(mm, addr, pte, mk_pte(&folio->page, vma->vm_page_prot));
}
struct page → folio 전환 현황
| 서브시스템 | 전환 상태 (6.x) | 핵심 변경 |
|---|---|---|
| Swap Out (shrink) | 완료 | shrink_page_list → shrink_folio_list |
| Swap Cache | 완료 | XArray folio 기반 조회 |
| Swap Write | 진행 중 | swap_writepage → swap_write_folio |
| Swap Read | 진행 중 | swap_readpage → swap_read_folio |
| do_swap_page | 대부분 완료 | folio 기반 PTE 복원 |
| Readahead | 진행 중 | folio 기반 readahead |
Folio Swap Cache 변화
folio 전환 이후 swap cache도 folio 단위로 관리됩니다. 대형 folio는 swap cache에서 단일 엔트리로 존재하므로, 512개 개별 page 엔트리 대비 XArray 조회 비용이 크게 줄어듭니다.
/* mm/swap_state.c - folio swap cache 조회 */
struct folio *swap_cache_get_folio(swp_entry_t entry,
struct vm_area_struct *vma,
unsigned long addr)
{
struct swap_info_struct *si = swp_swap_info(entry);
struct address_space *space = swap_address_space(entry);
pgoff_t idx = swp_offset(entry);
/* XArray에서 folio 검색 — 대형 folio는 한 번에 조회 */
struct folio *folio = filemap_get_folio(space, idx);
if (!IS_ERR(folio)) {
/* swap cache hit: I/O 불필요 */
return folio;
}
return NULL;
}
Folio 관련 커널 구성 옵션
| 옵션 | 기본값 | 설명 |
|---|---|---|
CONFIG_THP_SWAP | y (6.x) | THP/large folio swap 지원 |
CONFIG_TRANSPARENT_HUGEPAGE | y | THP 전체 활성화 (folio 전제조건) |
CONFIG_READ_ONLY_THP_FOR_FS | y | 파일시스템 read-only THP |
# folio 및 THP swap 지원 커널 구성 확인
grep -E "CONFIG_THP_SWAP|CONFIG_TRANSPARENT_HUGEPAGE" /boot/config-$(uname -r)
# CONFIG_TRANSPARENT_HUGEPAGE=y
# CONFIG_THP_SWAP=y
# 런타임 large folio 통계
grep -E "thp_swpout|folio" /proc/vmstat 2>/dev/null
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never
# 특정 프로세스의 THP 사용 현황
grep -E "AnonHugePages|ShmemHugePages" /proc/$(pgrep -f myapp)/smaps_rollup
관련 문서
Swapping 서브시스템과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.