KSM (Kernel Same-page Merging)

리눅스 커널의 KSM(Kernel Same-page Merging) 서브시스템을 심층 분석합니다. 동일한 내용을 가진 익명 페이지(Anonymous Page)를 감지하여 하나의 COW(Copy-On-Write) 페이지(Page)로 병합함으로써 물리 메모리(Physical Memory) 사용량을 획기적으로 줄이는 메커니즘입니다. ksmd 스캔 데몬, stable/unstable 레드블랙 트리(Red-Black Tree), 해시(Hash) 기반 비교 알고리즘, COW 분리, madvise/prctl API, /sys/kernel/mm/ksm sysfs 인터페이스, KVM/컨테이너(Container) 환경 활용, NUMA-aware 병합, THP 상호작용, side-channel 보안 이슈, 성능 특성과 튜닝까지 커널 소스(mm/ksm.c) 기반으로 분석합니다.

전제 조건: 메모리 관리(Memory Management) 개요VMA & mmap 문서를 먼저 읽으세요. KSM은 익명 페이지(anonymous page)의 역매핑(rmap)과 COW(Copy-On-Write) 메커니즘을 기반으로 동작하므로, 페이지 테이블(Page Table)과 VMA 구조를 이해해야 합니다.
일상 비유: KSM은 도서관의 중복 도서 관리와 비슷합니다. 100명의 회원이 같은 책을 각각 소유하고 있다면, 도서관이 한 권만 비치하고 모두가 공유하게 합니다. 누군가 책에 메모를 쓰고 싶으면(쓰기) 그때서야 개인 복사본을 만들어줍니다. 이것이 COW입니다.

핵심 요약

  • 동일 페이지 병합 -- 내용이 동일한 익명 페이지를 하나의 물리 페이지로 합치고, 원본 페이지를 해제하여 메모리를 절약합니다.
  • COW 기반 공유 -- 병합된 페이지는 write-protect 상태로, 프로세스(Process)가 쓰기를 시도하면 page fault가 발생하여 개별 복사본이 생성됩니다.
  • ksmd 데몬 -- 커널 스레드(Kernel Thread) ksmd가 주기적으로 등록된 메모리 영역을 스캔하며 병합 후보를 찾습니다.
  • 두 개의 트리 -- stable tree(이미 병합된 페이지)와 unstable tree(후보 페이지)로 효율적인 검색을 수행합니다.
  • 가상화(Virtualization) 핵심 기술 -- 동일 OS 이미지로 실행되는 수십~수백 VM의 중복 메모리를 병합하여 서버 통합 밀도를 크게 높입니다.

단계별 이해

  1. KSM 개념 파악
    동일 내용의 물리 페이지를 하나로 합치는 개념과, 왜 익명 페이지만 대상인지 이해합니다.
  2. 데이터 구조 학습
    stable tree와 unstable tree의 레드블랙 트리 구조, rmap_item의 역할을 파악합니다.
  3. 스캔 알고리즘 추적
    ksmd가 페이지 내용을 해시하고 트리에서 비교하여 병합하는 과정을 따라갑니다.
  4. API와 제어 인터페이스
    madvise(), prctl(), /sys/kernel/mm/ksm/ 파라미터를 사용하여 KSM을 제어하는 방법을 익힙니다.
  5. 실전 튜닝과 모니터링
    워크로드에 맞는 스캔 속도 조절, 보안 고려사항, 성능 모니터링 방법을 숙지합니다.
관련 커널 소스: mm/ksm.c (KSM 핵심 로직), include/linux/ksm.h (API 선언), mm/memory.c (COW page fault 처리). KSM은 Andrea Arcangeli가 KVM 메모리 최적화를 위해 v2.6.32(2009년)에 도입했으며, 원래 이름은 KSM(Kernel Shared Memory)이었습니다. 종합 목록은 참고자료 -- 표준 & 규격 섹션을 참고하세요.

KSM 개요

KSM(Kernel Same-page Merging)은 물리 메모리에서 동일한 내용을 가진 익명 페이지(anonymous page)를 자동으로 감지하고 하나의 물리 페이지로 병합하는 커널 서브시스템입니다. 병합된 페이지는 COW(Copy-On-Write) 보호 상태가 되어, 읽기 접근은 공유하고 쓰기 접근 시에만 복사본을 생성합니다.

개발 동기와 역사

버전연도주요 변화
v2.6.322009Andrea Arcangeli가 KVM 메모리 최적화 목적으로 초기 구현 (KSM = Kernel Shared Memory)
v3.92013NUMA-aware 병합: merge_across_nodes 파라미터 추가
v4.42016zero page 최적화: 0으로 채워진 페이지를 커널 zero page와 병합
v5.72020advisor mode 도입으로 자동 튜닝 기반 마련
v6.02022ksm_swpin_copy 카운터 추가, 스왑(Swap) 인 시 KSM 추적 개선
v6.12022prctl(PR_SET_MEMORY_MERGE) 추가: 프로세스 전체 madvise 자동 적용
v6.42023general_profit 카운터, advisor scan-time/cpu-percent 모드 도입
v6.72024per-process KSM 통계: /proc/PID/ksm_stat

왜 KSM이 필요한가

동일한 게스트 OS(예: Ubuntu 22.04)를 실행하는 50대의 VM이 있다고 가정합니다. 각 VM에 2GB RAM을 할당하면 총 100GB가 필요하지만, 커널 코드, 라이브러리, 초기화된 데이터 등 상당 부분이 동일합니다. KSM은 이 중복을 실시간(Real-time)으로 감지하여 물리 메모리 사용량을 40~70%까지 절감할 수 있습니다.

파일 백업 페이지 vs 익명 페이지: 파일 백업 페이지(file-backed page)는 이미 페이지 캐시(Page Cache)를 통해 공유되므로 KSM의 대상이 아닙니다. KSM은 오직 익명 페이지(스택, 힙, 익명 mmap)만 처리합니다. 이는 파일과 연결되지 않아 자연적 공유가 불가능한 페이지입니다.

KSM 아키텍처

KSM 서브시스템은 세 가지 핵심 데이터 구조와 하나의 커널 스레드로 구성됩니다.

KSM 아키텍처 개요 ksmd (커널 스레드) mm_slot 연결 리스트 (등록된 mm_struct) VM-1 mm VM-2 mm VM-3 mm Container mm ... 순회 스캔 Stable Tree (rbtree) 병합 완료된 KSM 페이지 root page page Unstable Tree (rbtree) 병합 후보 페이지 (매 스캔 재구축) root cand cand

핵심 데이터 구조

구조체(Struct)위치역할
struct ksm_mm_slotmm/ksm.cKSM에 등록된 각 mm_struct를 추적. 연결 리스트(Linked List)로 모든 등록 프로세스를 순회
struct ksm_rmap_itemmm/ksm.c스캔 대상 가상 주소(Virtual Address)와 해당 물리 페이지의 매핑(Mapping) 정보. stable/unstable 트리 노드
struct ksm_stable_nodemm/ksm.cstable tree의 각 노드. 병합된 KSM 페이지의 대표 노드
struct ksm_scanmm/ksm.cksmd의 현재 스캔 위치(mm_slot, address)를 기억하는 전역 커서
/* mm/ksm.c - 핵심 자료구조 (간략화) */

struct ksm_rmap_item {
    struct ksm_rmap_item *rmap_list; /* 다음 rmap_item 연결 */
    union {
        struct anon_vma *anon_vma;   /* 역매핑에 사용 */
    };
    struct mm_struct *mm;            /* 소속 프로세스 */
    unsigned long address;          /* 가상 주소 */
    unsigned int oldchecksum;       /* 이전 해시값 */
    union {
        struct rb_node node;         /* unstable tree 노드 */
        struct {
            struct ksm_stable_node *head; /* stable node 포인터 */
            struct hlist_node hlist;      /* stable node의 hlist */
        };
    };
};

struct ksm_stable_node {
    union {
        struct rb_node node;    /* stable tree 노드 */
        struct {
            struct list_head *head; /* migration 리스트 */
        };
    };
    struct hlist_head hlist;   /* 이 페이지를 공유하는 rmap_item 리스트 */
    union {
        unsigned long kpfn;    /* KSM 페이지의 PFN */
        unsigned long chain;   /* chain 이동 횟수 */
    };
    int rmap_hlist_len;             /* 공유 횟수 (rmap 리스트 길이) */
    int nid;                        /* NUMA 노드 ID */
};
rmap_item의 이중 역할: ksm_rmap_item은 union을 사용하여 unstable tree에 있을 때는 rb_node로, stable tree에 병합된 후에는 hlist_node로 동작합니다. 이 설계로 추가 메모리 할당 없이 상태 전이가 가능합니다.

ksmd 스캔 알고리즘

ksmd는 무한 루프에서 등록된 메모리 영역을 순회하며 동일 페이지를 찾습니다. 각 스캔 주기에서 pages_to_scan개의 페이지를 처리한 후 sleep_millisecs만큼 휴식합니다.

ksmd 스캔 알고리즘 흐름 페이지 가져오기 (scan cursor) CRC32/xxhash 체크섬 계산 oldchecksum == 새 checksum? 내용 변경 -> checksum 갱신 Stable Tree에서 memcmp 검색 Stable Tree에서 일치 발견? 기존 KSM 페이지에 병합! Unstable Tree에서 memcmp 검색 Unstable Tree에서 일치 발견? 새 KSM 페이지 생성 Stable Tree에 삽입 Unstable Tree에 삽입 No Yes Yes No Yes No

스캔 단계 상세

  1. 페이지 가져오기 -- ksm_scan 커서가 가리키는 mm_slot의 다음 가상 주소에서 페이지를 가져옵니다. get_mergeable_page()가 해당 주소의 PTE를 확인하고 물리 페이지를 반환합니다.
  2. 체크섬(Checksum) 비교 -- 페이지 내용의 CRC32 해시를 계산합니다. 이전 스캔의 oldchecksum과 비교하여 페이지 내용이 변경되었으면 unstable tree에서 제거하고 새 체크섬을 저장합니다. 내용이 안정적(두 번 연속 같은 해시)이어야 비교를 진행합니다.
  3. Stable tree 검색 -- stable_tree_search()가 레드블랙 트리를 순회하며 memcmp()로 4KB 전체를 바이트 단위 비교합니다. 일치하면 기존 KSM 페이지에 병합합니다.
  4. Unstable tree 검색 -- stable tree에서 매치가 없으면 unstable_tree_search_insert()를 호출합니다. 일치하면 두 페이지 내용으로 새 KSM 페이지를 생성하고 stable tree에 삽입합니다. 일치하지 않으면 현재 페이지를 unstable tree에 삽입합니다.
/* mm/ksm.c - ksm_do_scan() 핵심 루프 (간략화) */
static void ksm_do_scan(unsigned int scan_npages)
{
    struct ksm_rmap_item *rmap_item;
    struct page *page;

    while (scan_npages-- && likely(!freezing(current))) {
        /* 1. 다음 후보 페이지와 rmap_item 가져오기 */
        rmap_item = scan_get_next_rmap_item(&page);
        if (!rmap_item)
            return;

        /* 2. 페이지 내용 기반으로 병합 시도 */
        cmp_and_merge_page(page, rmap_item);

        put_page(page);
    }
}
체크섬의 역할: 체크섬은 필터 역할만 합니다. 해시 충돌 가능성이 있으므로 최종 병합 전에는 반드시 memcmp()로 4KB 전체를 비교합니다. 체크섬이 불안정(변경된) 페이지를 조기에 걸러냄으로써 비용이 큰 memcmp() 호출을 줄이는 것이 핵심입니다.

scan_get_next_rmap_item() 상세

scan_get_next_rmap_item()은 ksmd 스캔의 핵심 커서 이동 함수입니다. mm_slot 연결 리스트를 순회하면서 각 프로세스의 VMA 내 가상 주소를 PAGE_SIZE 단위로 탐색합니다.

