KSM(Kernel Same-page Merging)은 커널이 실행 중인 프로세스들의 동일한 내용의 익명 페이지를 탐지하여 하나의 물리 페이지로 병합(merging)하는 메모리 중복 제거 메커니즘입니다. 가상화 환경(KVM/QEMU)에서 여러 가상 머신이 동일한 커널이나 라이브러리를 실행할 때, 또는 같은 프로세스를 fork()한 후 COW로 분리되지 않은 페이지를 다시 병합할 때 메모리 사용량을 크게 절감할 수 있습니다.
KSM은 백그라운드 커널 스레드인 ksmd가 주기적으로 모든 VM_MERGEABLE 플래그가 설정된 VMA를 스캔하고, 페이지 내용을 비교하여 stable tree(쓰기 보호된 KSM 페이지)와 unstable tree(일시적으로 보관되는 후보 페이지)에 정렬합니다. 동일한 페이지를 찾으면 write-protect 후 PTE를 교체하여 물리 페이지를 공유합니다.
애플리케이션은 madvise(..., MADV_MERGEABLE)로 병합 후보를 명시하고, MADV_UNMERGEABLE로 제외합니다. 운영자는 sysfs로 ksmd의 실행 여부와 스캔 속도를 조절합니다.
같은 사본 여러 부를 한 권으로 정리해 나머지를 공유하는 도서관 사본 관리와 비슷합니다. 스캔과 비교 비용이 있으므로 CPU 오버헤드가 생기며, 필요한 VMA만 골라 켜는 것이 중요합니다.
mm/ksm.c ← KSM 핵심 구현 (스캔, 비교, 병합, stable/unstable tree)
mm/madvise.c ← MADV_MERGEABLE/MADV_UNMERGEABLE 처리
include/linux/ksm.h ← KSM 외부 API 및 인라인 함수
mm/mm_slot.h ← mm_slot 해시 테이블 관리
include/linux/mm.h ← struct mm_struct 내 ksm 관련 필드
# KSM 스레드 상태 확인
ps aux | grep ksmd
# KSM 병합 통계 (전체 시스템)
cat /sys/kernel/mm/ksm/pages_shared # stable tree의 고유 KSM 페이지 수
cat /sys/kernel/mm/ksm/pages_sharing # KSM 페이지를 공유 중인 추가 매핑 수
cat /sys/kernel/mm/ksm/pages_unshared # unstable tree의 미병합 후보 수
cat /sys/kernel/mm/ksm/pages_volatile # rmap_item 중 어느 트리에도 없는 수
cat /sys/kernel/mm/ksm/pages_scanned # 총 스캔된 페이지 수
cat /sys/kernel/mm/ksm/full_scans # 전체 스캔 완료 횟수
cat /sys/kernel/mm/ksm/pages_skipped # smart scan으로 건너뛴 페이지 수
cat /sys/kernel/mm/ksm/ksm_zero_pages # zero page 병합 수
# KSM 수익 계산 (절약 메모리 - 오버헤드)
cat /sys/kernel/mm/ksm/general_profit
# KSM 스캔 파라미터 확인/조정
cat /sys/kernel/mm/ksm/pages_to_scan # 한 번에 스캔할 페이지 수 (기본 100)
cat /sys/kernel/mm/ksm/sleep_millisecs # 스캔 간 대기 시간 (기본 20ms)
cat /sys/kernel/mm/ksm/run # 0:정지, 1:병합, 2:병합해제
cat /sys/kernel/mm/ksm/merge_across_nodes # NUMA 노드 간 병합 허용 여부
cat /sys/kernel/mm/ksm/use_zero_pages # zero page 병합 활성화 여부
cat /sys/kernel/mm/ksm/smart_scan # 스마트 스캔 활성화 여부
cat /sys/kernel/mm/ksm/max_page_sharing # 하나의 KSM 페이지 최대 공유 수 (기본 256)
# KSM advisor 확인/조정
cat /sys/kernel/mm/ksm/advisor_mode # none / scan-time
cat /sys/kernel/mm/ksm/advisor_max_cpu # advisor 최대 CPU%
cat /sys/kernel/mm/ksm/advisor_target_scan_time # 목표 스캔 시간(초)
echo scan-time | sudo tee /sys/kernel/mm/ksm/advisor_mode
echo none | sudo tee /sys/kernel/mm/ksm/advisor_mode
# KSM 활성화 (sysfs를 통한 제어)
echo 1 | sudo tee /sys/kernel/mm/ksm/run # ksmd 시작
echo 0 | sudo tee /sys/kernel/mm/ksm/run # ksmd 중지
# 프로세스별 KSM 사용량 확인
cat /proc/<pid>/status | grep KSM
awk '/VmFlags/ && /mg/ {print}' /proc/<pid>/smaps # mergeable VMA 확인
# per-mm KSM 정보 (/proc/<pid>/ksm_stat - 해당 커널에서 지원 시)
cat /proc/<pid>/ksm_stat 2>/dev/null
# KSM 관련 커널 설정 확인
zgrep KSM /proc/config.gz 2>/dev/null || grep KSM /boot/config-$(uname -r)
stable tree의 노드로, write-protect된 KSM 페이지를 나타냅니다. 동일한 내용의 여러 매핑이 있을 때 chain/dup 구조로 관리됩니다.
// mm/ksm.c:159-185
struct ksm_stable_node {
union {
struct rb_node node; /* stable tree 내 rb-tree 노드 */
struct { /* migration list에 있을 때 */
struct list_head *head;
struct {
struct hlist_node hlist_dup; /* chain 내 dup 연결 */
struct list_head list; /* migrate_nodes 연결 */
};
};
};
struct hlist_head hlist; /* 이 stable node를 사용하는 rmap_item 목록 */
union {
unsigned long kpfn; /* KSM 페이지의 물리 프레임 번호 */
unsigned long chain_prune_time; /* chain pruning 마지막 시간 */
};
#define STABLE_NODE_CHAIN -1024 /* chain 노드 판별 마커 */
int rmap_hlist_len; /* hlist의 rmap_item 수 또는 STABLE_NODE_CHAIN */
#ifdef CONFIG_NUMA
int nid; /* NUMA 노드 ID */
#endif
};
가상 주소→물리 페이지의 역방향 매핑(rmap) 항목입니다. unstable tree의 rb-tree 노드이거나 stable tree의 hlist 요소로 사용됩니다.
// mm/ksm.c:201-221
struct ksm_rmap_item {
struct ksm_rmap_item *rmap_list; /* mm_slot의 단일 연결 리스트 */
union {
struct anon_vma *anon_vma; /* stable tree에 있을 때 anon_vma */
#ifdef CONFIG_NUMA
int nid; /* unstable tree에 있을 때 NUMA node */
#endif
};
struct mm_struct *mm; /* 이 rmap_item이 속한 mm */
unsigned long address; /* 가상 주소 + 플래그 비트 */
unsigned int oldchecksum; /* unstable tree에서 이전 체크섬 */
rmap_age_t age; /* 스캔 반복 횟수 (스마트 스캔용) */
rmap_age_t remaining_skips; /* 남은 건너뜀 횟수 */
union {
struct rb_node node; /* unstable tree 내 rb-tree 노드 */
struct { /* stable tree에서 사용 시 */
struct ksm_stable_node *head;
struct hlist_node hlist;
};
};
};
// 주소 하위 비트 플래그 (mm/ksm.c:223-225)
#define SEQNR_MASK 0x0ff /* 불안정 트리 seqnr */
#define UNSTABLE_FLAG 0x100 /* unstable tree 노드 */
#define STABLE_FLAG 0x200 /* stable tree의 rmap_item */
스캔 커서가 현재 위치한 mm 정보를 담습니다. 해시 테이블로 mm→mm_slot 빠른 조회를 지원합니다.
// mm/ksm.c:126-129
struct ksm_mm_slot {
struct mm_slot slot; /* 해시 + 연결 리스트 관리 */
struct ksm_rmap_item *rmap_list; /* 이 mm의 rmap_item 목록 머리 */
};
// mm/mm_slot.h:15-19
struct mm_slot {
struct hlist_node hash; /* 해시 테이블 연결 */
struct list_head mm_node; /* mm 목록 연결 */
struct mm_struct *mm; /* 대상 mm_struct */
};
전역 스캔 커서로, 현재 어떤 mm, 어떤 주소, 어떤 rmap_list를 스캔 중인지 추적합니다.
// mm/ksm.c:140-145
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; /* 완료된 전체 스캔 횟수 */
};
scan-time advisor가 스캔 속도를 적응형으로 조절하기 위한 컨텍스트입니다.
// mm/ksm.c:325-331
struct advisor_ctx {
ktime_t start_scan; /* 현재 스캔 시작 시간 */
unsigned long scan_time; /* 이전 스캔 소요 시간 */
unsigned long change; /* pages_to_scan 변경 비율 (EWMA) */
unsigned long long cpu_time; /* 이전 스캔의 ksmd CPU 사용 시간 */
};
// mm/ksm.c:228-248 — stable/unstable tree 루트
static struct rb_root one_stable_tree[1] = { RB_ROOT };
static struct rb_root one_unstable_tree[1] = { RB_ROOT };
static struct rb_root *root_stable_tree = one_stable_tree;
static struct rb_root *root_unstable_tree = one_unstable_tree;
// mm/ksm.c:252-301 — 주요 통계 변수
static unsigned long ksm_pages_scanned; // 스캔된 페이지 수
static unsigned long ksm_pages_shared; // stable tree 고유 KSM 페이지 수
static unsigned long ksm_pages_sharing; // KSM 페이지 공유 매핑 수
static unsigned long ksm_pages_unshared; // unstable tree 미병합 수
static unsigned long ksm_rmap_items; // 사용 중 rmap_item 수
static unsigned long ksm_pages_skipped; // smart scan으로 건너뛴 수
// mm/ksm.c:2801-2826
static int ksm_scan_thread(void *nothing)
역할: 백그라운드에서 ksm_do_scan()을 반복 호출하는 커널 스레드입니다. set_user_nice(current, 5)로 낮은 우선순위로 동작하며, KSM_RUN_MERGE 상태일 때만 스캔을 수행합니다.
분기 로직:
ksmd_should_run() → ksm_run & KSM_RUN_MERGE && mm_list 비어있지 않음이면 스캔 실행ksm_thread_sleep_millisecs 동안 대기ksm_thread_sleep_millisecs 값이 변경되면 즉시 깨어남 (ksm_iter_wait)// mm/ksm.c:2780-2794
static void ksm_do_scan(unsigned int scan_npages)
역할: scan_npages만큼 페이지를 스캔합니다. 각 반복에서 scan_get_next_rmap_item()으로 다음 후보 페이지를 얻고, cmp_and_merge_page()로 비교/병합을 시도합니다.
분기 로직:
freezing(current)이면 즉시 종료 (동결 상태)rmap_item이 NULL이면 더 이상 스캔할 것이 없음 → 종료put_page(page)로 참조 해제, ksm_pages_scanned++// mm/ksm.c:2248-2386
static void cmp_and_merge_page(struct page *page, struct ksm_rmap_item *rmap_item)
역할: 단일 페이지의 stable tree 검색, checksum 비교, unstable tree 검색/삽입, 병합을 수행하는 핵심 함수입니다.
분기 로직:
1. 이미 stable tree에 있으면 → is_page_sharing_candidate() 확인 후 조기 반환
2. checksum이 이전과 다르면 → 페이지가 빈번히 변경됨 → unstable tree에도 넣지 않음
3. try_to_merge_with_zero_page() → zero page 병합 가능하면 병합 후 반환
4. stable_tree_search(page) → stable tree에서 동일 페이지 찾기
- 찾으면 → try_to_merge_with_ksm_page()로 병합 시도
5. unstable tree에서 unstable_tree_search_insert() → 동일 페이지 찾으면
- try_to_merge_two_pages()로 두 페이지를 병합 후 stable tree에 삽입
// mm/ksm.c:1825-2030
static struct folio *stable_tree_search(struct page *page)
역할: stable tree에서 스캔 중인 페이지와 동일한 내용의 KSM 페이지를 찾습니다. chain/dup 구조를 순회하며 memcmp_pages()로 내용을 비교합니다.
분기 로직:
ksm_get_folio()가 제거 → 검색 재시작memcmp_pages() == 0 → 동일 페이지 발견 - is_page_sharing_candidate() → 공유 가능하면 folio 반환
- 공유 불가 → chain 내 다른 dup 탐색 또는 NULL 반환
// mm/ksm.c:2133-2198
static struct ksm_rmap_item *unstable_tree_search_insert(
struct ksm_rmap_item *rmap_item,
struct page *page,
struct page **tree_pagep)
역할: unstable tree에서 동일 페이지를 찾거나, 없으면 현재 rmap_item을 삽입합니다. unstable tree는 매 전체 스캔 완료 시 리셋되므로 안전합니다.
분기 로직:
memcmp_pages() == 0 → 동일 페이지 발견 - NUMA 노드 불일치 확인 (!ksm_merge_across_nodes && 다른 노드)
- *tree_pagep에 tree_page 설정 후 tree_rmap_item 반환
rmap_item->address |= UNSTABLE_FLAG | seqnr, ksm_pages_unshared++// mm/ksm.c:1272-1361
static int write_protect_page(struct vm_area_struct *vma, struct folio *folio,
pte_t *orig_pte)
역할: 병합 전에 페이지를 쓰기 보호합니다. dirty/clean 상태가 섞여 있으면 PTE를 다시 맞춰야 하며 folio lock이 필요합니다.
분기 로직:
pte_write || pte_dirty || anon_exclusive || mm_tlb_flush_pending → PTE 클리어 후 재설정folio_mapcount + 1 + swapped != folio_ref_count → O_DIRECT 진행 중 → 실패anon_exclusive → folio_try_share_anon_rmap_pte() 실패 시 → 실패// mm/ksm.c:2975-3013
int ksm_madvise(struct vm_area_struct *vma, unsigned long start,
unsigned long end, int advice, vm_flags_t *vm_flags)
역할: MADV_MERGEABLE와 MADV_UNMERGEABLE 요청을 처리합니다. MADV_MERGEABLE은 __ksm_enter(mm)로 해당 mm_struct를 KSM 관리 대상에 넣고 VM_MERGEABLE을 켜며, MADV_UNMERGEABLE은 이미 병합된 페이지가 있으면 break_ksm()으로 분리한 뒤 플래그를 끕니다.
분기 로직:
MADV_MERGEABLE → vma_ksm_compatible() 확인, __ksm_enter(mm) 필요 시 호출, *vm_flags |= VM_MERGEABLEMADV_UNMERGEABLE → VM_MERGEABLE가 없으면 무시, 익명 VMA면 break_ksm(vma, start, end, true) 후 플래그 해제// mm/madvise.c:1416-1422
case MADV_MERGEABLE:
case MADV_UNMERGEABLE:
error = ksm_madvise(vma, range->start, range->end,
behavior, &new_flags);
if (error)
goto out;
break;
ksm_scan_thread() ← ksmd 메인 스레드
└─ ksm_do_scan(pages_to_scan) ← scan_npages만큼 반복
└─ scan_get_next_rmap_item(&page) ← 다음 후보 페이지/ rmap_item 획득
└─ walk_page_range_vma() ← VMA 순회하여 익명 페이지 탐색
└─ get_next_rmap_item() ← rmap_item 할당/조회
└─ should_skip_rmap_item() ← smart scan: 건너뛸지 결정
└─ cmp_and_merge_page(page, rmap_item) ← 핵심 비교/병합
├─ calc_checksum() ← xxhash 기반 체크섬 계산
├─ stable_tree_search(page) ← stable tree에서 동일 페이지 탐색
│ └─ memcmp_pages() ← 페이지 내용 바이트 비교
│ └─ chain_prune() ← stale chain/dup 정리
├─ unstable_tree_search_insert() ← unstable tree 검색/삽입
│ └─ memcmp_pages() ← 페이지 내용 비교
├─ try_to_merge_with_ksm_page() ← 기존 KSM 페이지와 병합
│ └─ try_to_merge_one_page()
│ └─ write_protect_page() ← PTE 쓰기 보호
│ └─ replace_page() ← PTE를 KSM 페이지로 교체
└─ try_to_merge_two_pages() ← 두 미병합 페이지 병합
└─ stable_tree_insert() ← stable tree에 새 KSM 노드 삽입
└─ stable_tree_append() ← rmap_item을 stable node에 연결
| 항목 | KSM_RUN_STOP (0) | KSM_RUN_MERGE (1) | KSM_RUN_UNMERGE (2) |
|---|---|---|---|
| ksmd 동작 | 중지 | 스캔/병합 수행 | 병합 해제만 수행 |
| 트리 동작 | 불변 | stable/unstable tree 갱신 | 모든 rmap_item을 unstable에서 제거 |
| COW 발동 | 없음 | 병합 시 write-protect 발생 | break_ksm()으로 모든 KSM 페이지 병합 해제 |
| 트리거 | `echo 0 > run` | `echo 1 > run` | `echo 2 > run` |
| 항목 | Smart Scan (기본 켜짐) | 일반 스캔 (끄면) |
|---|---|---|
| 건너뜀 메커니즘 | age/remaining_skips로 빈번 변경 페이지 스킵 | 모든 페이지 스캔 |
| 건너뜀 기준 | age ≥ 3부터 점진적 스킵 (1→2→4→8회) | 없음 |
| pages_skipped | 증가 | 0 유지 |
| 대상 | `folio_test_ksm()`이 아닌 일반 익명 페이지 | 모든 anonymous 페이지 |
| 장점 | 불필요한 스캔 최소화, CPU 절약 | 완전한 스캔 보장 |
| 항목 | merge_across_nodes=1 (기본) | merge_across_nodes=0 |
|---|---|---|
| stable/unstable tree | 전역 1개씩 | NUMA node별 별도 tree |
| NUMA 원격 병합 | 허용 | 불허 (같은 노드 내에서만 병합) |
| 메모리 절감 | 최대 (원격 포함) | locality 우선 |
| tree 구조 | `root_stable_tree[0]`만 사용 | `root_stable_tree[nr_node_ids]` 배열 |
| 항목 | KSM_ADVISOR_NONE (기본) | KSM_ADVISOR_SCAN_TIME |
|---|---|---|
| pages_to_scan 제어 | 수동 (sysfs) | 자동 적응형 |
| 목표 | 없음 | `ksm_advisor_target_scan_time` (기본 200초) |
| CPU 제한 | 없음 | `KSM_ADVISOR_MIN_CPU` (10%) ~ `ksm_advisor_max_cpu` (70%) |
| EWMA 적용 | 없음 | 스캔 시간 변화량을 EWMA로 평활 |
| 항목 | 애플리케이션 `madvise` | 운영자 sysfs |
|---|---|---|
| 대상 | 개별 VMA | 전체 ksmd 정책 |
| 핵심 동작 | `MADV_MERGEABLE`로 `VM_MERGEABLE` 설정, `MADV_UNMERGEABLE`로 해제 | `run`, `pages_to_scan`, `sleep_millisecs`, `advisor_mode` 조정 |
| 커널 진입점 | `mm/madvise.c` → `ksm_madvise()` | `mm/ksm.c` sysfs store 함수 |
| 효과 | 병합 후보 지정/해제 | 스캔 실행, 속도, CPU 상한 제어 |
| 항목 | 일반 stable_node | chain stable_node | dup stable_node |
|---|---|---|---|
| 역할 | KSM 페이지 1개를 나타냄 | 동일 내용의 여러 KSM 페이지 그룹 | chain 내 개별 KSM 페이지 |
| rmap_hlist_len | 0 ~ ksm_max_page_sharing | STABLE_NODE_CHAIN (-1024) | 0 ~ ksm_max_page_sharing |
| hlist | rmap_item 연결 | dup stable_node 연결 | rmap_item 연결 |
| rb_tree | (rb_node 사용) | (rb_node 사용) | (rb_node 미사용, hlist_dup로 chain에 연결) |
KSM의 메모리 절감 효과는 general_profit으로 측정됩니다:
general_profit = (pages_sharing + ksm_zero_pages) × PAGE_SIZE
- rmap_items × sizeof(struct ksm_rmap_item)
| 파일 | 읽기/쓰기 | 설명 | 기본값 |
|---|---|---|---|
| `/sys/kernel/mm/ksm/run` | RW | 0:중지, 1:병합, 2:병합해제 | 0 |
| `/sys/kernel/mm/ksm/pages_to_scan` | RW | 한 번에 스캔할 페이지 수 | 100 |
| `/sys/kernel/mm/ksm/sleep_millisecs` | RW | 스캔 간 대기 (ms) | 20 |
| `/sys/kernel/mm/ksm/merge_across_nodes` | RW | NUMA 간 병합 허용 | 1 |
| `/sys/kernel/mm/ksm/use_zero_pages` | RW | zero page 병합 | 0 |
| `/sys/kernel/mm/ksm/smart_scan` | RW | 스마트 스캔 | 1 |
| `/sys/kernel/mm/ksm/max_page_sharing` | RW | 최대 공유 매핑 수 | 256 |
| `/sys/kernel/mm/ksm/advisor_mode` | RW | none / scan-time | none |
| `/sys/kernel/mm/ksm/advisor_max_cpu` | RW | 최대 CPU% | 70 |
| `/sys/kernel/mm/ksm/advisor_min_pages_to_scan` | RW | 최소 스캔 페이지 수 | 500 |
| `/sys/kernel/mm/ksm/advisor_max_pages_to_scan` | RW | 최대 스캔 페이지 수 | 30000 |
| `/sys/kernel/mm/ksm/advisor_target_scan_time` | RW | 목표 스캔 시간 (초) | 200 |
| `/sys/kernel/mm/ksm/pages_shared` | RO | 고유 KSM 페이지 수 | - |
| `/sys/kernel/mm/ksm/pages_sharing` | RO | 공유 매핑 수 | - |
| `/sys/kernel/mm/ksm/pages_unshared` | RO | 미병합 후보 수 | - |
| `/sys/kernel/mm/ksm/pages_volatile` | RO | 어느 트리에도 없는 rmap_item | - |
| `/sys/kernel/mm/ksm/pages_scanned` | RO | 총 스캔 페이지 수 | - |
| `/sys/kernel/mm/ksm/pages_skipped` | RO | 건너뛴 페이지 수 | - |
| `/sys/kernel/mm/ksm/ksm_zero_pages` | RO | zero page 병합 수 | - |
| `/sys/kernel/mm/ksm/full_scans` | RO | 전체 스캔 완료 횟수 | - |
| `/sys/kernel/mm/ksm/general_profit` | RO | 메모리 수익 (bytes) | - |
| `/sys/kernel/mm/ksm/advisor_mode` | RW | none / scan-time | none |
| `/sys/kernel/mm/ksm/advisor_max_cpu` | RW | 최대 CPU% | 70 |
| `/sys/kernel/mm/ksm/advisor_target_scan_time` | RW | 목표 스캔 시간 (초) | 200 |