Swapping 서브시스템

Linux 커널 Swapping 메커니즘: swap 공간 설정, swap cache, swap out/in 경로, swappiness 튜닝, zswap/zram 압축 스왑, 성능 모니터링 종합 가이드.

관련 페이지: 기본 메모리 관리는 메모리 관리 (기초), 고급 메모리 관리는 메모리 관리 (심화) 페이지를 참고하세요. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 메모리 관리 기초CPU 캐시 문서를 먼저 읽으세요. 메모리 서브시스템은 가상 메모리와 물리 메모리 정책이 동시에 동작하므로, 주소 변환과 회수 정책을 같이 보는 관점이 필요합니다.
일상 비유: 이 주제는 창고 적재와 재배치 운영과 비슷합니다. 빈 공간을 잘 배치해야 출고가 빨라지듯이, 페이지 배치/회수 정책이 성능과 지연을 직접 좌우합니다.

핵심 요약

  • Swap 공간 — 물리 메모리 부족 시 익명 페이지를 임시 저장하는 디스크 영역입니다.
  • Swap Cache — swap out된 페이지를 RAM에 캐싱하여, 다시 접근 시 디스크 I/O 없이 빠르게 복원합니다.
  • kswapd — 백그라운드에서 메모리 부족을 미리 감지하고 페이지를 회수하는 커널 스레드입니다.
  • swappiness — 익명 페이지(swap)와 파일 페이지(page cache) 중 어느 쪽을 먼저 회수할지 비율을 조정하는 커널 파라미터입니다.
  • zswap / zram — 디스크 대신 압축된 RAM 영역을 swap으로 사용하여 성능을 크게 개선합니다.

단계별 이해

  1. Swap의 필요성 — 익명 페이지(힙, 스택, mmap anonymous)는 backing store가 없어 swap 없이는 회수 불가능합니다.

    파일 페이지는 원본 파일에서 다시 읽을 수 있지만, 익명 페이지는 swap이 유일한 저장소입니다.

  2. Swap Out/In 과정 — kswapd가 메모리 압력을 감지하면 LRU 리스트에서 오래 사용되지 않은 페이지를 선택하여 디스크에 기록하고, 물리 페이지를 회수합니다.

    나중에 해당 페이지에 접근하면 page fault가 발생하고, 커널이 swap 영역에서 다시 읽어옵니다(swap in).

  3. Swap Cache 최적화 — swap out된 페이지가 아직 RAM에 남아 있으면, 다시 접근 시 디스크 I/O 없이 즉시 복원됩니다.

    이를 통해 반복적으로 접근되는 페이지의 swap in 비용을 크게 줄입니다.

  4. 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된 프로세스 간 일관성을 보장합니다.