scan_get_next_rmap_item() 커서 이동 ksm_mm_head 연결 리스트 (등록된 프로세스) mm_slot[0] mm_slot[1] ★ mm_slot[2] ... mm_slot[N] seqnr++ 현재 mm_slot[1]의 VMA 순회 VMA: 0x7f00-0x7f10 VM_MERGEABLE ✓ VMA: 0x7f10-0x7f20 VM_SHARED (skip) VMA: 0x7f20-0x7f40 VM_MERGEABLE ✓ ★ VMA: 0x7f40-0x7f50 VM_MERGEABLE ✓ 현재 VMA (0x7f20-0x7f40) 내 페이지 단위 스캔: address += PAGE_SIZE cursor ... PAGE_SIZE 단위 이동 ... 커서 이동 알고리즘 1. ksm_scan.address += PAGE_SIZE 2. address가 현재 VMA 범위를 초과하면 → 다음 VM_MERGEABLE VMA로 이동 3. 현재 mm의 모든 VMA를 순회했으면 → 다음 mm_slot으로 이동 4. 모든 mm_slot을 순회 완료 → seqnr++ (unstable tree 암묵적 무효화) 5. get_mergeable_page()로 PTE→물리 페이지 변환, rmap_item 할당/재사용
/* mm/ksm.c - scan_get_next_rmap_item() 핵심 흐름 (간략화) */
static struct ksm_rmap_item *scan_get_next_rmap_item(struct page **page)
{
    struct ksm_mm_slot *slot;
    struct mm_struct *mm;
    struct vm_area_struct *vma;
    struct ksm_rmap_item *rmap_item;

    slot = ksm_scan.mm_slot;
    mm = slot->slot.mm;

    mmap_read_lock(mm);

    /* VMA 순회: VM_MERGEABLE 플래그가 있는 VMA만 대상 */
    for (vma = find_vma(mm, ksm_scan.address);
         vma && vma->vm_start < vma->vm_end;
         vma = find_vma(mm, ksm_scan.address)) {

        /* MERGEABLE이 아닌 VMA는 건너뜀 */
        if (!(vma->vm_flags & VM_MERGEABLE)) {
            ksm_scan.address = vma->vm_end;
            continue;
        }

        /* 현재 주소가 VMA 시작 이전이면 조정 */
        if (ksm_scan.address < vma->vm_start)
            ksm_scan.address = vma->vm_start;

        /* VMA 내 페이지 단위 순회 */
        while (ksm_scan.address < vma->vm_end) {
            /* PTE에서 물리 페이지를 가져옴 */
            *page = follow_page(vma, ksm_scan.address,
                                FOLL_GET | FOLL_MIGRATION);
            if (!*page || PageKsm(*page)) {
                /* 이미 KSM 페이지이거나 매핑 없음 */
                ksm_scan.address += PAGE_SIZE;
                continue;
            }

            /* rmap_item 할당 또는 기존 것 재사용 */
            rmap_item = get_next_rmap_item(slot,
                            ksm_scan.rmap_list,
                            ksm_scan.address);
            ksm_scan.address += PAGE_SIZE;

            mmap_read_unlock(mm);
            return rmap_item;
        }
    }

    /* 현재 mm의 모든 VMA 순회 완료 → 다음 mm_slot으로 */
    mmap_read_unlock(mm);

    slot = list_entry(slot->mm_node.next, ...);
    ksm_scan.mm_slot = slot;
    ksm_scan.address = 0;

    /* 모든 mm_slot 순회 완료? → seqnr 증가 */
    if (slot == &ksm_mm_head) {
        ksm_scan.seqnr++;  /* unstable tree 암묵적 무효화 */
        return NULL;
    }
    /* 다음 mm_slot에서 재시도 (재귀적 호출 아님, 실제로는 goto) */
    return scan_get_next_rmap_item(page);
}

get_mergeable_page() PTE 검증

get_mergeable_page()는 주어진 가상 주소에서 병합 가능한 물리 페이지를 가져오는 함수입니다. 다양한 조건을 검증하여 KSM 병합에 적합한 페이지만 반환합니다.

/* mm/ksm.c - get_mergeable_page() 검증 로직 (간략화) */
static struct page *get_mergeable_page(struct ksm_rmap_item *rmap_item)
{
    struct mm_struct *mm = rmap_item->mm;
    unsigned long addr = rmap_item->address;
    struct vm_area_struct *vma;
    struct page *page;

    mmap_read_lock(mm);
    vma = find_mergeable_vma(mm, addr);
    if (!vma)
        goto out;

    /* follow_page(): PTE walk로 물리 페이지 획득 */
    page = follow_page(vma, addr, FOLL_GET | FOLL_MIGRATION);
    if (IS_ERR_OR_NULL(page))
        goto out;

    /* 검증 조건들 */
    if (PageAnon(page) &&       /* 1. 익명 페이지여야 함 */
        !PageKsm(page) &&       /* 2. 이미 KSM 페이지가 아니어야 함 */
        !PageTransCompound(page)) /* 3. THP 복합 페이지가 아니어야 함 */
    {
        mmap_read_unlock(mm);
        return page;
    }

    put_page(page);
out:
    mmap_read_unlock(mm);
    return NULL;
}

/* find_mergeable_vma(): VMA 적합성 검증 */
static struct vm_area_struct *find_mergeable_vma(
    struct mm_struct *mm, unsigned long addr)
{
    struct vm_area_struct *vma;

    vma = vma_lookup(mm, addr);
    if (!vma)
        return NULL;

    /* 병합 불가 조건 */
    if (vma->vm_flags & (VM_SHARED   |  /* 공유 매핑 제외 */
                          VM_PFNMAP   |  /* PFN 매핑 제외 */
                          VM_IO       |  /* I/O 매핑 제외 */
                          VM_DONTEXPAND)) /* 확장 불가 제외 */
        return NULL;

    /* VM_MERGEABLE 플래그 필수 */
    if (!(vma->vm_flags & VM_MERGEABLE))
        return NULL;

    return vma;
}
THP 페이지 처리: get_mergeable_page()PageTransCompound 검사로 THP(2MB) 페이지를 건너뜁니다. KSM이 THP를 병합하려면 먼저 split_huge_page()로 512개의 4KB 페이지로 분할해야 하는데, 이 분할 비용이 크므로 ksmd는 이미 분할된 4KB 페이지만 대상으로 합니다. THP 영역에서 KSM을 사용하려면 해당 VMA에서 THP를 비활성화하는 것이 효율적입니다.

cmp_and_merge_page() 디스패치 로직

KSM 알고리즘의 핵심 디스패치 함수인 cmp_and_merge_page()는 하나의 후보 페이지를 받아 체크섬 비교, stable tree 검색, unstable tree 검색/삽입을 순차적으로 수행합니다.

cmp_and_merge_page() 내부 흐름 입력: page + rmap_item rmap_item이 이미 stable tree에? KSM 페이지의 내용이 변경됨 → stable tree에서 제거 Yes use_zero_pages && 페이지가 전부 0? No ZERO_PAGE(0)에 병합 zero_pages_sharing++ Yes checksum = calc_checksum(page) checksum == rmap_item→oldchecksum? oldchecksum = checksum 내용 변경 → 이번 스캔 종료 No stable_tree_search(page) Yes stable tree에서 일치 발견? 기존 KSM 페이지에 병합 try_to_merge_with_ksm_page() Yes unstable_tree_search_insert(page) No unstable tree에서 일치 발견? 새 KSM 페이지 생성 try_to_merge_two_pages() Yes 이미 unstable tree에 삽입됨 (search_insert가 삽입 수행) No 통계 카운터 갱신 stable 병합: pages_sharing++ 새 KSM: pages_shared++ 삽입만: pages_unshared++ 체크섬 변경: pages_volatile++
/* mm/ksm.c - cmp_and_merge_page() 핵심 디스패치 (간략화) */
static void cmp_and_merge_page(struct page *page,
                               struct ksm_rmap_item *rmap_item)
{
    struct page *kpage;
    unsigned int checksum;
    struct ksm_rmap_item *tree_rmap_item;

    /* 1단계: 이미 stable tree에 있는 rmap_item이면
     *   KSM 페이지가 변경되었는지 확인 후 제거 */
    if (in_stable_tree(rmap_item)) {
        /* stable 노드의 페이지와 비교 */
        kpage = get_ksm_page(rmap_item->head, ...);
        if (kpage == page)
            return;  /* 동일 KSM 페이지 → 변경 없음 */
        /* 내용 변경 → stable에서 제거 */
        remove_rmap_item_from_tree(rmap_item);
    }

    /* 2단계: zero page 최적화 */
    if (ksm_use_zero_pages &&
        pages_identical(page, ZERO_PAGE(0))) {
        replace_page(vma, page, ZERO_PAGE(0));
        ksm_zero_pages++;
        return;
    }

    /* 3단계: 체크섬 안정성 확인 */
    checksum = calc_checksum(page);
    if (rmap_item->oldchecksum != checksum) {
        /* 내용이 변경됨 → 체크섬 갱신 후 종료 */
        rmap_item->oldchecksum = checksum;
        return;  /* pages_volatile 증가 */
    }

    /* 4단계: stable tree 검색 (O(log n) memcmp) */
    kpage = stable_tree_search(page);
    if (kpage) {
        /* 일치! → 기존 KSM 페이지에 병합 */
        try_to_merge_with_ksm_page(rmap_item, page, kpage);
        put_page(kpage);
        return;  /* pages_sharing 증가 */
    }

    /* 5단계: unstable tree 검색 + 삽입 */
    tree_rmap_item = unstable_tree_search_insert(rmap_item, page);
    if (tree_rmap_item) {
        /* unstable에서 매치! → 새 KSM 페이지 생성 */
        struct page *tree_page;
        tree_page = get_mergeable_page(tree_rmap_item);
        kpage = try_to_merge_two_pages(
            rmap_item, page, tree_rmap_item, tree_page);
        put_page(tree_page);
        if (kpage) {
            /* stable tree에 삽입 */
            stable_tree_insert(kpage);
            /* pages_shared 증가 */
        }
    }
    /* 매치 없음: unstable tree에 이미 삽입됨 */
    /* pages_unshared 증가 */
}

Stable Tree 구조

Stable tree는 이미 병합이 완료된 KSM 페이지를 관리하는 레드블랙 트리입니다. 각 노드(ksm_stable_node)는 하나의 고유한 내용을 가진 KSM 페이지를 대표하며, 해당 페이지를 공유하는 모든 rmap_itemhlist로 연결합니다.

특성

속성설명
정렬 키페이지 내용의 memcmp() 결과 (사전식 비교)
생존 기간병합된 페이지가 존재하는 한 유지 (스캔 주기 독립)
노드당 공유 수rmap_hlist_len으로 추적, max_page_sharing(기본 256)으로 제한
NUMA 구분merge_across_nodes=0 시 NUMA 노드별 별도 stable tree 운영
stale 노드 처리페이지가 사라진 stable_node는 lazy하게 발견 시 제거
/* stable tree에서 페이지 검색 (간략화) */
static struct page *stable_tree_search(struct page *page)
{
    struct rb_node *node = root_stable_tree.rb_node;

    while (node) {
        struct ksm_stable_node *snode;
        struct page *tree_page;
        int ret;

        snode = rb_entry(node, struct ksm_stable_node, node);
        tree_page = get_ksm_page(snode, GET_KSM_PAGE_NOLOCK);
        if (!tree_page) {
            /* stale 노드: 트리에서 제거 */
            remove_node_from_stable_tree(snode);
            continue;
        }

        ret = memcmp_pages(page, tree_page);
        if (ret < 0)
            node = node->rb_left;
        else if (ret > 0)
            node = node->rb_right;
        else
            return tree_page;  /* 일치! */
    }
    return NULL;
}
max_page_sharing 제한: 하나의 stable_node에 너무 많은 rmap_item이 연결되면 COW 발생 시 역매핑 순회 비용이 급증합니다. /sys/kernel/mm/ksm/max_page_sharing(기본 256)으로 제한하며, 초과 시 동일 내용이라도 새 stable_node를 chain으로 생성합니다.

Stable Node Chain 메커니즘

max_page_sharing을 초과하면, KSM은 동일 내용의 KSM 페이지를 추가로 할당하고 새 stable_node를 생성하여 chain으로 연결합니다. 이는 하나의 stable_node에 수천 개의 rmap_item이 연결되어 COW 시 역매핑 순회 비용이 O(n)으로 폭증하는 것을 방지합니다.

Stable Node Chain (max_page_sharing=256) 일반 상태 (공유 ≤ 256) stable_node (rb_node) rmap[0] rmap[1] ... rmap[n] rmap_hlist_len ≤ 256 Chain 상태 (공유 > 256) stable_node[0] hlist: rmap[0..255] stable_node[1] hlist: rmap[256..511] stable_node[2] hlist: rmap[512..767] chain chain 검색 비용: 일반: O(log n) rbtree chain: O(log n) + O(c) Chain 생성 과정 1. stable_node의 hlist_len이 256에 도달 2. 새 KSM 페이지 할당 (동일 내용 복사) 3. 새 stable_node 생성, chain으로 연결 4. 새 rmap_item은 새 stable_node에 추가 stable_node_chains 카운터로 모니터링 chain이 많으면 max_page_sharing 증가 고려
/* mm/ksm.c - stable_tree_append()에서 chain 생성 (간략화) */
static void stable_tree_append(
    struct ksm_rmap_item *rmap_item,
    struct ksm_stable_node *stable_node,
    bool max_page_sharing_bypass)
{
    /* max_page_sharing 초과 여부 확인 */
    if (!max_page_sharing_bypass &&
        stable_node->rmap_hlist_len >= ksm_max_page_sharing) {
        /* chain 생성 필요: 새 KSM 페이지 + stable_node */
        struct ksm_stable_node *chain_node;
        struct page *chain_page;

        chain_page = alloc_page(GFP_HIGHUSER);
        copy_user_highpage(chain_page,
            get_ksm_page(stable_node, ...), ...);
        SetPageKsm(chain_page);

        chain_node = alloc_stable_node_chain(stable_node);
        chain_node->kpfn = page_to_pfn(chain_page);
        chain_node->rmap_hlist_len = 0;

        ksm_stable_node_chains++;
        /* rmap_item을 새 chain_node에 연결 */
        stable_node = chain_node;
    }

    /* hlist에 rmap_item 추가 */
    hlist_add_head(&rmap_item->hlist, &stable_node->hlist);
    stable_node->rmap_hlist_len++;
    rmap_item->head = stable_node;
    rmap_item->address |= STABLE_FLAG;
}
Chain의 메모리 비용: chain은 동일 내용의 KSM 페이지를 여러 벌 보유하므로 메모리 절약 효율이 감소합니다. stable_node_chains 카운터가 높으면 max_page_sharing을 늘려(예: 256 → 512) chain 생성을 줄이거나, 하나의 KSM 페이지에 너무 많은 프로세스가 몰리는 워크로드 패턴을 점검하세요. 반대로 값을 너무 높이면 COW 발생 시 역매핑 순회 비용이 증가합니다.

Unstable Tree 구조

Unstable tree는 아직 병합되지 않은 후보 페이지를 보관하는 레드블랙 트리입니다. Stable tree와 달리, unstable tree는 매 전체 스캔 주기(seqnr 증가)마다 암묵적으로 재구축됩니다.

왜 매 스캔마다 재구축하는가

unstable tree의 페이지는 아직 write-protect가 되지 않았으므로, 언제든 프로세스가 내용을 변경할 수 있습니다. 내용이 변경되면 트리의 정렬 키(memcmp 결과)가 무효화(Invalidation)되어 검색 결과를 신뢰할 수 없습니다. 따라서 KSM은 전체 스캔을 한 바퀴 완료할 때마다 seqnr을 증가시키고, 이전 seqnr의 unstable 노드는 자연스럽게 무효 처리합니다.

비교 항목Stable TreeUnstable Tree
저장 대상병합 완료된 KSM 페이지병합 후보 페이지
write-protectYes (COW 보호)No
수명KSM 페이지가 존재하는 동안한 스캔 주기(seqnr)
검색 보장내용 불변 -> 검색 신뢰내용 변경 가능 -> false positive 가능
정렬 키memcmp 바이트 순서(Byte Order)memcmp 바이트 순서 (불안정)
/* unstable tree 검색 + 삽입 (간략화) */
static struct ksm_rmap_item *
unstable_tree_search_insert(struct ksm_rmap_item *rmap_item,
                            struct page *page)
{
    struct rb_node **new = &root_unstable_tree.rb_node;
    struct rb_node *parent = NULL;

    while (*new) {
        struct ksm_rmap_item *tree_rmap_item;
        struct page *tree_page;
        int ret;

        parent = *new;
        tree_rmap_item = rb_entry(*new, struct ksm_rmap_item, node);
        tree_page = get_mergeable_page(tree_rmap_item);

        ret = memcmp_pages(page, tree_page);
        put_page(tree_page);

        if (ret < 0)
            new = &parent->rb_left;
        else if (ret > 0)
            new = &parent->rb_right;
        else
            return tree_rmap_item;  /* 매치 발견! */
    }

    /* 매치 없음: 현재 항목을 트리에 삽입 */
    rb_link_node(&rmap_item->node, parent, new);
    rb_insert_color(&rmap_item->node, &root_unstable_tree);
    return NULL;
}

페이지 병합 흐름

두 페이지가 동일하다고 확인되면 KSM은 다음 과정으로 병합을 수행합니다.

페이지 병합 흐름 병합 전 Process A PTE Page X (PFN=100) Process B PTE Page Y (PFN=200) memcmp(X, Y) == 0 (내용 동일) 병합 후 (COW) A PTE (R/O) write-protect B PTE (R/O) write-protect KSM Page (PFN=300) PageKsm flag set Page X, Y 해제 -> free memory merge_with_ksm_page() / try_to_merge_two_pages() 내부 단계 1. KSM 페이지 할당 copy_user_highpage() 2. PTE 교체 replace_page() R/O 3. rmap 업데이트 stable_node hlist 추가 4. 원본 페이지 해제 put_page() refcount-- TLB Flush: 기존 PTE 캐시 무효화 (모든 CPU에 IPI) flush_tlb_page() -> IPI to all CPUs sharing mm

Zero Page 최적화

0으로 채워진 페이지는 가장 흔한 중복 패턴입니다. KSM은 이를 특별히 처리하여 커널의 전역 zero page(ZERO_PAGE(0))와 직접 병합합니다. 별도의 KSM 페이지를 할당할 필요가 없으므로 메모리 절약 효과가 추가됩니다.

/* zero page 감지 (간략화) */
if (pages_identical(page, ZERO_PAGE(0))) {
    /* KSM 페이지 할당 없이 바로 zero page로 교체 */
    replace_page(vma, page, ZERO_PAGE(0), pte);
    ksm_zero_pages++;   /* /sys/kernel/mm/ksm/zero_pages_sharing */
}

COW 분리

병합된 KSM 페이지에 프로세스가 쓰기를 시도하면 COW(Copy-On-Write) page fault가 발생합니다. 커널은 새 페이지를 할당하고 KSM 페이지의 내용을 복사한 후, 해당 프로세스의 PTE만 새 페이지를 가리키도록 변경합니다.

COW 분리 과정 쓰기 전 (공유 상태) A PTE R/O B PTE R/O KSM Page Process A 쓰기 시도 R/O PTE -> #PF (page fault) do_wp_page() -> PageKsm 확인 -> wp_page_copy() COW 분리 완료 A PTE R/W (new) New Page (복사본) B PTE R/O (유지) KSM Page (유지) KSM 페이지의 mapcount가 1이 되면 stable_node에서 제거되고 일반 페이지로 전환
COW 비용: COW 분리는 4KB 메모리 복사 + TLB flush + 새 페이지 할당을 수반합니다. 쓰기가 빈번한 워크로드에서 KSM을 사용하면 병합과 분리가 반복되어(thrashing), 오히려 CPU 오버헤드(Overhead)만 증가할 수 있습니다. KSM은 읽기 위주 워크로드에서 가장 효과적입니다.
/* mm/memory.c - COW page fault에서 KSM 페이지 처리 (간략화) */
static vm_fault_t do_wp_page(struct vm_fault *vmf)
{
    struct page *old_page = vmf->page;

    if (PageKsm(old_page)) {
        /* KSM 페이지는 항상 COW 수행 (재사용 불가)
         * 일반 COW와 달리 mapcount==1이라도 재사용 불가:
         * KSM 페이지는 stable tree에 속하며,
         * 내용이 변경되면 트리 정렬이 깨지기 때문 */
        return wp_page_copy(vmf);
    }
    /* ... 일반 COW 로직 ... */
}

wp_page_copy() KSM 처리 상세

wp_page_copy()는 KSM COW의 핵심 함수입니다. 새 페이지를 할당하고, KSM 페이지의 내용을 복사한 후, PTE를 교체합니다.

wp_page_copy() KSM COW 분리 단계 1. 페이지 할당 alloc_page_vma() NUMA 정책에 따라 로컬 노드에서 할당 2. 내용 복사 copy_user_highpage() 4KB memcpy (SIMD 최적화) 3. PTE 교체 ptep_clear_flush() set_pte_at_notify() R/W PTE 설정 4. rmap 업데이트 page_remove_rmap(ksm_page) page_add_anon_rmap(new_page) put_page(ksm_page) mapcount 변화와 stable_node 정리 mapcount > 1 (공유 유지) KSM 페이지는 다른 프로세스와 계속 공유됨 (stable_node 유지) mapcount == 1 (마지막 공유) KSM 페이지가 하나의 프로세스만 사용 → 다음 ksmd 스캔에서 제거 mapcount == 0 (완전 해제) 모든 프로세스가 COW 분리 → KSM 페이지 해제, stable_node 제거 fork() 시 KSM 페이지 처리 fork() → copy_page_range() → 자식 프로세스도 KSM 페이지를 R/O로 공유 KSM 페이지의 mapcount만 증가 (추가 물리 페이지 할당 없음) ksm_might_need_to_copy(): fork 후 자식이 쓰기 시, KSM 페이지인지 확인하여 일반 COW와 동일하게 wp_page_copy()로 처리 (새 일반 페이지 생성)
/* mm/ksm.c - fork 시 KSM 페이지 처리 */
struct page *ksm_might_need_to_copy(
    struct page *page,
    struct vm_area_struct *vma,
    unsigned long address)
{
    struct page *new_page;

    /* KSM 페이지가 아니면 원본 그대로 사용 */
    if (!PageKsm(page))
        return page;

    /* 자식 프로세스의 anon_vma가 아직 설정되지 않은 경우
     * fork 직후 KSM 페이지를 복사해야 할 수 있음 */
    if (page_stable_node(page) &&
        !(vma->vm_flags & VM_MERGEABLE)) {
        /* MERGEABLE이 아닌 VMA로 fork됨 → 복사 필요 */
        new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE,
                                   vma, address);
        if (new_page) {
            copy_user_highpage(new_page, page, address, vma);
            /* 새 페이지는 일반 익명 페이지 (PageKsm 아님) */
        }
        return new_page;
    }

    return page;  /* MERGEABLE VMA → 공유 유지 */
}
KSM + fork 최적화: fork() 시 KSM 페이지는 일반 COW 페이지와 동일하게 처리됩니다. 부모와 자식 모두 같은 KSM 페이지를 R/O로 공유하며, mapcount만 증가합니다. 자식이 exec()를 호출하면 주소 공간이 교체되어 KSM 매핑이 자연스럽게 해제됩니다. 이는 fork+exec 패턴에서 불필요한 페이지 복사를 방지하는 효율적인 설계입니다.

사용자 API

KSM에 메모리 영역을 등록하는 방법은 두 가지입니다.

madvise() 시스템 호출(System Call)