프로세스 PTE swp_entry_t (P=0) Swap Cache address_space (swapper_spaces) XArray: offset → struct page PageSwapCache 플래그 설정됨 Swap 영역 파티션 or 파일 slot[0] slot[1] slot[2] ... 물리 페이지 struct page (RAM) lookup page swap out swap in Swap Cache 역할 1. 중복 swap I/O 방지 2. fork CoW 일관성 보장 3. swap readahead 저장소 4. swap in 완료 전 재접근 처리 SwapCached in /proc/meminfo → 이 캐시의 크기를 나타냄
/* 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->mappingswapper_spaces[]address_space를 가리킵니다. /proc/meminfoSwapCached 항목이 현재 스왑 캐시 크기를 나타냅니다.

페이지 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 감지: vmstatsi/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합니다.

메모리 회수 swap_writepage() zswap (RAM) 압축 엔진: lz4/lzo/zstd 메모리 풀: z3fold/zsmalloc max_pool_percent로 크기 제한 풀 가득 차면 → writeback same-filled page 최적화 스왑 영역 디스크/SSD swap in zswap 먼저 miss→디스크 압축 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 비교

특성zswapzram
동작 방식 기존 스왑 영역의 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, iowaitzswap/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);
}
클러스터 기반 swap 슬롯 할당 흐름 CPU N reclaim 경로 get_swap_page() Per-CPU cluster 힌트 사용 연속 슬롯 우선 할당 성공? swap_map 갱신 + entry 반환 I/O locality 개선 실패 free_clusters 글로벌 획득 경합 시 비용 증가 최후 경로: lowest_bit~highest_bit 선형 탐색 단편화/고사용률일수록 CPU 비용 증가 pool_total_size가 아니라 swap_map 밀도도 같이 봐야 함 운영 포인트 여러 swap 장치 + 우선순위 I/O 분산 효과
swap 할당은 "빈 공간 존재 여부"보다 "연속성/경합"이 성능을 더 크게 좌우합니다.

cgroup v2 Swap 제어 심화

현대 운영 환경에서 swap 정책은 전역 `vm.swappiness`보다 cgroup 단위 제어가 더 중요합니다. 컨테이너별 상한과 보호 정책으로 swap thrashing 전파를 차단할 수 있습니다.

파일의미실전 활용
memory.swap.max해당 cgroup swap 사용 상한0이면 swap 금지, 장애 전파 차단
memory.high소프트 상한 (초과 시 reclaim/스로틀)OOM 전에 완충 구간 형성
memory.max하드 상한초과 시 강제 회수/킬
memory.eventshigh/max/oom 이벤트 카운터자동 스케일링/알람 트리거
memory.pressurePSI 지표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"
운영 원칙: latency 민감 워크로드는 memory.swap.max=0, 배치/비동기 워크로드는 제한된 swap 허용이 안정적입니다. 같은 호스트에서 두 유형을 분리하지 않으면 swap 지연이 서비스 전반으로 전파됩니다.

Swap 지연시간 분해

swap fault 한 번의 비용은 단일 값이 아닙니다. lock 대기, 압축 해제, 블록 I/O, 페이지 테이블 복원 비용이 합산됩니다. 병목 구간을 분리 측정해야 튜닝이 정확해집니다.

Swap Fault 지연시간 분해 (예시) PTE walk Swap Cache lookup zswap hit zswap miss 시 디스크 read I/O PTE restore zswap hit 경로: 보통 수십~수백 us CPU 비용 중심 (압축 해제 + 매핑 복원) 디스크 경로: 수 ms ~ 수십 ms 스토리지 큐/혼잡도 영향 큼 계측 체크리스트 1) pswpin/pswpout 증가율과 memory.pressure(PSI) 동시 확인 2) zswap hit/miss 비율과 written_back_pages로 디스크 경로 비중 파악 3) iowait가 높고 si가 지속되면 swap 정책보다 저장장치 병목이 우선 원인
튜닝 우선순위는 항상 "디스크 경로 비중"을 먼저 파악한 뒤 결정해야 합니다.

swap-subsystem과 zswap 페이지 중복 검토

두 문서를 비교하면 아래 주제가 중복됩니다. 중복 자체는 탐색 편의에는 유리하지만, 심화 관점에서는 역할 분리가 더 명확해야 합니다.

실무에서는 먼저 이 페이지에서 시스템 정책을 고정하고, 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 병목 원인 분기공통 증상이라도 진단 관문을 분리
운영 문서 분업: 정책 계층과 기능 계층 분리 swap-subsystem (정책 계층) - reclaim 경로, swappiness, memcg, PSI - 서비스 유형별 swap 허용/차단 규칙 - 장애 시 전체 호스트 안정성 우선 - 측정: vmstat, memory.pressure, iowait - 산출물: 운영 기준선(baseline) zswap (기능 계층) - zpool/압축 알고리즘/accept_threshold - reject 카운터 해석과 writeback 제어 - 디버깅: zswap debugfs/ftrace - 측정: stored/written_back/reject_* - 산출물: 파라미터 세트 정책 확정 후 파라미터 조정 읽기 순서: swap-subsystem 정책 확정 → zswap 파라미터 최적화 → 다시 PSI/iowait로 전체 검증
중복 최소화 규칙: 문서를 갱신할 때 동일 주제를 두 페이지에 모두 확장하지 말고, 한 페이지를 원문으로 지정한 뒤 다른 페이지는 3~5줄 요약과 링크로 유지하십시오.

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;
    }
}
스캔 비율 계산: priority N에서 스캔되는 LRU 페이지 수는 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.cpriority 감소 루프, 노드 단위 회수 조율
shrink_node()mm/vmscan.c노드 내 memcg 순회, LRU 리스트 회수
get_scan_count()mm/vmscan.cswappiness 기반 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;
}
swappinessanon:file 비율적합한 워크로드
00:200 (파일만 회수)swap 완전 차단 (OOM 위험 증가)
1010:190데이터베이스 (anon 보호 강조)
60 (기본)60:140범용 서버
100100:100 (동등)zram 사용 시 권장
200200:0 (anon만 회수)특수: file cache 최우선 보호

kswapd 워터마크 트리거 다이어그램

Zone 워터마크와 kswapd 동작 트리거 Free Pages HIGH watermark LOW watermark MIN watermark 정상 할당 영역 kswapd 활성 구간 Direct Reclaim 구간 free ≥ HIGH → kswapd sleep 회수 목표 도달, kswapd 대기 상태 free < LOW → wakeup_kswapd() alloc_pages()가 kswapd 깨움 백그라운드 회수 시작 (비차단) free < MIN → Direct Reclaim 할당자가 직접 회수 수행 (차단) 프로세스 지연 발생, 최악의 경우 OOM 워터마크 = /proc/zoneinfo에서 확인 | vm.min_free_kbytes로 MIN 조정 → LOW/HIGH 자동 조정
kswapd는 free pages가 LOW 이하로 떨어지면 깨어나 HIGH까지 회수합니다.
vm.min_free_kbytes 튜닝 주의: 이 값을 너무 높이면 유휴 메모리가 증가하여 워크로드 성능이 저하되고, 너무 낮추면 direct reclaim 빈도가 증가하여 지연이 발생합니다. 기본값은 시스템 RAM에 따라 자동 계산됩니다.

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 → 최신 세대, 미접근 → 유지 */
}
항목기존 LRUMGLRU
에이징 정확도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_writepage1 (kswapd), 0 (direct)dirty page writeback 허용 여부
may_unmap1매핑된 페이지 unmap 허용 여부
may_swap1swap out 허용 여부
gfp_maskGFP_KERNEL할당 플래그 (회수 범위 결정)
priority12 → 0스캔 범위 (낮을수록 공격적)
nr_to_reclaimSWAP_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까지 비동기적으로 페이지를 회수합니다. 정상적인 시스템에서는 대부분의 회수가 이 경로에서 처리됩니다.