호출효과
madvise(addr, len, MADV_MERGEABLE)지정 영역을 KSM 스캔 대상으로 등록
madvise(addr, len, MADV_UNMERGEABLE)지정 영역의 KSM 등록 해제, 이미 병합된 페이지는 즉시 COW 분리
/* QEMU/KVM에서 게스트 메모리를 KSM에 등록하는 예시 */
void *guest_mem = mmap(NULL, guest_ram_size,
                       PROT_READ | PROT_WRITE,
                       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (guest_mem == MAP_FAILED)
    perror("mmap");

/* KSM 스캔 대상으로 등록 */
if (madvise(guest_mem, guest_ram_size, MADV_MERGEABLE) < 0)
    perror("madvise MADV_MERGEABLE");

prctl() 프로세스 전체 등록 (v6.1+)

Linux 6.1에서 추가된 PR_SET_MEMORY_MERGE프로세스의 모든 호환 가능한 익명 VMA에 자동으로 MADV_MERGEABLE을 적용합니다. 향후 생성되는 새 VMA에도 자동 적용됩니다.

#include <sys/prctl.h>

/* 프로세스 전체를 KSM 대상으로 등록 */
prctl(PR_SET_MEMORY_MERGE, 1, 0, 0, 0);

/* 등록 해제 */
prctl(PR_SET_MEMORY_MERGE, 0, 0, 0, 0);

/* 현재 상태 조회 */
int enabled = prctl(PR_GET_MEMORY_MERGE, 0, 0, 0, 0);
systemd 통합: systemd v254+에서는 서비스 유닛에 MemoryKSM=yes를 설정하면 자동으로 prctl(PR_SET_MEMORY_MERGE, 1)을 호출합니다. 대규모 컨테이너 환경에서 서비스 단위로 KSM을 제어할 수 있습니다.

/sys/kernel/mm/ksm/ 인터페이스

KSM의 모든 제어와 모니터링은 sysfs를 통해 수행됩니다.

제어 파라미터

파라미터기본값설명
run00=정지, 1=실행, 2=병합 해제(unmerge all)
pages_to_scan100ksmd가 한 주기에 스캔하는 최대 페이지 수
sleep_millisecs20스캔 주기 간 휴식 시간 (ms)
merge_across_nodes11=NUMA 노드 간 병합 허용, 0=같은 노드만
max_page_sharing256하나의 KSM 페이지를 공유할 수 있는 최대 매핑 수
use_zero_pages01=영값 페이지를 커널 zero page와 병합
advisor_modenonenone/scan-time/cpu-percent 자동 튜닝 모드
advisor_max_pages_to_scan30000advisor 모드 최대 pages_to_scan
advisor_min_pages_to_scan500advisor 모드 최소 pages_to_scan
advisor_max_cpu70cpu-percent 모드 최대 CPU 사용률 (%)
advisor_target_scan_time200scan-time 모드 목표 전체 스캔 시간 (초)

통계 카운터 (읽기 전용(Read-Only))

카운터설명
pages_shared현재 KSM 페이지 수 (stable tree 노드 수)
pages_sharingKSM 페이지를 공유하는 총 매핑 수 (절약된 페이지 수)
pages_unsharedunstable tree에 있는 후보 페이지 수
pages_volatile내용이 자주 변경되어 병합 불가한 페이지 수
zero_pages_sharing커널 zero page와 병합된 매핑 수
full_scans전체 스캔 완료 횟수 (seqnr)
stable_node_chainsmax_page_sharing 초과로 체인된 stable 노드 수
general_profitKSM으로 절약된 총 메모리 (바이트)
메모리 절약량 계산:
절약 페이지 = pages_sharing + zero_pages_sharing - pages_shared
절약 메모리 = 절약 페이지 * 4096 바이트
또는 general_profit 카운터를 직접 읽으면 rmap_item 오버헤드를 차감한 순 절약량을 얻을 수 있습니다.

병합 내부 구현 상세

KSM의 핵심 함수인 cmp_and_merge_page()는 하나의 후보 페이지를 stable tree와 unstable tree에서 순차적으로 검색하고 병합을 시도합니다. 이 함수의 내부 동작을 상세히 추적합니다.

try_to_merge_with_ksm_page()

stable tree에서 매치를 찾은 경우 호출됩니다. 후보 페이지의 PTE를 기존 KSM 페이지로 교체하는 핵심 로직입니다.

/* mm/ksm.c - try_to_merge_with_ksm_page() 핵심 (간략화) */
static int try_to_merge_with_ksm_page(
    struct ksm_rmap_item *rmap_item,
    struct page *page,           /* 후보 페이지 */
    struct page *kpage)          /* KSM 페이지 */
{
    struct mm_struct *mm = rmap_item->mm;
    struct vm_area_struct *vma;
    int err = -EFAULT;

    mmap_read_lock(mm);
    vma = find_mergeable_vma(mm, rmap_item->address);
    if (!vma)
        goto out;

    /* PTE 교체: page -> kpage (read-only) */
    err = try_to_merge_one_page(vma, page, kpage);
    if (err)
        goto out;

    /* rmap_item을 stable_node의 hlist에 연결 */
    stable_tree_append(rmap_item, page_stable_node(kpage),
                       max_page_sharing);

out:
    mmap_read_unlock(mm);
    return err;
}

try_to_merge_one_page() 상세

/* PTE를 KSM 페이지로 교체하는 핵심 함수 (간략화) */
static int try_to_merge_one_page(
    struct vm_area_struct *vma,
    struct page *page,
    struct page *kpage)
{
    pte_t orig_pte, new_pte;
    struct page *page2;

    /* 1. PTE lock 획득 */
    orig_pte = *pte_offset_map_lock(mm, pmd, addr, &ptl);

    /* 2. 교체 대상 페이지가 여전히 같은 페이지인지 확인 */
    page2 = vm_normal_page(vma, addr, orig_pte);
    if (page2 != page)
        goto out_unlock;  /* race condition: 이미 변경됨 */

    /* 3. write_protect_page: PTE를 read-only로 변경 */
    if (pte_write(orig_pte))
        set_pte_at(mm, addr, ptep, pte_wrprotect(orig_pte));

    /* 4. memcmp 최종 확인 (PTE 변경 후 내용이 같은지) */
    if (pages_identical(page, kpage)) {
        /* 5. PTE를 KSM 페이지로 교체 */
        new_pte = mk_pte(kpage, vma->vm_page_prot);
        new_pte = pte_wrprotect(new_pte);  /* R/O 강제 */
        new_pte = pte_mkold(new_pte);
        set_pte_at_notify(mm, addr, ptep, new_pte);

        /* 6. 원본 페이지의 mapcount 감소, KSM 페이지 mapcount 증가 */
        page_remove_rmap(page, vma, false);
        page_add_anon_rmap(kpage, vma, addr, RMAP_NONE);
    }

out_unlock:
    pte_unmap_unlock(ptep, ptl);
    return err;
}
Race condition 방어: KSM은 mmap_read_lock을 잡고 PTE를 확인하지만, 다른 스레드(Thread)가 동시에 같은 페이지에 쓰기를 시도할 수 있습니다. 따라서 write_protect_page()로 PTE를 먼저 read-only로 만든 후, pages_identical()로 한 번 더 내용을 확인합니다. 이 시점에서 다른 스레드가 쓰기를 하면 COW가 발생하여 다른 페이지가 되므로, 안전하게 병합할 수 있습니다.

두 후보 페이지의 병합

unstable tree에서 매치를 발견한 경우, 두 일반 페이지를 새 KSM 페이지로 병합합니다.

/* try_to_merge_two_pages() 핵심 흐름 */
static struct page *try_to_merge_two_pages(
    struct ksm_rmap_item *rmap_item,
    struct page *page,
    struct ksm_rmap_item *tree_rmap_item,
    struct page *tree_page)
{
    struct page *kpage;

    /* 1. 새 KSM 페이지 할당 */
    kpage = alloc_page(GFP_HIGHUSER);
    if (!kpage)
        return NULL;

    /* 2. 내용 복사 */
    copy_user_highpage(kpage, page, rmap_item->address, vma);

    /* 3. PageKsm 플래그 설정 */
    SetPageKsm(kpage);

    /* 4. 첫 번째 페이지의 PTE를 kpage로 교체 */
    err = try_to_merge_with_ksm_page(rmap_item, page, kpage);
    if (err)
        goto fail;

    /* 5. 두 번째 페이지(unstable tree 매치)의 PTE도 kpage로 교체 */
    err = try_to_merge_with_ksm_page(tree_rmap_item, tree_page, kpage);
    if (err) {
        /* 두 번째 실패 시 첫 번째도 롤백 */
        break_cow(rmap_item);
        goto fail;
    }

    /* 6. stable tree에 새 노드 삽입 */
    stable_tree_insert(kpage);

    return kpage;
fail:
    put_page(kpage);
    return NULL;
}

memcmp_pages() 구현

4KB 페이지의 바이트 단위 비교는 KSM에서 가장 빈번하게 호출되는 연산입니다. 커널은 kmap_local_page()로 페이지를 임시 매핑한 후 memcmp()를 수행합니다.

static int memcmp_pages(struct page *page1, struct page *page2)
{
    char *addr1, *addr2;
    int ret;

    addr1 = kmap_local_page(page1);
    addr2 = kmap_local_page(page2);
    ret = memcmp(addr1, addr2, PAGE_SIZE);  /* 4096 바이트 비교 */
    kunmap_local(addr2);
    kunmap_local(addr1);
    return ret;
}
SIMD 최적화: x86에서 memcmp()는 SSE/AVX 벡터 명령어로 최적화되어 있어, 4KB 비교는 일반적으로 1~2 마이크로초 내에 완료됩니다. 그러나 캐시(Cache) 미스가 발생하면(cold page) 수십 마이크로초까지 증가할 수 있습니다.

write_protect_page() 상세

KSM 병합의 핵심 전제 조건은 병합 대상 페이지의 내용이 병합 시점에도 동일해야 한다는 것입니다. write_protect_page()는 이를 보장하기 위해 PTE를 read-only로 변경하고, 이후 다른 스레드가 쓰기를 시도하면 COW fault가 발생하도록 합니다.

write_protect_page() Race Condition 방어 ksmd (병합 시도) Thread T (쓰기 시도) ① memcmp(page, kpage) == 0 (동일!) ② PTE lock 획득 (spin_lock) ③ pte_wrprotect(): R/W → R/O 변경 이 시점 이후 쓰기는 반드시 fault 발생 ← 위험 구간: ①~③ 사이에 쓰기 가능 쓰기 발생! → page 내용 변경됨 하지만 아직 PTE는 R/W ④ pages_identical(page, kpage) 재확인! R/O 변경 후 한 번 더 내용 비교 동일 → 병합 진행 PTE를 KSM으로 교체 다름 → 병합 취소 PTE를 R/W로 복원 Yes No 이후 쓰기 시도 → page fault 발생 → do_wp_page() → COW 분리 핵심: ③ R/O 전환 후 ④ 재확인으로 TOCTOU(Time-of-Check-to-Time-of-Use) 취약점 방지
/* mm/ksm.c - write_protect_page() 핵심 (간략화) */
static int write_protect_page(
    struct vm_area_struct *vma,
    struct page *page,
    pte_t *orig_pte)
{
    pte_t entry;
    int err = -EFAULT;

    /* PTE lock 획득 */
    entry = *pte_offset_map_lock(mm, pmd, addr, &ptl);

    /* 1. PTE가 여전히 같은 물리 페이지를 가리키는지 확인 */
    if (!pte_same(entry, *orig_pte))
        goto out_unlock;

    /* 2. 페이지가 이미 다른 곳에서 참조되는지 (dirty page) */
    if (pte_write(entry) || pte_dirty(entry)) {
        struct page *ref_page;
        ref_page = vm_normal_page(vma, addr, entry);
        if (ref_page != page)
            goto out_unlock;  /* race: 다른 페이지로 변경됨 */

        /* mapcount > 1이고 dirty이면 안전하지 않음 */
        if (page_mapcount(page) + 1 + PageSwapCache(page)
            != page_count(page))
            goto out_unlock;  /* 추가 참조 존재 */

        /* 3. R/O로 변경 + dirty 비트 클리어 */
        entry = pte_mkclean(pte_wrprotect(entry));
        set_pte_at_notify(mm, addr, ptep, entry);
    }

    *orig_pte = entry;
    err = 0;

out_unlock:
    pte_unmap_unlock(ptep, ptl);
    return err;
}
mapcount + refcount 검증: write_protect_page()는 단순히 PTE를 R/O로 바꾸는 것이 아니라, page_mapcountpage_count의 관계를 검증하여 다른 참조(GUP pin, swap cache 등)가 없는지 확인합니다. 예를 들어 get_user_pages()로 직접 접근 중인 페이지를 병합하면 데이터 손상이 발생할 수 있으므로, 이런 경우 병합을 거부합니다.

KSM 페이지 마이그레이션

커널의 페이지 마이그레이션(page migration)은 NUMA 밸런싱, 메모리 핫플러그, compaction 등에서 물리 페이지를 이동시킵니다. KSM 페이지는 여러 프로세스가 공유하므로 마이그레이션 시 특별한 처리가 필요합니다.

KSM 페이지 마이그레이션 흐름 마이그레이션 전 (NUMA Node 0) stable_node (kpfn=0x1000) rmap_hlist_len=3 PTE A→0x1000 PTE B→0x1000 PTE C→0x1000 KSM Page (PFN=0x1000) 마이그레이션 후 (NUMA Node 1) stable_node (kpfn=0x5000) kpfn 업데이트됨 PTE A→0x5000 PTE B→0x5000 PTE C→0x5000 KSM Page (PFN=0x5000) ksm_migrate_page() 처리 단계 1. 새 페이지 할당 대상 NUMA 노드에서 2. 내용 복사 copy_highpage() 3. stable_node 갱신 kpfn = new_pfn 4. 모든 PTE 갱신 rmap_walk + TLB flush KSM 페이지는 PageKsm 플래그로 식별 → rmap_walk가 stable_node의 hlist를 순회하여 모든 PTE 갱신 merge_across_nodes=0이고 NUMA 이동 시 → stable tree에서 제거 후 대상 노드의 stable tree에 재삽입
/* mm/ksm.c - ksm_migrate_page() (간략화) */
void ksm_migrate_page(struct page *newpage,
                      struct page *oldpage)
{
    struct ksm_stable_node *stable_node;

    /* KSM 페이지가 아니면 아무것도 하지 않음 */
    if (!PageKsm(oldpage))
        return;

    stable_node = page_stable_node(oldpage);
    if (stable_node) {
        /* stable_node의 kpfn을 새 페이지로 업데이트 */
        stable_node->kpfn = page_to_pfn(newpage);

        /* NUMA-aware: 노드 변경 시 nid 업데이트 */
        stable_node->nid = page_to_nid(newpage);

        /* 새 페이지에 stable_node 연결 */
        set_page_stable_node(newpage, stable_node);

        /* PageKsm 플래그 이전 */
        SetPageKsm(newpage);
        ClearPageKsm(oldpage);
    }
    /* PTE 갱신은 migrate_page_move_mapping()에서
     * rmap_walk()를 통해 자동으로 처리 */
}
마이그레이션과 merge_across_nodes: merge_across_nodes=0인 환경에서 NUMA 밸런싱이 KSM 페이지를 다른 노드로 이동시키면, 해당 페이지는 원래 노드의 stable tree에서 제거되고 새 노드의 stable tree에 재삽입됩니다. 이 과정에서 기존 공유 관계가 해제될 수 있으므로, NUMA 밸런싱과 KSM을 동시에 사용할 때는 merge_across_nodes=1이 더 안정적입니다.

KSM과 mlock/mprotect 상호작용

KSM 페이지가 mlock()으로 잠기거나 mprotect()로 권한이 변경될 때의 동작을 이해해야 합니다.

작업KSM 페이지 영향상세
mlock()KSM 페이지 유지mlock된 KSM 페이지는 LRU에서 제외되어 회수/스왑 대상에서 제외. 병합 상태는 유지됨
munlock()정상 복귀KSM 페이지가 다시 LRU에 복귀하여 회수 대상이 됨
mprotect(PROT_WRITE)COW 가능 상태PTE는 여전히 R/O (KSM COW 보호). 쓰기 시 COW fault 발생
mprotect(PROT_NONE)접근 불가PTE가 접근 불가로 변경. KSM 병합은 유지되지만 접근 시 fault
mprotect(PROT_READ)정상 읽기KSM 페이지를 읽기 전용으로 접근 (COW 보호와 일치, 자연스러운 조합)
MADV_UNMERGEABLE즉시 COW 분리해당 영역의 모든 KSM 페이지를 개별 페이지로 분리. 추가 메모리 필요
/* mprotect()에서 KSM 페이지의 PTE 권한 변경 */
/* mm/mprotect.c - change_pte_range() (간략화) */
if (PageKsm(page)) {
    /* KSM 페이지는 mprotect(PROT_WRITE)를 해도
     * PTE에 write 비트를 직접 설정하지 않음.
     * VMA의 vm_page_prot만 변경하여
     * 향후 COW 후 새 페이지의 PTE에 적용.
     *
     * 이유: write 비트를 설정하면 COW 없이
     * KSM 페이지에 직접 쓸 수 있게 되어
     * 다른 프로세스의 데이터가 손상됨 */
    if (pte_write(newpte))
        newpte = pte_wrprotect(newpte);
}

스왑과 KSM 상호작용

KSM 페이지가 스왑 아웃되면 특별한 처리가 필요합니다. 병합된 KSM 페이지는 여러 프로세스의 PTE가 가리키고 있으므로, 스왑 시에도 공유 관계를 유지해야 합니다.

스왑 아웃 시 동작

단계동작설명
1KSM 페이지 선택kswapd/direct reclaim이 KSM 페이지를 스왑 대상으로 선택
2스왑 엔트리 할당스왑 슬롯 하나를 할당 (ksm_count 참조)
3PTE 교체모든 공유 PTE를 swap entry로 교체
4페이지 기록페이지 내용을 스왑 디바이스에 한 번만 기록
5stable_node 유지스왑 아웃되어도 stable_node는 유지 (kpfn 대신 swap entry 저장)

스왑 인 시 동작

스왑 인 시에는 두 가지 경로가 있습니다:

/* 스왑 인 시 KSM 페이지 복원 경로 */

/* 경로 1: 첫 번째 접근 - 스왑에서 읽어서 새 페이지 생성 */
/* -> 이 페이지가 새로운 KSM 페이지가 됨 */
/* -> stable_node의 kpfn을 새 페이지로 업데이트 */

/* 경로 2: 이후 접근 - 이미 스왑 인된 KSM 페이지 존재 */
/* -> swap_cache에서 KSM 페이지를 찾아 공유 */
/* -> 추가 디스크 I/O 없음 */

/* ksm_swpin_copy: 스왑 인 시 COW가 필요한 경우 카운터 */
/* (KSM 페이지가 이미 COW 분리된 상태에서 스왑 인) */
ksm_swpin_copy 카운터: /proc/vmstatksm_swpin_copy는 스왑 인 시 KSM 페이지를 복원하면서 COW 복사가 발생한 횟수입니다. 이 값이 높으면 KSM 병합 후 쓰기가 빈번하여 비효율적임을 의미합니다.

cgroup 연동

cgroup v2에서는 KSM 관련 통계를 메모리 컨트롤러를 통해 확인할 수 있습니다.

메모리 cgroup KSM 통계

# cgroup v2에서 KSM 통계 확인
cat /sys/fs/cgroup/my-container/memory.stat | grep ksm
# ksm 12345678    -- 이 cgroup에서 KSM으로 공유 중인 메모리 (바이트)

# cgroup별 KSM 효과 비교
for cg in /sys/fs/cgroup/*/; do
    ksm=$(grep "^ksm " $cg/memory.stat 2>/dev/null | awk '{print $2}')
    if [ -n "$ksm" ] && [ "$ksm" -gt 0 ]; then
        echo "$(basename $cg): $(( ksm / 1024 )) KB shared via KSM"
    fi
done

KSM과 메모리 제한(memory.max)

KSM으로 병합된 페이지는 cgroup의 메모리 사용량 계산에서 공유 비율에 따라 분담됩니다. 하나의 KSM 페이지를 N개의 cgroup이 공유하면, 각 cgroup에 1/N씩 과금됩니다.

상황cgroup A 과금cgroup B 과금총 물리 메모리
병합 전 (각 4KB)4KB4KB8KB
KSM 병합 후2KB (1/2)2KB (1/2)4KB
COW 분리 후 (A가 쓰기)4KB4KB8KB
cgroup OOM과 KSM: KSM으로 절약된 메모리에 의존하여 memory.max를 낮게 설정하면, 워크로드 변경으로 COW 분리가 발생할 때 cgroup OOM이 트리거될 수 있습니다. memory.max 설정 시 KSM 절약분의 30% 정도를 여유로 두는 것이 안전합니다.

ksmtuned 자동 튜너

Red Hat 계열 배포판에서 제공하는 ksmtuned는 시스템 메모리 상태에 따라 KSM 파라미터를 자동으로 조절하는 데몬입니다.

ksmtuned.conf 설정

# /etc/ksmtuned.conf (Red Hat/CentOS/Fedora)

# KSM 임계값 (여유 메모리가 이 값 이하이면 공격적 스캔)
KSM_THRES_COEF=20     # 전체 메모리의 20% 이하이면 활성화
KSM_THRES_CONST=2048  # 또는 2048MB 이하이면 활성화

# 스캔 속도 조절
NPAGES_MIN=64          # 최소 pages_to_scan
NPAGES_MAX=1250        # 최대 pages_to_scan
NPAGES_BOOST=300       # 메모리 부족 시 증가량
NPAGES_DECAY=-50       # 메모리 충분 시 감소량

# 모니터링 주기
MONITOR_INTERVAL=60    # 60초마다 메모리 상태 확인
SLEEP_MSEC=10          # ksmd sleep_millisecs

ksmtuned 동작 논리

  1. 메모리 부족 감지: 여유 메모리가 KSM_THRES_COEF% 이하이면 pages_to_scanNPAGES_BOOST만큼 증가시킵니다.
  2. 메모리 충분: 여유 메모리가 충분하면 pages_to_scanNPAGES_DECAY만큼 감소시킵니다.
  3. 범위 제한: NPAGES_MIN~NPAGES_MAX 범위를 초과하지 않습니다.
  4. 비활성화: 메모리가 매우 충분하면 run=0으로 ksmd를 정지시킵니다.
ksmtuned vs advisor_mode: 커널 6.4+를 사용한다면 ksmtuned 대신 커널 내장 advisor_mode를 사용하는 것이 권장됩니다. advisor_mode는 유저스페이스 폴링(Polling) 없이 커널 내부에서 직접 조절하므로 반응 속도가 빠르고 오버헤드가 적습니다.

디버깅(Debugging)과 추적

ftrace 이벤트

# KSM ftrace 이벤트 활성화
echo 1 > /sys/kernel/debug/tracing/events/ksm/enable

# 가용 이벤트 확인
ls /sys/kernel/debug/tracing/events/ksm/
# ksm_start_scan  ksm_stop_scan  ksm_enter  ksm_exit
# ksm_merge_one_page  ksm_merge_with_ksm_page
# ksm_remove_ksm_page  ksm_remove_rmap_item

# 실시간 추적
cat /sys/kernel/debug/tracing/trace_pipe
# ksmd-42  [001] ..... 12345.678: ksm_merge_one_page: pfn=0x1a2b3 rmap_item=0xffff...
# ksmd-42  [001] ..... 12345.679: ksm_merge_with_ksm_page: ksm_pfn=0x3c4d5 sharing=47

종합 진단 스크립트

#!/bin/bash
# KSM 종합 진단 스크립트

echo "=== KSM Status ==="
echo "Run state: $(cat /sys/kernel/mm/ksm/run)"
echo

echo "=== Sharing Statistics ==="
shared=$(cat /sys/kernel/mm/ksm/pages_shared)
sharing=$(cat /sys/kernel/mm/ksm/pages_sharing)
unshared=$(cat /sys/kernel/mm/ksm/pages_unshared)
volatile=$(cat /sys/kernel/mm/ksm/pages_volatile)
zero=$(cat /sys/kernel/mm/ksm/zero_pages_sharing)

echo "KSM pages (unique content): $shared"
echo "Sharing (total mappings):   $sharing"
echo "Unshared (candidates):      $unshared"
echo "Volatile (changing):        $volatile"
echo "Zero pages merged:          $zero"
echo

# 효율성 계산
if [ "$shared" -gt 0 ]; then
    saved=$(( (sharing - shared + zero) * 4 ))
    ratio=$(( sharing / shared ))
    echo "=== Efficiency ==="
    echo "Memory saved: ${saved} KB ($(( saved / 1024 )) MB)"
    echo "Avg sharing ratio: ${ratio}:1"
    echo "General profit: $(cat /sys/kernel/mm/ksm/general_profit) bytes"
fi
echo

echo "=== Scan Progress ==="
echo "Full scans completed: $(cat /sys/kernel/mm/ksm/full_scans)"
echo "pages_to_scan: $(cat /sys/kernel/mm/ksm/pages_to_scan)"
echo "sleep_millisecs: $(cat /sys/kernel/mm/ksm/sleep_millisecs)"
echo

echo "=== CPU Usage ==="
ps -C ksmd -o pid,%cpu,%mem,etime --no-headers
echo

echo "=== Top KSM Processes ==="
for pid in /proc/[0-9]*/ksm_stat; do
    p=$(dirname $pid | xargs basename)
    profit=$(grep ksm_process_profit $pid 2>/dev/null | awk '{print $2}')
    if [ -n "$profit" ] && [ "$profit" -gt 0 ]; then
        name=$(cat /proc/$p/comm 2>/dev/null)
        echo "  PID $p ($name): profit=$(( profit / 1024 )) KB"
    fi
done 2>/dev/null | sort -t= -k2 -n -r | head -10

자주 발생하는 문제와 해결

문제원인해결 방법
KSM이 동작하지 않음run=0 또는 CONFIG_KSM=necho 1 > /sys/kernel/mm/ksm/run, 커널 재빌드
pages_sharing이 0인 채 유지대상 프로세스가 MADV_MERGEABLE 미호출prctl(PR_SET_MEMORY_MERGE) 또는 madvise 호출 추가
수렴이 매우 느림pages_to_scan이 너무 낮음값 증가 (예: 100 -> 1000) 또는 advisor_mode 사용
ksmd CPU 100% 사용등록 영역이 거대하고 pages_to_scan이 높음advisor_mode=cpu-percent, advisor_max_cpu=10
갑작스러운 메모리 증가대량 COW 분리 (게스트 워크로드 변경)여유 메모리 확보, OOM 방지 설정 확인
NUMA 성능 저하merge_across_nodes=1로 원격 노드 접근 발생merge_across_nodes=0 설정

가상화 환경 활용

KSM의 가장 대표적인 활용처는 KVM/QEMU 기반 가상화입니다. 동일한 게스트 OS 이미지로 다수의 VM을 실행하면, 커널 코드, 공유 라이브러리(Shared Library), 초기화된 데이터 영역에서 대량의 중복 페이지가 발생합니다.

KVM + KSM 메모리 절약 시나리오 VM-1 (2GB) Ubuntu 22.04 nginx + PHP glibc, kernel... VM-2 (2GB) Ubuntu 22.04 nginx + PHP glibc, kernel... ... VM-50 (2GB) Ubuntu 22.04 nginx + PHP glibc, kernel... KSM 병합 결과 공유 라이브러리: 1 copy 커널 코드: 1 copy zero pages: 1 copy 49 copies 절약/항목 메모리 사용량 비교 (50 VMs x 2GB) KSM 미사용: 100 GB 필요 KSM 사용: ~40-60 GB (40-60% 절감) QEMU/KVM KSM 활성화 설정 echo 1 > /sys/kernel/mm/ksm/run qemu -m 2G -mem-merge on ... (기본 on)

KVM/QEMU 설정

# KSM 활성화
echo 1 > /sys/kernel/mm/ksm/run

# 스캔 속도 조절 (VM 50대 기준 권장)
echo 1000 > /sys/kernel/mm/ksm/pages_to_scan
echo 20 > /sys/kernel/mm/ksm/sleep_millisecs

# zero page 최적화 활성화
echo 1 > /sys/kernel/mm/ksm/use_zero_pages

# QEMU: mem-merge는 기본 on (명시적 비활성화)
qemu-system-x86_64 -m 2G -mem-merge on ...
# 또는 libvirt XML:
# <memoryBacking><nosharepages/></memoryBacking>  -- KSM 비활성화
Overcommit 주의: KSM으로 절약된 메모리에 의존하여 VM을 과다 배치하면, 게스트 워크로드 변경 시 COW 분리가 대량 발생하여 메모리 부족 → OOM이 될 수 있습니다. 최소 20-30% 여유 메모리를 확보하세요.

컨테이너 환경 활용

컨테이너는 VM보다 메모리 공유 기회가 적지만(커널은 이미 공유), 동일 베이스 이미지에서 실행되는 수백 개의 컨테이너에서 런타임 데이터, 힙 초기화 영역, 언어 런타임 구조체 등의 중복이 발생할 수 있습니다.

시나리오KSM 효과권장 여부
동일 Java 앱 100개 PodJVM 메타데이터, 클래스 로더(Loader) 영역 병합효과적
동일 Node.js 앱 100개 PodV8 힙 초기 구조 병합중간 효과
이기종 앱 혼합공통 부분 적어 비용 대비 효과 낮음비권장
데이터베이스 컨테이너버퍼(Buffer) 풀이 고유 데이터 -> 병합 기회 적음비권장
# Kubernetes 노드에서 KSM 활성화 (systemd 서비스)
# /etc/systemd/system/ksm.service
[Unit]
Description=KSM activation
After=multi-user.target

[Service]
Type=oneshot
ExecStart=/bin/bash -c 'echo 1 > /sys/kernel/mm/ksm/run; echo 1000 > /sys/kernel/mm/ksm/pages_to_scan'
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
cgroup과 KSM: cgroup v2의 memory.stat에서 ksm 항목을 통해 그룹별 KSM 병합 현황을 확인할 수 있습니다. Kubernetes에서는 노드 레벨에서만 KSM을 제어하며, 개별 Pod 단위 제어는 지원되지 않습니다.

NUMA-aware KSM

NUMA 시스템에서 KSM 병합은 메모리 접근 지연(Latency) 시간에 영향을 줍니다. 서로 다른 NUMA 노드의 페이지를 병합하면, 한쪽 프로세스는 원격 노드 접근(remote access) 지연을 겪게 됩니다.

NUMA-aware KSM 비교 merge_across_nodes=1 (기본) NUMA Node 0 VM-1 pages VM-3 pages NUMA Node 1 VM-2 pages VM-4 pages Global Stable Tree + 메모리 절약 최대화 - Remote access 지연 발생 지연 민감 워크로드에 부적합 merge_across_nodes=0 NUMA Node 0 VM-1 pages VM-3 pages NUMA Node 1 VM-2 pages VM-4 pages Node 0 Stable Tree Node 1 Stable Tree + Local access 보장 - 메모리 절약 감소 (노드 내 중복만) 지연 민감 워크로드에 적합
# NUMA-aware KSM 설정
# 같은 노드 내에서만 병합 (지연 시간 민감 워크로드)
echo 0 > /sys/kernel/mm/ksm/merge_across_nodes

# NUMA 노드별 KSM 상태 확인
cat /sys/devices/system/node/node*/meminfo | grep KSM

THP와 KSM 상호작용

THP(Transparent Huge Pages)와 KSM은 서로 상충하는 관계입니다.

특성THPKSM
페이지 크기2MB (PMD 레벨)4KB (PTE 레벨)
최적화 대상TLB 미스 감소메모리 사용량 감소
메모리 효과내부 단편화(Fragmentation) 증가 가능물리 페이지 수 감소
공존 방식KSM은 THP를 4KB로 분할(split) 후 병합

KSM이 THP 영역의 페이지를 병합하려면 먼저 2MB huge page를 512개의 4KB 페이지로 분할해야 합니다. 이 분할(split)은 비용이 크고, THP의 TLB 미스 감소 효과를 상쇄합니다.

THP + KSM 조합 주의: THP를 always로 설정한 상태에서 KSM을 활성화하면, khugepaged가 THP로 승격시킨 페이지를 KSM이 다시 분할하는 싸움(fight)이 발생할 수 있습니다. 일반적으로 KSM 환경에서는 THP를 madvise 모드로 설정하는 것이 권장됩니다.
# KSM 환경에서 권장 THP 설정
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
# THP가 필요한 앱만 명시적으로 MADV_HUGEPAGE 사용

보안 이슈

KSM은 side-channel 공격의 벡터가 될 수 있습니다. COW 분리 시 발생하는 page fault의 시간 차이를 측정하여, 다른 프로세스(또는 VM)의 메모리 내용을 추론할 수 있습니다.

KSM Side-Channel 공격 원리 공격자 VM 1. 추측 데이터로 페이지 내용 설정 2. MADV_MERGEABLE ksmd 병합 시도 공격자 페이지 == 피해자 페이지? 동일하면 -> COW 공유! (pages_sharing 증가) 피해자 VM 비밀 데이터 보유 (암호키, 인증서 등) 3. 타이밍 측정 (쓰기 접근 시간) 병합 됨: 쓰기 시 COW fault -> 느림 (~수 us) 병합 안 됨: 쓰기 시 정상 접근 -> 빠름 (~수십 ns) 결론: 시간 차이 = 데이터 일치 여부 -> 1비트 오라클 반복하면 바이트 단위로 비밀 데이터 복원 가능 (Suzaki et al., 2011)

알려진 공격

공격발표원리
Memory Disclosure via KSMSuzaki et al., 2011KSM 병합 타이밍으로 VM 간 데이터 추론
Flush+Reload on KSMGruss et al., 2015캐시 타이밍 + KSM 병합 결합 공격
CAIN AttackXiao et al., 2016VM 간 공유 라이브러리 탐지 -> 취약점(Vulnerability) 공격 대상 식별
Dedup Est MachinaBosman et al., 2016JavaScript에서 KSM 병합 탐지 -> ASLR 우회

완화 방법

클라우드 환경 주의: 멀티 테넌트 환경에서 KSM은 테넌트 간 정보 유출 위험이 있습니다. 단일 테넌트 환경(전용 서버, 베어메탈)이나 신뢰할 수 있는 VM 그룹 내에서만 KSM을 사용하는 것이 안전합니다.

CSP별 KSM 정책

CSP/하이퍼바이저(Hypervisor)기본 정책사유
AWS (Xen/Nitro)KSM 비활성화테넌트 격리, side-channel 방지
GCP (KVM)KSM 비활성화보안 우선, 전용 호스트에서만 선택적 허용
Azure (Hyper-V)TPS 비활성화2014년 이후 보안 업데이트에서 기본 비활성화
VMware vSphereTPS inter-VM 비활성화 (6.0+)VM 내(intra-VM)만 허용, inter-VM은 salt 필요
KVM (자체 호스팅)사용자 선택단일 테넌트 환경에서 활성화 권장
Proxmox VEKSM 비활성화 (7.0+)기본 비활성화, 사용자 명시적 활성화 필요

보안 강화 설정

# 보안 강화: KSM 완전 비활성화 + 커널 파라미터
echo 0 > /sys/kernel/mm/ksm/run

# 이미 병합된 페이지도 해제 (여유 메모리 확인 후)
echo 2 > /sys/kernel/mm/ksm/run
# 완료 후 정지
echo 0 > /sys/kernel/mm/ksm/run

# 부팅 시 KSM 비활성화 보장 (systemd tmpfiles)
# /etc/tmpfiles.d/ksm-disable.conf
w /sys/kernel/mm/ksm/run - - - - 0

# QEMU에서 KSM 비활성화
qemu-system-x86_64 -mem-merge off ...

# libvirt에서 KSM 비활성화
# <memoryBacking><nosharepages/></memoryBacking>

성능 특성

KSM은 CPU 시간을 메모리로 교환하는 트레이드오프입니다. ksmd는 페이지 내용을 읽고 해시/비교하므로 CPU 사용량이 증가하고, 메모리 버스(Bus) 대역폭(Bandwidth)을 소비합니다.

KSM 성능 트레이드오프 CPU 비용 (지불) - 페이지 내용 해싱 (CRC32) - memcmp() 4KB 바이트 비교 - COW fault + 페이지 복사 - rbtree 삽입/검색 O(log n) 메모리 이득 (보상) + 중복 페이지 제거 (GBs 절약) + VM/컨테이너 밀도 증가 + 스왑 압력 감소 + zero page 최적화 최적 균형점 찾기 pages_to_scan 높이면: CPU 사용 증가, 수렴 속도 빠름 pages_to_scan 낮추면: CPU 사용 감소, 수렴 속도 느림 advisor_mode=scan-time 또는 cpu-percent로 자동 조절 권장 (v6.4+)

오버헤드 요소

요소비용비고
rmap_item 메모리~64 바이트/페이지KSM에 등록된 모든 페이지에 할당
stable_node 메모리~64 바이트/고유 페이지병합된 KSM 페이지마다 하나
해시 계산~1-2 us/페이지CRC32, 4KB 전체 읽기
memcmp~0.5-1 us/페이지4KB 바이트 비교, 캐시 미스 포함
COW fault~5-10 us/페이지페이지 할당 + 복사 + TLB flush
rbtree 검색O(log n)수백만 노드에서도 ~20회 비교
스캔 속도 계산: pages_to_scan=1000, sleep_millisecs=20이면, 초당 약 50,000 페이지(200MB)를 스캔합니다. 8GB 등록 메모리의 전체 스캔에 약 160초가 걸립니다. 실제 병합은 수 회 전체 스캔 후 수렴합니다.

모니터링

/sys/kernel/mm/ksm/ 실시간 확인

# KSM 상태 한눈에 보기
for f in /sys/kernel/mm/ksm/*; do
    echo "$(basename $f): $(cat $f)"
done

# 핵심 지표만 모니터링 (1초 간격)
watch -n1 'echo "shared: $(cat /sys/kernel/mm/ksm/pages_shared)";
echo "sharing: $(cat /sys/kernel/mm/ksm/pages_sharing)";
echo "unshared: $(cat /sys/kernel/mm/ksm/pages_unshared)";
echo "volatile: $(cat /sys/kernel/mm/ksm/pages_volatile)";
echo "profit: $(cat /sys/kernel/mm/ksm/general_profit) bytes";
echo "full_scans: $(cat /sys/kernel/mm/ksm/full_scans)"'

프로세스별 KSM 통계 (v6.7+)

# 특정 프로세스의 KSM 현황
cat /proc/1234/ksm_stat
# ksm_rmap_items 12345     -- 등록된 rmap_item 수
# ksm_zero_pages  678      -- zero page 병합 수
# ksm_merging_pages 5432   -- 병합된 페이지 수
# ksm_process_profit 22282240  -- 프로세스 순 절약 (바이트)

# smaps에서도 KSM 확인 가능
grep -i ksm /proc/1234/smaps_rollup
# Ksm:              21248 kB

vmstat과 event 추적

# vmstat KSM 관련 카운터
grep -i ksm /proc/vmstat
# ksm_swpin_copy 0       -- 스왑 인 시 KSM COW 복사 횟수
# cow_ksm 1234           -- KSM COW fault 총 횟수

# ftrace로 KSM 이벤트 추적
echo 1 > /sys/kernel/debug/tracing/events/ksm/enable
cat /sys/kernel/debug/tracing/trace_pipe

튜닝 가이드

워크로드별 권장 설정

워크로드pages_to_scansleep_millisecsuse_zero_pagesmerge_across_nodes
KVM VM 10대 (동일 OS)5002011
KVM VM 50대 이상20002011
NUMA 서버 (지연 민감)10002010
컨테이너 100+ (동종)10005011
보안 민감 환경KSM 비활성화 (run=0)

Advisor 모드 (v6.4+)

Advisor 모드는 KSM이 자동으로 pages_to_scan을 조절하여 목표 시간 또는 CPU 사용률을 달성하도록 합니다.

# scan-time 모드: 전체 스캔을 200초 내에 완료하도록 자동 조절
echo scan-time > /sys/kernel/mm/ksm/advisor_mode
echo 200 > /sys/kernel/mm/ksm/advisor_target_scan_time

# cpu-percent 모드: ksmd CPU 사용률을 10% 이하로 유지
echo cpu-percent > /sys/kernel/mm/ksm/advisor_mode
echo 10 > /sys/kernel/mm/ksm/advisor_max_cpu

# advisor가 조절하는 범위 설정
echo 500 > /sys/kernel/mm/ksm/advisor_min_pages_to_scan
echo 30000 > /sys/kernel/mm/ksm/advisor_max_pages_to_scan

튜닝 체크리스트

  1. 효과 확인 -- pages_sharingpages_unshared보다 현저히 적으면 KSM이 비효율적입니다. pages_volatile이 높으면 워크로드가 쓰기 집약적이므로 KSM 비활성화를 고려합니다.
  2. 수렴 시간 확인 -- full_scans가 3~5회 이상 진행되어야 병합이 수렴합니다. pages_to_scan을 높여 수렴을 앞당길 수 있습니다.
  3. CPU 모니터링 -- top에서 ksmd CPU 사용률을 확인합니다. 5% 이상이면 pages_to_scan을 줄이거나 advisor_mode를 활성화합니다.
  4. 메모리 오버헤드 확인 -- general_profit이 음수면 rmap_item 오버헤드가 절약량을 초과합니다. KSM 대상 영역을 줄이세요.

유사 기술 비교

KSM 외에도 메모리 중복 제거(deduplication)를 수행하는 기술들이 있습니다.

기술레벨대상방식장단점
KSM커널 (mm)익명 페이지콘텐츠 비교 + COW범용적, CPU 오버헤드
UKSM커널 패치(Patch)모든 페이지적응형 해싱더 빠른 수렴, 메인라인 미포함
VMware TPS하이퍼바이저VM 메모리해시 + COW게스트 수정 불필요, 보안 기본 비활성화
Hyper-V DMM하이퍼바이저VM 메모리동적 메모리ballooning 기반, 중복 제거 아님
Xen TMEM하이퍼바이저VM 메모리페이지 그랜트명시적 API, 투명하지 않음
zswap커널 (mm)스왑 페이지압축중복 제거가 아닌 압축, 보완적
ZRAM블록 디바이스스왑압축KSM과 조합 가능

KSM vs UKSM (Ultra-KSM)

UKSM은 KSM의 개선 버전으로, 다음과 같은 차이점이 있습니다:

KSM + zswap/ZRAM 조합

KSM과 메모리 압축(Memory Compaction) 기술은 서로 보완적입니다. KSM이 먼저 동일한 페이지를 제거하고, 나머지 고유 페이지 중 스왑 대상을 zswap이 압축합니다. 두 기술을 조합하면 메모리 효율을 극대화할 수 있습니다.

일반적인 처리 순서:

  1. KSM 병합: 동일 내용 페이지를 하나로 통합 (중복 제거)
  2. 페이지 회수: 여유 메모리 부족 시 LRU 기반 회수
  3. zswap 압축: 스왑 아웃 대상 페이지를 메모리 내 압축 저장
  4. 스왑 기록: zswap 풀 초과 시 디스크로 기록
# KSM + zswap 조합 설정
# 1. KSM으로 중복 페이지 병합
echo 1 > /sys/kernel/mm/ksm/run

# 2. zswap으로 스왑 아웃 시 압축
echo 1 > /sys/module/zswap/parameters/enabled
echo lz4 > /sys/module/zswap/parameters/compressor
echo 20 > /sys/module/zswap/parameters/max_pool_percent

# 효과: KSM이 중복 제거 -> 나머지를 zswap이 압축
# 결과: 메모리 효율 극대화 (2~3배 이상 절약 가능)

커널 빌드 옵션

옵션기본값설명
CONFIG_KSMy (대부분 배포판)KSM 서브시스템 활성화
CONFIG_KSM_ADVISORy (v6.4+)KSM advisor 자동 튜닝 지원
# 현재 커널에서 KSM 지원 확인
zgrep CONFIG_KSM /proc/config.gz
# CONFIG_KSM=y

# 또는
grep CONFIG_KSM /boot/config-$(uname -r)

# KSM sysfs 디렉토리 존재 확인
ls /sys/kernel/mm/ksm/
# run  pages_to_scan  sleep_millisecs  ...
배포판 기본 상태: 대부분의 배포판은 CONFIG_KSM=y로 커널을 빌드하지만, ksmd는 run=0(정지)으로 시작합니다. 명시적으로 echo 1 > /sys/kernel/mm/ksm/run을 실행해야 합니다. Red Hat 계열은 ksmksmtuned 서비스를 제공합니다.

내부 자료구조

ksm_scan 커서

ksm_scan 전역 변수는 ksmd의 현재 스캔 위치를 기억합니다. ksmd가 휴식 후 재개할 때 이 커서에서부터 스캔을 계속합니다.

struct ksm_scan {
    struct ksm_mm_slot *mm_slot;    /* 현재 스캔 중인 mm_slot */
    unsigned long address;          /* 현재 스캔 가상 주소 */
    struct ksm_rmap_item **rmap_list;/* 현재 rmap_item 리스트 위치 */
    unsigned long seqnr;            /* 전체 스캔 완료 횟수 */
};

rmap_item 상태 전이

ksm_rmap_item은 KSM 스캔 과정에서 여러 상태를 거칩니다. address 필드의 상위 비트를 플래그로 사용하여 현재 상태를 구분합니다.

rmap_item 상태 전이 다이어그램 NEW 할당 직후, 플래그 없음 CHECKSUM oldchecksum 설정, 다음 스캔 대기 UNSTABLE UNSTABLE_FLAG | seqnr, unstable tree 노드 STABLE STABLE_FLAG, stable_node hlist 연결 INVALID (stale) seqnr 불일치, 다음 접근 시 제거 FREED mm_struct 소멸 또는 UNMERGEABLE 첫 스캔: checksum 저장 내용 변경 (oldchecksum 갱신) checksum 안정 + stable 매치 없음 stable tree 매치 발견! unstable tree에서 매치 → 새 KSM 페이지 seqnr 증가 (전체 스캔 완료) 다음 스캔 접근 시 재시작 COW로 내용 변경 프로세스 종료 프로세스 종료
상태플래그의미위치
NEW없음할당 직후, 아직 스캔되지 않음rmap_list에만 존재
CHECKSUMSEQNR_MASKoldchecksum이 설정됨, 다음 스캔에서 비교 예정rmap_list
UNSTABLEUNSTABLE_FLAG | seqnrunstable tree에 삽입됨, rb_node로 연결unstable tree + rmap_list
STABLESTABLE_FLAGstable tree의 stable_node에 hlist로 연결됨stable tree + rmap_list
INVALIDseqnr 불일치이전 스캔 주기의 unstable 항목, 접근 시 제거rmap_list (트리에서는 이미 무효)
/* rmap_item 상태 확인 매크로 */
#define SEQNR_MASK     0x0ff       /* seqnr 하위 8비트 */
#define UNSTABLE_FLAG  0x100       /* unstable tree에 있음 */
#define STABLE_FLAG    0x200       /* stable tree에 연결 */