경로트리거차단 여부성능 영향
kswapdfree < LOW비차단 (백그라운드)정상 — 사용자 지연 없음
Direct Reclaimfree < MIN차단심각 — 프로세스 stall
kcompactdhigh-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_factor10 (0.1%)LOW-MIN, HIGH-MIN 간격150~500: direct reclaim 빈도 감소
vm.watermark_boost_factor15000fragmentation 시 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;
}

페이지 회수 결정 플로차트

페이지 할당 실패 시 회수 결정 플로차트 __alloc_pages() free ≥ LOW ? zone_watermark_ok() Yes 할당 성공 No wakeup_kswapd() free ≥ MIN ? Yes kswapd 대기 후 재시도 No Direct Reclaim 회수 성공? No OOM Killer Yes 할당 성공
alloc_pages()는 워터마크를 단계별로 확인하며 kswapd → direct reclaim → OOM 순서로 에스컬레이션합니다.
실전 모니터링: pgsteal_kswapdpgsteal_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 μspgsteal_kswapd
kswapd (dirty writeback)~50 μs (SSD)~5 ms (HDD)pgsteal_kswapd + pageoutrun
Direct reclaim (clean)~10 μs~100 μspgsteal_direct
Direct reclaim (dirty)~1 ms (SSD)~50 ms (HDD)pgsteal_direct + allocstall_*
kcompactd~100 μs~10 mscompact_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;
    }
}
dirty 비율 튜닝: vm.dirty_ratiovm.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 사전 방지
Android와 PSI: Android의 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 ReclaimNR_MEMSTALL__perform_reclaim()shrink_node()
Swap-in I/ONR_MEMSTALLdo_swap_page()swap_readpage()
Thrashing (refault)NR_MEMSTALLworkingset_refault()
THP splitNR_MEMSTALLsplit_huge_page() (간접)

PSI vs 기존 모니터링 방식 비교

방식지표장점한계
vmstat si/soswap in/out 초당 블록 수간단, 오래된 커널 지원stall 영향 크기 모름
/proc/meminfoSwapTotal, SwapFree 등현재 상태 확인추세/속도 정보 없음
cgroup memory.eventslow, high, max, oomcgroup 단위 이벤트시간 비율 정보 없음
PSI memorysome/full, avg10/60/300실제 지연 비율, 트리거 지원커널 4.20+ 필요
PSI의 핵심 가치: 기존 지표는 "swap이 발생하고 있다"를 알려주지만, PSI는 "swap 때문에 애플리케이션이 실제로 얼마나 느려졌는가"를 정량화합니다. 이 차이가 사전 대응적 관리의 핵심입니다.

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-cluster3readahead 크기 = 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
순차 vs 랜덤: 데이터베이스처럼 랜덤 접근이 지배적인 워크로드에서는 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 비트 레이아웃 다이어그램

swp_entry_t 비트 레이아웃 (x86_64) PTE (64 bits) 0 P=0 TYPE bit 1~5 (5 bits) 디바이스 번호 0~31 OFFSET bit 6~63 (58 bits) 디바이스 내 슬롯 번호 (최대 2⁵⁸ 슬롯 = 1 EiB) swap_info_struct (per-device) type → swap_info[type] 배열 인덱스 swap_map[] → 각 슬롯 참조 카운트 cluster_info[] → 클러스터 메타데이터 bdev / swap_file → 백엔드 디바이스/파일 Per-CPU Slot Cache percpu_cluster.index → 현재 클러스터 percpu_cluster.next → 다음 할당 힌트 락 없이 빠른 슬롯 할당 가능 클러스터 소진 시 → 글로벌 free_clusters
swp_entry_t는 5비트 type + 58비트 offset으로 최대 32개 디바이스, 디바이스당 2⁵⁸ 슬롯을 주소 지정합니다.