static inline bool in_stable_tree(struct ksm_rmap_item *rmap_item)
{
    return rmap_item->address & STABLE_FLAG;
}

/* 상태 전이 시 플래그 조작 */
static void remove_rmap_item_from_tree(
    struct ksm_rmap_item *rmap_item)
{
    if (rmap_item->address & STABLE_FLAG) {
        /* STABLE → CHECKSUM: hlist에서 제거 */
        struct ksm_stable_node *snode = rmap_item->head;
        hlist_del(&rmap_item->hlist);
        snode->rmap_hlist_len--;
        if (!snode->rmap_hlist_len) {
            /* 마지막 공유자 → stable_node 제거 */
            rb_erase(&snode->node, &root_stable_tree);
            free_stable_node(snode);
            ksm_pages_shared--;
        }
        ksm_pages_sharing--;
    } else if (rmap_item->address & UNSTABLE_FLAG) {
        /* UNSTABLE → 제거: rb_node 제거 */
        rb_erase(&rmap_item->node, &root_unstable_tree);
        ksm_pages_unshared--;
    }
    rmap_item->address &= PAGE_MASK;  /* 플래그 초기화 */
}
INVALID 상태의 lazy 처리: seqnr이 증가하면 모든 unstable tree 노드가 논리적으로 무효화되지만, 실제로 트리를 순회하며 제거하지는 않습니다. 대신 다음 스캔에서 해당 rmap_item에 접근할 때 seqnr 불일치를 감지하고 그때 제거합니다. 이 lazy 방식은 전체 unstable tree 정리의 O(n) 비용을 분산시킵니다.

mm_slot 생명주기

ksm_mm_slot은 프로세스가 madvise(MADV_MERGEABLE) 또는 prctl(PR_SET_MEMORY_MERGE)을 호출할 때 생성되고, 해당 mm_struct가 소멸되거나 MADV_UNMERGEABLE이 호출될 때 제거됩니다.

/* madvise(MADV_MERGEABLE) 호출 시 */
int __ksm_enter(struct mm_struct *mm)
{
    struct ksm_mm_slot *mm_slot;

    mm_slot = mm_slot_alloc(mm_slot_cache);
    if (!mm_slot)
        return -ENOMEM;

    /* KSM mm_slot 해시 테이블에 삽입 */
    mm_slot_insert(mm_slots_hash, mm, &mm_slot->slot);

    /* ksmd의 스캔 리스트에 추가 */
    spin_lock(&ksm_mmlist_lock);
    list_add_tail(&mm_slot->mm_node, &ksm_mm_head.mm_node);
    spin_unlock(&ksm_mmlist_lock);

    set_bit(MMF_VM_MERGEABLE, &mm->flags);
    return 0;
}

/* mm_struct 소멸 시 (exit_mmap에서 호출) */
void __ksm_exit(struct mm_struct *mm)
{
    struct ksm_mm_slot *mm_slot;

    mm_slot = mm_slot_lookup(mm_slots_hash, mm);
    if (!mm_slot)
        return;

    /* ksmd가 이 mm을 스캔 중이면 다음으로 이동 */
    if (ksm_scan.mm_slot == mm_slot)
        ksm_scan.mm_slot = list_entry(
            mm_slot->mm_node.next, ...);

    /* 모든 rmap_item 해제 */
    remove_trailing_rmap_items(mm_slot, &mm_slot->rmap_list);

    list_del(&mm_slot->mm_node);
    mm_slot_free(mm_slot_cache, mm_slot);
    clear_bit(MMF_VM_MERGEABLE, &mm->flags);
}

PageKsm 플래그

KSM으로 병합된 페이지는 struct pagepage->flagsPG_ksm 비트가 설정됩니다. 이 플래그로 COW fault 처리 시 KSM 페이지를 식별합니다.

/* include/linux/page-flags.h */
PAGE_TYPE_OPS(Ksm, ksm, ksm)  /* PageKsm(), SetPageKsm(), ClearPageKsm() */