swap_info_struct 핵심 필드

필드타입역할
swap_mapunsigned char *슬롯별 참조 카운트 (0=free, 1+=사용중)
cluster_infostruct swap_cluster_info *클러스터별 free/사용 상태
swap_filestruct file *swap 파일/파티션의 file 객체
bdevstruct block_device *블록 디바이스 참조
pagesunsigned long총 슬롯 수
inuse_pagesunsigned int현재 사용 중인 슬롯 수
priointswap 우선순위 (높을수록 먼저 사용)
flagsunsigned longSWP_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; /* 디스크 시작 섹터 */
};
swap 파일 조각화: 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 성능 영향: 실시간 DISCARD(--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
디바이스 유형권장 우선순위용도지연시간
zram100 (최고)압축 swap, 1차 흡수~1 μs
NVMe SSD50고속 swap, 2차 흡수~50 μs
SATA SSD30중속 swap~200 μs
HDD10 (최저)대용량 swap, 최후의 수단~5 ms
NFS/NBD5네트워크 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 교착 */
네트워크 swap 주의: NFS/NBD swap은 네트워크 장애 시 시스템 hang을 유발할 수 있습니다. 커널 5.x+에서 swap over NFS가 개선되었지만, 프로덕션에서는 여전히 주의가 필요합니다. SWP_SYNCHRONOUS_IO 플래그 미지원으로 성능도 불리합니다.
항목로컬 디바이스 SwapNFS/NBD Swap
지연시간~50 μs (SSD)~1-100 ms (네트워크 의존)
신뢰성높음네트워크 장애 시 hang 위험
SWP_SYNCHRONOUS_IO지원 (zram, brd)미지원
readaheadcluster + VMAVMA 주로
메모리 예약불필요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
하이브리드 swap의 장점: zram이 1차로 메모리 압력을 흡수하고, overflow만 디스크로 내려가므로 디스크 I/O를 최소화합니다. swappiness=100과 함께 사용하면 파일 캐시를 보호하면서도 익명 페이지를 효과적으로 관리할 수 있습니다.

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.xswap 전 반드시 split → 512개 4KB 페이지로 개별 swapsplit 비용 + 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은 다음 작업을 수반합니다:

# 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
실전 권장: SSD swap에서는 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 + 개별 SwapTHP 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 Out 흐름 (커널 6.x) shrink_folio_list() folio_alloc_swap() 연속 슬롯 할당 add_to_swap_cache() swap cache에 등록 try_to_unmap(folio) PTE를 swap entry로 교체 folio_start_writeback() writeback 상태 표시 swap_write_folio() zram / brd SWP_SYNCHRONOUS_IO 동기 I/O (즉시 완료) Block I/O Layer SSD / HDD / NFS 비동기 bio submit folio 회수 완료
folio 기반 swap out은 shrink_folio_list()에서 시작하여 folio 단위로 슬롯 할당 → cache 등록 → PTE 교체 → writeback을 수행합니다.

Folio Swap의 장점

Large Folio Swapout

커널 6.x에서는 2MB THP뿐 아니라 다양한 크기의 large folio (16KB, 32KB, 64KB 등)가 지원됩니다. swap 경로에서도 이러한 중간 크기 folio를 split 없이 처리할 수 있어, 워크로드 특성에 맞는 최적 크기를 자동 선택합니다.

Folio 크기페이지 수Swap 슬롯적합한 워크로드
4 KB (base)11소규모 할당, 세밀한 관리
16 KB44 (연속)ARM64 기본 페이지 크기
64 KB1616 (연속)중간 규모 할당
2 MB (THP)512512 (연속)대규모 메모리 워크로드
# folio 관련 통계 확인
grep -E "folio|thp_swpout" /proc/vmstat

# large folio 할당 현황
cat /proc/buddyinfo
# large folio swap 지원 확인
grep CONFIG_THP_SWAP /boot/config-$(uname -r)
점진적 전환: folio API 전환은 수백 개의 패치를 통해 진행 중이며, 커널 6.x에서도 일부 경로는 아직 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_SWAPy (6.x)THP/large folio swap 지원
CONFIG_TRANSPARENT_HUGEPAGEyTHP 전체 활성화 (folio 전제조건)
CONFIG_READ_ONLY_THP_FOR_FSy파일시스템 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 서브시스템과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.