/* KSM 페이지인지 확인 */
if (PageKsm(page)) {
    /* KSM 페이지: anon_vma 대신 stable_node로 역매핑 */
    struct ksm_stable_node *snode = page_stable_node(page);
}

벤치마킹과 측정

KSM 테스트 프로그램

다음은 KSM 병합 효과를 측정하는 간단한 테스트 프로그램입니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

#define NUM_REGIONS  100
#define REGION_SIZE  (1 * 1024 * 1024)  /* 1MB */

int main(void)
{
    void *regions[NUM_REGIONS];
    int i;

    /* 100개의 1MB 영역을 동일한 패턴으로 채움 */
    for (i = 0; i < NUM_REGIONS; i++) {
        regions[i] = mmap(NULL, REGION_SIZE,
                          PROT_READ | PROT_WRITE,
                          MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        if (regions[i] == MAP_FAILED) {
            perror("mmap");
            return 1;
        }

        /* 동일한 패턴으로 채움 */
        memset(regions[i], 0x42, REGION_SIZE);

        /* KSM 대상으로 등록 */
        if (madvise(regions[i], REGION_SIZE, MADV_MERGEABLE) < 0)
            perror("madvise");
    }

    printf("Allocated %d MB, waiting for KSM...\n",
           NUM_REGIONS * REGION_SIZE / (1024 * 1024));
    printf("Check /sys/kernel/mm/ksm/ for progress\n");

    /* KSM이 병합할 시간을 줌 */
    sleep(300);

    /* 정리 */
    for (i = 0; i < NUM_REGIONS; i++)
        munmap(regions[i], REGION_SIZE);

    return 0;
}

컴파일 및 실행

# 컴파일
gcc -O2 -o ksm_test ksm_test.c

# KSM 활성화 확인
cat /sys/kernel/mm/ksm/run   # 1이어야 함

# 테스트 실행
./ksm_test &
TEST_PID=$!

# 프로세스별 KSM 통계 (v6.7+)
watch -n2 cat /proc/$TEST_PID/ksm_stat

# smaps에서 KSM 바이트 확인
grep -i ksm /proc/$TEST_PID/smaps_rollup

# 100MB 중복 영역이 KSM으로 병합되어
# ~99MB 절약되는 것을 확인할 수 있음

perf를 이용한 ksmd 프로파일링(Profiling)

# ksmd의 CPU 시간 분포 확인
perf top -p $(pgrep ksmd)
# 주요 함수: memcmp_pages, stable_tree_search,
# unstable_tree_search_insert, cmp_and_merge_page

# ksmd의 호출 빈도 통계
perf stat -p $(pgrep ksmd) -- sleep 60
# instructions, cache-misses, page-faults 확인

# ksmd 호출 그래프
perf record -g -p $(pgrep ksmd) -- sleep 30
perf report
# memcmp_pages가 50-70% 차지가 일반적

측정 스크립트

#!/bin/bash
# KSM 병합 속도 측정

# 기준선 기록
start_sharing=$(cat /sys/kernel/mm/ksm/pages_sharing)
start_scans=$(cat /sys/kernel/mm/ksm/full_scans)
start_time=$(date +%s)

echo "KSM convergence monitoring (Ctrl+C to stop)"
echo "Time(s) | Scans | Shared | Sharing | Saved(MB)"
echo "--------|-------|--------|---------|----------"

while true; do
    now=$(date +%s)
    elapsed=$(( now - start_time ))
    scans=$(cat /sys/kernel/mm/ksm/full_scans)
    shared=$(cat /sys/kernel/mm/ksm/pages_shared)
    sharing=$(cat /sys/kernel/mm/ksm/pages_sharing)
    saved=$(( (sharing - shared) * 4 / 1024 ))

    printf "%7d | %5d | %6d | %7d | %5d MB\n" \
        $elapsed $scans $shared $sharing $saved

    # 수렴 감지: 3회 연속 변화 없으면 종료
    if [ "$sharing" = "$prev_sharing" ] && \
       [ "$prev_sharing" = "$pprev_sharing" ] && \
       [ "$sharing" -gt 0 ]; then
        echo "Converged after ${elapsed}s, ${scans} scans"
        break
    fi

    pprev_sharing=$prev_sharing
    prev_sharing=$sharing
    sleep 5
done

대표적 벤치마크 결과

시나리오VM 수메모리KSM 절약수렴 시간ksmd CPU
동일 Ubuntu, 유휴1020 GB65% (13 GB)~5분2-3%
동일 Ubuntu, 웹서버1020 GB45% (9 GB)~8분3-5%
혼합 OS (Ubuntu+CentOS)1020 GB25% (5 GB)~10분4-6%
동일 Java 앱 컨테이너5025 GB30% (7.5 GB)~15분5-8%
이기종 앱 혼합2040 GB10% (4 GB)~20분5-7%
벤치마크 조건: pages_to_scan=1000, sleep_millisecs=20, use_zero_pages=1 설정. 수렴 시간은 full_scans가 안정화되는 시점. ksmd CPU는 top에서 측정한 평균값. 실제 결과는 워크로드 특성에 따라 크게 달라질 수 있습니다.

KSM 운영 플레이북

KSM 활성화 절차

# 1. KSM 지원 확인
test -d /sys/kernel/mm/ksm && echo "KSM supported" || echo "KSM not available"

# 2. 현재 상태 확인
cat /sys/kernel/mm/ksm/run   # 0=정지, 1=실행

# 3. KSM 활성화
echo 1 > /sys/kernel/mm/ksm/run

# 4. 스캔 속도 설정 (워크로드에 맞게 조절)
echo 1000 > /sys/kernel/mm/ksm/pages_to_scan
echo 20 > /sys/kernel/mm/ksm/sleep_millisecs

# 5. zero page 최적화 활성화
echo 1 > /sys/kernel/mm/ksm/use_zero_pages

# 6. advisor 모드 설정 (v6.4+)
echo scan-time > /sys/kernel/mm/ksm/advisor_mode
echo 300 > /sys/kernel/mm/ksm/advisor_target_scan_time

문제 진단

증상확인 사항조치
pages_sharing이 0run이 1인지, full_scans가 증가하는지대상 프로세스가 MADV_MERGEABLE을 호출했는지 확인
pages_volatile이 높음워크로드가 쓰기 집약적KSM 대상 영역 축소 또는 비활성화
ksmd CPU 사용률 높음pages_to_scan값 줄이기 또는 advisor_mode 활성화
general_profit이 음수rmap_item 오버헤드 > 절약량등록 영역 축소, 효과 없는 프로세스 제외
COW storm (대량 fault)게스트 워크로드 변경병합된 페이지가 동시에 변경됨, 메모리 여유 확인

KSM 비활성화 절차

# 방법 1: 새 병합 중단 (기존 병합 유지)
echo 0 > /sys/kernel/mm/ksm/run

# 방법 2: 모든 병합 해제 (기존 KSM 페이지를 전부 COW 분리)
echo 2 > /sys/kernel/mm/ksm/run
# 주의: pages_sharing만큼의 새 페이지가 필요하므로
# 충분한 여유 메모리가 있는지 먼저 확인!
free -h

# 완료 후 정지
echo 0 > /sys/kernel/mm/ksm/run
run=2 주의: 모든 KSM 페이지를 언머지(unmerge)하면 pages_sharing만큼의 물리 페이지가 추가로 필요합니다. 여유 메모리가 부족하면 OOM이 발생할 수 있으므로, 반드시 가용 메모리를 확인한 후 실행하세요.

부팅 시 영구 설정

# /etc/sysctl.d/ksm.conf (sysctl로는 직접 설정 불가, 대신 udev/systemd 사용)

# systemd tmpfiles 방식
# /etc/tmpfiles.d/ksm.conf
w /sys/kernel/mm/ksm/run              - - - - 1
w /sys/kernel/mm/ksm/pages_to_scan    - - - - 1000
w /sys/kernel/mm/ksm/sleep_millisecs  - - - - 20
w /sys/kernel/mm/ksm/use_zero_pages   - - - - 1

# Red Hat 계열: ksmtuned 서비스
systemctl enable ksm
systemctl enable ksmtuned
# /etc/ksmtuned.conf에서 파라미터 자동 조절 설정

흔한 실수와 안티 패턴

실수증상올바른 방법
KSM 활성화 후 madvise 미호출pages_sharing이 0인 채 유지madvise(MADV_MERGEABLE) 또는 prctl(PR_SET_MEMORY_MERGE)을 반드시 호출
쓰기 집약 워크로드에서 KSM 사용ksmd CPU 높음, pages_volatile 높음, cow_ksm 증가읽기 위주 워크로드에서만 KSM 활성화. pages_volatilepages_sharing보다 높으면 비활성화 고려
KSM 절약분에 의존한 overcommit워크로드 변경 시 대량 COW → OOMKSM 절약분의 20~30%를 여유로 확보. memory.max 설정 시 안전 마진 포함
run=2로 언머지 시 메모리 미확인가용 메모리 부족 → OOMfree -h로 여유 메모리 확인 후 실행. pages_sharing * 4KB만큼 필요
THP always + KSM 동시 사용khugepaged와 ksmd가 충돌, CPU 낭비KSM 환경에서는 echo madvise > /sys/.../transparent_hugepage/enabled
멀티 테넌트 환경에서 KSM 사용side-channel 정보 유출 가능단일 테넌트 또는 신뢰 도메인 내에서만 사용. CSP 보안 정책 확인
pages_to_scan을 너무 높게 설정ksmd가 CPU 100% 점유advisor_mode=cpu-percent + advisor_max_cpu=10으로 자동 조절
NUMA 서버에서 merge_across_nodes=1원격 노드 접근으로 지연 증가지연 민감 워크로드에서는 merge_across_nodes=0 사용
general_profit 음수 무시rmap_item 오버헤드가 절약량 초과KSM 등록 영역 축소, 효과 없는 프로세스의 MADV_UNMERGEABLE 호출
fork 폭주(fork bomb) + KSM모든 자식이 KSM 페이지 공유 → 한꺼번에 COWfork 후 exec 패턴이면 KSM 효과적. 장시간 실행 자식에는 개별 madvise 관리
KSM 효율성 판단 공식:
효율 비율 = pages_sharing / (pages_sharing + pages_unshared + pages_volatile)
이 비율이 0.5 미만이면 KSM의 CPU 비용 대비 효과가 낮습니다. general_profit이 양수인지도 함께 확인하세요.

KSM 전체 생명주기 요약

KSM 전체 생명주기 1. 등록 단계 앱이 madvise(MADV_MERGEABLE) 또는 prctl(PR_SET_MEMORY_MERGE) 호출 → __ksm_enter() → mm_slot 할당 → ksm_mm_head 리스트에 추가 → VMA에 VM_MERGEABLE 설정 2. 스캔 단계 (ksmd 주기적 실행) ksm_do_scan() → scan_get_next_rmap_item(): mm_slot 순회 → VMA 순회 → PAGE_SIZE 단위 이동 get_mergeable_page(): PTE → 물리 페이지 획득 (PageAnon, !PageKsm, !THP 검증) pages_to_scan개 처리 후 sleep_millisecs만큼 휴식 → 반복 3. 비교/병합 단계 cmp_and_merge_page(): checksum 안정성 확인 → zero page 최적화 → stable tree 검색 stable 매치 → try_to_merge_with_ksm_page(): write_protect → pages_identical → PTE 교체 → rmap 갱신 unstable 매치 → try_to_merge_two_pages(): 새 KSM 페이지 할당 → 두 PTE 모두 교체 → stable tree 삽입 매치 없음 → unstable tree에 삽입, 다음 스캔에서 다른 페이지와 매치 대기 4. 공유 상태 (안정 기간) 여러 프로세스의 PTE가 동일 KSM 페이지를 R/O로 공유 → 읽기 접근은 정상 동작 쓰기 시도 → do_wp_page() → PageKsm 확인 → wp_page_copy() → 개별 페이지 복사 (COW 분리) 5. 해제 단계 MADV_UNMERGEABLE → 즉시 모든 KSM 페이지 COW 분리 (추가 메모리 필요) 프로세스 종료 → __ksm_exit() → mm_slot 해제, rmap_item 해제 → mapcount 감소 → 0이면 KSM 페이지 해제 6. 전역 정지/언머지 run=0 → ksmd 정지 (기존 병합 유지) | run=2 → 모든 KSM 페이지 언머지 후 정지 (OOM 주의)

참고자료

커널 문서

LWN 기사

커널 소스

발표 자료