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) 기반으로 분석합니다.
핵심 요약
- 동일 페이지 병합 -- 내용이 동일한 익명 페이지를 하나의 물리 페이지로 합치고, 원본 페이지를 해제하여 메모리를 절약합니다.
- COW 기반 공유 -- 병합된 페이지는 write-protect 상태로, 프로세스(Process)가 쓰기를 시도하면 page fault가 발생하여 개별 복사본이 생성됩니다.
- ksmd 데몬 -- 커널 스레드(Kernel Thread)
ksmd가 주기적으로 등록된 메모리 영역을 스캔하며 병합 후보를 찾습니다. - 두 개의 트리 -- stable tree(이미 병합된 페이지)와 unstable tree(후보 페이지)로 효율적인 검색을 수행합니다.
- 가상화(Virtualization) 핵심 기술 -- 동일 OS 이미지로 실행되는 수십~수백 VM의 중복 메모리를 병합하여 서버 통합 밀도를 크게 높입니다.
단계별 이해
- KSM 개념 파악
동일 내용의 물리 페이지를 하나로 합치는 개념과, 왜 익명 페이지만 대상인지 이해합니다. - 데이터 구조 학습
stable tree와 unstable tree의 레드블랙 트리 구조, rmap_item의 역할을 파악합니다. - 스캔 알고리즘 추적
ksmd가 페이지 내용을 해시하고 트리에서 비교하여 병합하는 과정을 따라갑니다. - API와 제어 인터페이스
madvise(), prctl(), /sys/kernel/mm/ksm/ 파라미터를 사용하여 KSM을 제어하는 방법을 익힙니다. - 실전 튜닝과 모니터링
워크로드에 맞는 스캔 속도 조절, 보안 고려사항, 성능 모니터링 방법을 숙지합니다.
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)은 물리 메모리에서 동일한 내용을 가진 익명 페이지를 자동으로 감지하고 하나의 물리 페이지로 병합하는 커널 서브시스템입니다. 병합된 페이지는 COW(Copy-On-Write) 보호 상태가 되어, 읽기 접근은 공유하고 쓰기 접근 시에만 복사본을 생성합니다.
개발 동기와 역사
| 버전 | 연도 | 주요 변화 |
|---|---|---|
| v2.6.32 | 2009 | Andrea Arcangeli가 KVM 메모리 최적화 목적으로 초기 구현 (KSM = Kernel Shared Memory) |
| v3.9 | 2013 | NUMA-aware 병합: merge_across_nodes 파라미터 추가 |
| v4.4 | 2016 | zero page 최적화: 0으로 채워진 페이지를 커널 zero page와 병합 |
| v5.7 | 2020 | advisor mode 도입으로 자동 튜닝 기반 마련 |
| v6.0 | 2022 | ksm_swpin_copy 카운터 추가, 스왑(Swap) 인 시 KSM 추적 개선 |
| v6.1 | 2022 | prctl(PR_SET_MEMORY_MERGE) 추가: 프로세스 전체 madvise 자동 적용 |
| v6.4 | 2023 | general_profit 카운터, advisor scan-time/cpu-percent 모드 도입 |
| v6.7 | 2024 | per-process KSM 통계: /proc/PID/ksm_stat |
왜 KSM이 필요한가
동일한 게스트 OS(예: Ubuntu 24.04)를 실행하는 50대의 VM이 있다고 가정합니다. 각 VM에 2GB RAM을 할당하면 총 100GB가 필요하지만, 커널 코드, 라이브러리, 초기화된 데이터 등 상당 부분이 동일합니다. KSM은 이 중복을 실시간(Real-time)으로 감지하여 물리 메모리 사용량을 40~70%까지 절감할 수 있습니다.
KSM 아키텍처
KSM 서브시스템은 세 가지 핵심 데이터 구조와 하나의 커널 스레드로 구성됩니다.
핵심 데이터 구조
| 구조체(Struct) | 위치 | 역할 |
|---|---|---|
struct ksm_mm_slot | mm/ksm.c | KSM에 등록된 각 mm_struct를 추적. 연결 리스트(Linked List)로 모든 등록 프로세스를 순회 |
struct ksm_rmap_item | mm/ksm.c | 스캔 대상 가상 주소(Virtual Address)와 해당 물리 페이지의 매핑(Mapping) 정보. stable/unstable 트리 노드 |
struct ksm_stable_node | mm/ksm.c | stable tree의 각 노드. 병합된 KSM 페이지의 대표 노드 |
struct ksm_scan | mm/ksm.c | ksmd의 현재 스캔 위치(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 */
};
ksm_rmap_item은 union을 사용하여 unstable tree에 있을 때는 rb_node로, stable tree에 병합된 후에는 hlist_node로 동작합니다. 이 설계로 추가 메모리 할당 없이 상태 전이가 가능합니다.
ksmd 스캔 알고리즘
ksmd는 무한 루프에서 등록된 메모리 영역을 순회하며 동일 페이지를 찾습니다. 각 스캔 주기에서 pages_to_scan개의 페이지를 처리한 후 sleep_millisecs만큼 휴식합니다.
스캔 단계 상세
- 페이지 가져오기 --
ksm_scan커서가 가리키는mm_slot의 다음 가상 주소에서 페이지를 가져옵니다.get_mergeable_page()가 해당 주소의 PTE를 확인하고 물리 페이지를 반환합니다. - 체크섬(Checksum) 비교 -- 페이지 내용의 CRC32 해시를 계산합니다. 이전 스캔의
oldchecksum과 비교하여 페이지 내용이 변경되었으면 unstable tree에서 제거하고 새 체크섬을 저장합니다. 내용이 안정적(두 번 연속 같은 해시)이어야 비교를 진행합니다. - Stable tree 검색 --
stable_tree_search()가 레드블랙 트리를 순회하며memcmp()로 4KB 전체를 바이트 단위 비교합니다. 일치하면 기존 KSM 페이지에 병합합니다. - 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 단위로 탐색합니다.
/* 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;
}
get_mergeable_page()는 PageTransCompound 검사로 THP(2MB) 페이지를 건너뜁니다. KSM이 THP를 병합하려면 먼저 split_huge_page()로 512개의 4KB 페이지로 분할해야 하는데, 이 분할 비용이 크므로 ksmd는 이미 분할된 4KB 페이지만 대상으로 합니다. THP 영역에서 KSM을 사용하려면 해당 VMA에서 THP를 비활성화하는 것이 효율적입니다.
cmp_and_merge_page() 디스패치(Dispatch) 로직
KSM 알고리즘의 핵심 디스패치 함수인 cmp_and_merge_page()는 하나의 후보 페이지를 받아 체크섬 비교, stable tree 검색, unstable tree 검색/삽입을 순차적으로 수행합니다.
/* 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_item을 hlist로 연결합니다.
특성
| 속성 | 설명 |
|---|---|
| 정렬 키 | 페이지 내용의 memcmp() 결과 (사전식 비교) |
| 생존 기간 | 병합된 페이지가 존재하는 한 유지 (스캔 주기 독립) |
| 노드당 공유 수 | rmap_hlist_len으로 추적, max_page_sharing(기본 256)으로 제한 |
| NUMA 구분 | merge_across_nodes=0 시 NUMA 노드별 별도 stable tree 운영 |
| stale 노드 처리 | 페이지가 사라진 stable_node는 lazy하게 발견 시 제거 |
/* mm/ksm.c — 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;
}
/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)으로 폭증하는 것을 방지합니다.
/* 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;
}
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 Tree | Unstable Tree |
|---|---|---|
| 저장 대상 | 병합 완료된 KSM 페이지 | 병합 후보 페이지 |
| write-protect | Yes (COW 보호) | No |
| 수명 | KSM 페이지가 존재하는 동안 | 한 스캔 주기(seqnr) |
| 검색 보장 | 내용 불변 -> 검색 신뢰 | 내용 변경 가능 -> false positive 가능 |
| 정렬 키 | memcmp 바이트 순서(Byte Order) | memcmp 바이트 순서 (불안정) |
/* mm/ksm.c — 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은 다음 과정으로 병합을 수행합니다.
Zero Page 최적화
0으로 채워진 페이지는 가장 흔한 중복 패턴입니다. KSM은 이를 특별히 처리하여 커널의 전역 zero page(ZERO_PAGE(0))와 직접 병합합니다. 별도의 KSM 페이지를 할당할 필요가 없으므로 메모리 절약 효과가 추가됩니다.
/* mm/ksm.c — 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만 새 페이지를 가리키도록 변경합니다.
/* 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를 교체합니다.
/* 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 → 공유 유지 */
}
fork() 시 KSM 페이지는 일반 COW 페이지와 동일하게 처리됩니다. 부모와 자식 모두 같은 KSM 페이지를 R/O로 공유하며, mapcount만 증가합니다. 자식이 exec()를 호출하면 주소 공간(Address Space)이 교체되어 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);
MemoryKSM=yes를 설정하면 자동으로 prctl(PR_SET_MEMORY_MERGE, 1)을 호출합니다. 대규모 컨테이너 환경에서 서비스 단위로 KSM을 제어할 수 있습니다.
/sys/kernel/mm/ksm/ 인터페이스
KSM의 모든 제어와 모니터링은 sysfs를 통해 수행됩니다.
제어 파라미터
| 파라미터 | 기본값 | 설명 |
|---|---|---|
run | 0 | 0=정지, 1=실행, 2=병합 해제(unmerge all) |
pages_to_scan | 100 | ksmd가 한 주기에 스캔하는 최대 페이지 수 |
sleep_millisecs | 20 | 스캔 주기 간 휴식 시간 (ms) |
merge_across_nodes | 1 | 1=NUMA 노드 간 병합 허용, 0=같은 노드만 |
max_page_sharing | 256 | 하나의 KSM 페이지를 공유할 수 있는 최대 매핑 수 |
use_zero_pages | 0 | 1=영값 페이지를 커널 zero page와 병합 |
advisor_mode | none | none/scan-time/cpu-percent 자동 튜닝 모드 |
advisor_max_pages_to_scan | 30000 | advisor 모드 최대 pages_to_scan |
advisor_min_pages_to_scan | 500 | advisor 모드 최소 pages_to_scan |
advisor_max_cpu | 70 | cpu-percent 모드 최대 CPU 사용률 (%) |
advisor_target_scan_time | 200 | scan-time 모드 목표 전체 스캔 시간 (초) |
통계 카운터 (읽기 전용(Read-Only))
| 카운터 | 설명 |
|---|---|
pages_shared | 현재 KSM 페이지 수 (stable tree 노드 수) |
pages_sharing | KSM 페이지를 공유하는 총 매핑 수 (절약된 페이지 수) |
pages_unshared | unstable tree에 있는 후보 페이지 수 |
pages_volatile | 내용이 자주 변경되어 병합 불가한 페이지 수 |
zero_pages_sharing | 커널 zero page와 병합된 매핑 수 |
full_scans | 전체 스캔 완료 횟수 (seqnr) |
stable_node_chains | max_page_sharing 초과로 체인된 stable 노드 수 |
general_profit | KSM으로 절약된 총 메모리 (바이트) |
절약 페이지 = pages_sharing + zero_pages_sharing - pages_shared절약 메모리 = 절약 페이지 * 4096 바이트또는
general_profit 카운터를 직접 읽으면 rmap_item 오버헤드를 차감한 순 절약량을 얻을 수 있습니다.
sysfs 파라미터 실전 활용
각 sysfs 파라미터를 읽고 쓰는 실전 예시와 변경 시 미치는 영향을 살펴봅니다.
# ─── 기본 상태 확인 및 활성화 ───
cat /sys/kernel/mm/ksm/run # 현재 상태: 0=정지, 1=실행, 2=언머지
echo 1 > /sys/kernel/mm/ksm/run # KSM 활성화
# ─── 스캔 속도 조절 ───
# pages_to_scan × (1000 / sleep_millisecs) = 초당 스캔 페이지
# 예: 2000 × (1000/20) = 100,000 pages/sec = 약 390MB/sec
echo 2000 > /sys/kernel/mm/ksm/pages_to_scan
echo 20 > /sys/kernel/mm/ksm/sleep_millisecs
# ─── zero page 최적화: 영값 페이지를 커널 zero page로 대체 ───
# 효과: 새로 할당 후 memset(0)한 영역에서 큰 절약
echo 1 > /sys/kernel/mm/ksm/use_zero_pages
# ─── max_page_sharing 조절 ───
# 높이면: 더 많은 프로세스가 공유 가능 → 메모리 절약 증가
# 낮추면: rmap 탐색 비용 감소 → COW fault 시 지연 감소
echo 512 > /sys/kernel/mm/ksm/max_page_sharing
# ─── advisor 모드: 자동 튜닝 (v6.4+) ───
# scan-time 모드: 전체 스캔 시간 목표 (초)
echo scan-time > /sys/kernel/mm/ksm/advisor_mode
echo 120 > /sys/kernel/mm/ksm/advisor_target_scan_time
echo 500 > /sys/kernel/mm/ksm/advisor_min_pages_to_scan
echo 30000 > /sys/kernel/mm/ksm/advisor_max_pages_to_scan
# cpu-percent 모드: ksmd CPU 사용률 상한
echo cpu-percent > /sys/kernel/mm/ksm/advisor_mode
echo 10 > /sys/kernel/mm/ksm/advisor_max_cpu # 최대 10%
# advisor 비활성화
echo none > /sys/kernel/mm/ksm/advisor_mode
설명
pages_to_scan과 sleep_millisecs의 조합이 스캔 속도를 결정합니다. pages_to_scan을 높이면 병합 수렴이 빨라지지만 ksmd의 CPU 사용량이 증가합니다. advisor_mode를 사용하면 커널이 자동으로 pages_to_scan 값을 조절하여 목표 스캔 시간이나 CPU 사용률을 유지합니다. use_zero_pages=1은 영값 페이지를 커널 전역 zero page와 병합하므로, 추가 stable_node 오버헤드 없이 메모리를 절약합니다.
최적화 설정 프로파일
# ━━━ 프로파일 1: KVM 가상화 환경 (공격적 병합) ━━━
# 동일 OS 이미지 VM 10대 이상, 메모리 절약 최우선
echo 1 > /sys/kernel/mm/ksm/run
echo 3000 > /sys/kernel/mm/ksm/pages_to_scan
echo 10 > /sys/kernel/mm/ksm/sleep_millisecs
echo 1 > /sys/kernel/mm/ksm/use_zero_pages
echo 1 > /sys/kernel/mm/ksm/merge_across_nodes # NUMA 간 병합 허용
echo 512 > /sys/kernel/mm/ksm/max_page_sharing
# ━━━ 프로파일 2: 컨테이너 환경 (균형잡힌) ━━━
# 동일 이미지 컨테이너 50-100개, CPU 오버헤드 제한
echo 1 > /sys/kernel/mm/ksm/run
echo cpu-percent > /sys/kernel/mm/ksm/advisor_mode
echo 10 > /sys/kernel/mm/ksm/advisor_max_cpu
echo 1 > /sys/kernel/mm/ksm/use_zero_pages
echo 0 > /sys/kernel/mm/ksm/merge_across_nodes # NUMA 로컬 우선
# ━━━ 프로파일 3: 보수적 (테스트/모니터링 우선) ━━━
# 프로덕션 첫 도입, 영향 최소화
echo 1 > /sys/kernel/mm/ksm/run
echo 200 > /sys/kernel/mm/ksm/pages_to_scan
echo 100 > /sys/kernel/mm/ksm/sleep_millisecs # 느린 스캔
echo 0 > /sys/kernel/mm/ksm/use_zero_pages # zero page 최적화 미사용
echo 256 > /sys/kernel/mm/ksm/max_page_sharing
설명
가상화 프로파일은 스캔 속도를 최대화하여 빠른 병합 수렴을 목표로 합니다. 초당 약 300,000 페이지(1.2GB)를 스캔하므로, ksmd가 CPU 코어 하나를 상당 부분 점유합니다. 컨테이너 프로파일은 advisor_mode=cpu-percent로 CPU 오버헤드를 10% 이내로 제한하면서, NUMA 로컬 병합으로 접근 지연(Latency)을 최소화합니다. 보수적 프로파일은 첫 도입 시 KSM의 효과를 측정하면서 워크로드에 미치는 영향을 최소화합니다.
병합 내부 구현 상세
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() 상세
/* mm/ksm.c — 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;
}
mmap_read_lock을 잡고 PTE를 확인하지만, 다른 스레드(Thread)가 동시에 같은 페이지에 쓰기를 시도할 수 있습니다. 따라서 write_protect_page()로 PTE를 먼저 read-only로 만든 후, pages_identical()로 한 번 더 내용을 확인합니다. 이 시점에서 다른 스레드가 쓰기를 하면 COW가 발생하여 다른 페이지가 되므로, 안전하게 병합할 수 있습니다.
두 후보 페이지의 병합
unstable tree에서 매치를 발견한 경우, 두 일반 페이지를 새 KSM 페이지로 병합합니다.
/* mm/ksm.c — 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()를 수행합니다.
/* mm/ksm.c — memcmp_pages() (간략화) */
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;
}
memcmp()는 SSE/AVX 벡터 명령어로 최적화되어 있어, 4KB 비교는 일반적으로 1~2 마이크로초 내에 완료됩니다. 그러나 캐시(Cache) 미스가 발생하면(cold page) 수십 마이크로초까지 증가할 수 있습니다.
write_protect_page() 상세
KSM 병합의 핵심 전제 조건은 병합 대상 페이지의 내용이 병합 시점에도 동일해야 하는 것입니다. write_protect_page()는 이를 보장하기 위해 PTE를 read-only로 변경하고, 이후 다른 스레드가 쓰기를 시도하면 COW fault가 발생하도록 합니다.
/* 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;
}
write_protect_page()는 단순히 PTE를 R/O로 바꾸는 것이 아니라, page_mapcount와 page_count의 관계를 검증하여 다른 참조(GUP pin, swap cache 등)가 없는지 확인합니다. 예를 들어 get_user_pages()로 직접 접근 중인 페이지를 병합하면 데이터 손상이 발생할 수 있으므로, 이런 경우 병합을 거부합니다.
KSM 페이지 마이그레이션
커널의 페이지 마이그레이션(page migration)은 NUMA 밸런싱, 메모리 핫플러그(Hotplug), compaction 등에서 물리 페이지를 이동시킵니다. KSM 페이지는 여러 프로세스가 공유하므로 마이그레이션 시 특별한 처리가 필요합니다.
/* 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=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);
}
mprotect 호출 시 KSM 페이지 PTE 변화 과정
mprotect()가 KSM 페이지를 포함하는 영역에 호출되면, 커널은 VMA의 권한(vm_page_prot)을 변경하되 PTE 레벨에서 KSM 페이지의 write-protect는 유지합니다. 전체 경로는 다음과 같습니다:
/* mprotect(addr, len, PROT_READ|PROT_WRITE) 호출 시 KSM 페이지 경로 */
/* 1단계: sys_mprotect() → do_mprotect_pkey() */
/* VMA를 찾아 vm_flags를 VM_READ|VM_WRITE로 갱신 */
/* vm_page_prot도 갱신 (향후 새 PTE의 기본 보호 속성) */
/* 2단계: change_protection() → change_pte_range() */
for (각 PTE) {
page = vm_normal_page(vma, addr, pte);
if (PageKsm(page)) {
/* KSM 페이지: write 비트 강제 제거
* VMA가 PROT_WRITE여도 PTE는 R/O 유지
*
* PTE 상태: [PFN=KSM_page | R/O | Present]
*
* 이후 쓰기 시:
* CPU → write fault → do_wp_page()
* → PageKsm(page) 확인
* → COW: 새 페이지 할당 + 내용 복사
* → 새 PTE: [PFN=new_page | R/W | Present] */
newpte = pte_wrprotect(newpte);
} else {
/* 일반 페이지: vm_page_prot에 따라 write 비트 설정 */
newpte = pte_modify(oldpte, newprot);
}
set_pte_at(mm, addr, ptep, newpte);
}
설명
mprotect(PROT_WRITE)는 VMA 수준에서 쓰기를 허용하지만, KSM 페이지의 PTE에는 write 비트를 설정하지 않습니다. 이는 KSM 페이지가 여러 프로세스에서 공유되기 때문입니다. 쓰기를 허용하면 한 프로세스의 쓰기가 다른 프로세스의 데이터를 손상시킵니다. 대신 VMA에 VM_WRITE가 설정되어 있으므로, write fault 시 do_wp_page()가 COW를 수행하여 개별 복사본을 생성합니다. COW 후 새 페이지는 VM_WRITE에 따라 R/W PTE를 받습니다.
write 보호 해제 시 COW 트리거 메커니즘
/* mm/memory.c - do_wp_page() KSM COW 경로 (간략화) */
static vm_fault_t do_wp_page(struct vm_fault *vmf)
{
struct page *old_page = vmf->page;
if (PageKsm(old_page)) {
/* KSM COW 경로:
* 1. 새 페이지 할당 (현재 NUMA 노드 선호) */
struct page *new_page = alloc_page_vma(
GFP_HIGHUSER_MOVABLE, vma, vmf->address);
/* 2. KSM 페이지 내용 복사 (4KB memcpy) */
copy_user_highpage(new_page, old_page,
vmf->address, vma);
/* 3. PTE를 새 페이지로 교체 (R/W 설정) */
entry = mk_pte(new_page, vma->vm_page_prot);
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
set_pte_at_notify(mm, vmf->address,
vmf->pte, entry);
/* 4. KSM rmap_item에서 이 매핑 제거 */
break_ksm(vma, vmf->address, true);
/* 5. cow_ksm 카운터 증가 (/proc/vmstat) */
count_vm_event(COW_KSM);
/* 6. 기존 KSM 페이지 참조 감소
* (다른 프로세스가 아직 공유 중이면 유지) */
put_page(old_page);
}
}
설명
KSM 페이지에 대한 write fault가 발생하면 do_wp_page()는 PageKsm()을 확인하고 COW를 수행합니다. 새 페이지는 현재 프로세스의 NUMA 노드에서 할당되며, 기존 KSM 페이지의 내용을 복사합니다. PTE는 새 페이지를 가리키도록 교체되고, break_ksm()이 해당 rmap_item에서 이 매핑을 제거합니다. 기존 KSM 페이지는 다른 프로세스가 여전히 공유하고 있으면 해제되지 않습니다. /proc/vmstat의 cow_ksm 카운터로 KSM COW 빈도를 모니터링할 수 있습니다.
스왑과 KSM 상호작용
KSM 페이지가 스왑 아웃되면 특별한 처리가 필요합니다. 병합된 KSM 페이지는 여러 프로세스의 PTE가 가리키고 있으므로, 스왑 시에도 공유 관계를 유지해야 합니다.
스왑 아웃 시 동작
| 단계 | 동작 | 설명 |
|---|---|---|
| 1 | KSM 페이지 선택 | kswapd/direct reclaim이 KSM 페이지를 스왑 대상으로 선택 |
| 2 | 스왑 엔트리 할당 | 스왑 슬롯 하나를 할당 (ksm_count 참조) |
| 3 | PTE 교체 | 모든 공유 PTE를 swap entry로 교체 |
| 4 | 페이지 기록 | 페이지 내용을 스왑 디바이스에 한 번만 기록 |
| 5 | stable_node 유지 | 스왑 아웃되어도 stable_node는 유지 (kpfn 대신 swap entry 저장) |
스왑 인 시 동작
스왑 인 시에는 두 가지 경로가 있습니다:
스왑 아웃 커널 코드 경로
/* mm/vmscan.c → mm/rmap.c - KSM 페이지 스왑 아웃 경로 (간략화) */
/* 1단계: shrink_folio_list()에서 KSM 페이지 선택 */
static unsigned int shrink_folio_list(...)
{
/* KSM 페이지는 PageKsm() == true
* LRU에서 일반 익명 페이지와 동일하게 관리 */
/* 2단계: 스왑 슬롯 할당 */
swp_entry_t entry = get_swap_page(folio);
/* 3단계: rmap_walk()로 모든 공유 PTE를 순회
* KSM 페이지의 rmap은 stable_node→rmap_item 체인
* 각 rmap_item이 하나의 VMA/mm 매핑을 나타냄 */
try_to_unmap(folio, TTU_BATCH_FLUSH);
/* → try_to_unmap_one()에서:
* 각 PTE를 swap entry로 교체
* swp_pte = swp_entry_to_pte(entry);
* set_pte_at(mm, addr, pte, swp_pte);
*
* swap_duplicate()로 스왑 엔트리 참조 카운트 증가
* (N개 프로세스 공유 → swap count = N) */
/* 4단계: 페이지 내용을 스왑 디바이스에 기록 (1회만) */
swap_writepage(folio, &wbc);
/* 5단계: 물리 페이지 해제
* stable_node는 유지 (swap entry 참조) */
}
설명
KSM 페이지의 스왑 아웃은 일반 익명 페이지와 유사하지만, rmap_walk()가 KSM 고유의 rmap 체인(stable_node → rmap_item)을 순회한다는 차이가 있습니다. 하나의 KSM 페이지를 N개 프로세스가 공유하면, 스왑 엔트리의 참조 카운트(Reference Count)가 N이 됩니다. 페이지 내용은 스왑 디바이스에 한 번만 기록되므로, N개의 개별 페이지를 각각 스왑하는 것보다 I/O가 1/N로 줄어듭니다.
swap entry에서 KSM 페이지 복원
/* mm/memory.c - do_swap_page() KSM 복원 경로 (간략화) */
static vm_fault_t do_swap_page(struct vm_fault *vmf)
{
swp_entry_t entry = pte_to_swp_entry(vmf->orig_pte);
/* 경로 1: swap cache에 이미 존재하는 경우
* (다른 프로세스가 먼저 스왑 인 완료)
* → 디스크 I/O 없이 바로 매핑 */
folio = swap_cache_get_folio(entry, vma, vmf->address);
if (!folio) {
/* 경로 2: swap cache에 없음
* → 스왑 디바이스에서 읽기 */
folio = read_swap_cache_async(entry, ...);
/* 비동기 I/O 완료 후 swap cache에 추가 */
}
page = folio_file_page(folio, swp_offset(entry));
/* KSM 페이지인지 확인 */
if (PageKsm(page)) {
/* KSM 페이지: write-protect PTE로 매핑
* → COW 보호 유지 */
pte = mk_pte(page, vma->vm_page_prot);
pte = pte_wrprotect(pte); /* R/O 강제 */
}
set_pte_at(mm, vmf->address, vmf->pte, pte);
/* 스왑 엔트리 참조 카운트 감소
* 모든 프로세스가 스왑 인 완료하면 스왑 슬롯 해제 */
swap_free(entry);
}
설명
스왑 인 시 첫 번째 프로세스는 디스크에서 페이지를 읽어 swap cache에 추가합니다. 이후 같은 KSM 페이지를 공유하던 다른 프로세스들은 swap cache에서 바로 찾아 매핑하므로 추가 I/O가 없습니다. KSM 페이지는 스왑 인 후에도 write-protect PTE로 매핑되어 COW 보호를 유지합니다. 모든 공유 프로세스가 스왑 인을 완료하면 스왑 엔트리의 참조 카운트가 0이 되어 스왑 슬롯이 해제됩니다.
/proc/vmstat의 ksm_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) | 4KB | 4KB | 8KB |
| KSM 병합 후 | 2KB (1/2) | 2KB (1/2) | 4KB |
| COW 분리 후 (A가 쓰기) | 4KB | 4KB | 8KB |
cgroup v2 KSM accounting 상세
cgroup v2에서 KSM 관련 통계는 memory.stat 파일의 ksm 항목에 보고됩니다. 이 값은 해당 cgroup에 속하는 프로세스들이 KSM으로 공유 중인 메모리의 총량(바이트)입니다.
# ─── memory.stat에서 KSM 통계 읽기 ───
# 특정 cgroup의 KSM 공유 메모리 확인
cat /sys/fs/cgroup/my-app/memory.stat | grep '^ksm '
# ksm 52428800 → 이 cgroup에서 KSM으로 공유 중인 메모리 (50MB)
# memory.stat의 KSM 관련 전체 항목 확인
grep -E '^(ksm|anon|file) ' /sys/fs/cgroup/my-app/memory.stat
# anon 209715200 → 익명 메모리 총량 (200MB)
# file 104857600 → 파일 매핑 메모리 (100MB)
# ksm 52428800 → KSM 공유 메모리 (50MB)
# → KSM 절약률: 50MB / 200MB = 25%
# ─── 전체 cgroup 대비 KSM 효율 비교 스크립트 ───
echo "=== cgroup별 KSM 절약 현황 ==="
printf "%-30s %10s %10s %8s\n" "CGROUP" "ANON(MB)" "KSM(MB)" "RATIO"
for cg in /sys/fs/cgroup/*/; do
[ -f "$cg/memory.stat" ] || continue
anon=$(grep '^anon ' "$cg/memory.stat" | awk '{print $2}')
ksm=$(grep '^ksm ' "$cg/memory.stat" | awk '{print $2}')
if [ -n "$ksm" ] && [ "$ksm" -gt 0 ] 2>/dev/null; then
anon_mb=$(( anon / 1048576 ))
ksm_mb=$(( ksm / 1048576 ))
if [ "$anon" -gt 0 ]; then
ratio=$(( ksm * 100 / anon ))
else
ratio=0
fi
printf "%-30s %10d %10d %7d%%\n" \
"$(basename $cg)" "$anon_mb" "$ksm_mb" "$ratio"
fi
done
설명
memory.stat의 ksm 항목은 해당 cgroup 내 프로세스들이 KSM으로 공유 중인 메모리의 합계입니다. anon 대비 ksm 비율이 높을수록 KSM의 효과가 큽니다. 이 비율이 워크로드 변경으로 급감하면 COW 분리가 발생하여 메모리 사용량이 급증할 수 있으므로, 모니터링 지표로 활용해야 합니다.
cgroup 단위 KSM 활성화/비활성화
KSM 자체는 시스템 전역 설정이지만, cgroup 단위로 제어하는 방법이 있습니다:
# ─── 방법 1: systemd 서비스 단위 제어 (v254+) ───
# /etc/systemd/system/my-app.service.d/ksm.conf
[Service]
MemoryKSM=yes # prctl(PR_SET_MEMORY_MERGE, 1) 자동 호출
# 확인: systemd 슬라이스별 KSM 상태
systemctl show my-app.service --property=MemoryKSM
# MemoryKSM=yes
# ─── 방법 2: cgroup 레벨 prctl 래퍼 ───
# 특정 cgroup의 프로세스에만 KSM 적용
for pid in $(cat /sys/fs/cgroup/my-app/cgroup.procs); do
# nsenter로 해당 프로세스 네임스페이스에서 prctl 호출
nsenter -t "$pid" -m -- \
/usr/bin/python3 -c "import ctypes; ctypes.CDLL('libc.so.6').prctl(67, 1, 0, 0, 0)"
done
# ─── 방법 3: 특정 cgroup에서 KSM 비활성화 ───
# (보안 민감 워크로드: side-channel 방지)
for pid in $(cat /sys/fs/cgroup/secure-app/cgroup.procs); do
nsenter -t "$pid" -m -- \
/usr/bin/python3 -c "import ctypes; ctypes.CDLL('libc.so.6').prctl(67, 0, 0, 0, 0)"
done
설명
systemd v254부터 MemoryKSM=yes 옵션으로 서비스 단위 KSM 제어가 가능합니다. 이 옵션은 서비스 시작 시 prctl(PR_SET_MEMORY_MERGE, 1)을 호출하여 해당 프로세스의 모든 익명 매핑을 KSM에 등록합니다. cgroup v2 자체에는 KSM on/off 파라미터가 없으므로, 프로세스 단위 prctl()이나 madvise()로 제어해야 합니다. 보안이 중요한 워크로드에서는 side-channel 방지를 위해 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 동작 논리
- 메모리 부족 감지: 여유 메모리가
KSM_THRES_COEF% 이하이면pages_to_scan을NPAGES_BOOST만큼 증가시킵니다. - 메모리 충분: 여유 메모리가 충분하면
pages_to_scan을NPAGES_DECAY만큼 감소시킵니다. - 범위 제한:
NPAGES_MIN~NPAGES_MAX범위를 초과하지 않습니다. - 비활성화: 메모리가 매우 충분하면
run=0으로 ksmd를 정지시킵니다.
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=n | echo 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/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 비활성화
컨테이너 환경 활용
컨테이너는 VM보다 메모리 공유 기회가 적지만(커널은 이미 공유), 동일 베이스 이미지에서 실행되는 수백 개의 컨테이너에서 런타임 데이터, 힙 초기화 영역, 언어 런타임 구조체 등의 중복이 발생할 수 있습니다.
| 시나리오 | KSM 효과 | 권장 여부 |
|---|---|---|
| 동일 Java 앱 100개 Pod | JVM 메타데이터, 클래스 로더(Loader) 영역 병합 | 효과적 |
| 동일 Node.js 앱 100개 Pod | V8 힙 초기 구조 병합 | 중간 효과 |
| 이기종 앱 혼합 | 공통 부분 적어 비용 대비 효과 낮음 | 비권장 |
| 데이터베이스 컨테이너 | 버퍼(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
Kubernetes Pod에서 KSM 활용
# ─── Kubernetes DaemonSet으로 모든 노드에 KSM 적용 ───
# ksm-daemonset.yaml
# apiVersion: apps/v1
# kind: DaemonSet
# metadata:
# name: ksm-enabler
# spec:
# template:
# spec:
# hostPID: true
# containers:
# - name: ksm
# image: busybox
# securityContext:
# privileged: true
# command: ["/bin/sh", "-c"]
# args:
# - |
# echo 1 > /sys/kernel/mm/ksm/run
# echo 2000 > /sys/kernel/mm/ksm/pages_to_scan
# echo 1 > /sys/kernel/mm/ksm/use_zero_pages
# echo cpu-percent > /sys/kernel/mm/ksm/advisor_mode
# echo 10 > /sys/kernel/mm/ksm/advisor_max_cpu
# sleep infinity
# ─── Kubernetes 노드에서 Pod별 KSM 효과 확인 ───
# Pod의 cgroup 경로에서 KSM 통계 확인
POD_CGROUP=$(kubectl exec my-pod -- cat /proc/self/cgroup | \
grep '^0::' | cut -d: -f3)
cat /sys/fs/cgroup${POD_CGROUP}/memory.stat | grep '^ksm '
# 노드 전체 KSM 절약 현황
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' | \
while read node; do
echo "--- $node ---"
ssh "$node" 'cat /sys/kernel/mm/ksm/general_profit'
done
Docker에서 KSM 설정
# ─── Docker 호스트에서 KSM 활성화 ───
# 호스트 레벨 설정 (모든 컨테이너에 적용)
echo 1 > /sys/kernel/mm/ksm/run
echo 1000 > /sys/kernel/mm/ksm/pages_to_scan
echo 1 > /sys/kernel/mm/ksm/use_zero_pages
# ─── Docker 컨테이너에서 prctl로 KSM 등록 ───
# Dockerfile에서 앱 시작 전 KSM 등록
# (컨테이너 내부에서 prctl 호출 필요)
docker run --rm -it my-java-app sh -c '
python3 -c "
import ctypes, os
libc = ctypes.CDLL(\"libc.so.6\")
# PR_SET_MEMORY_MERGE = 67
libc.prctl(67, 1, 0, 0, 0)
os.execlp(\"java\", \"java\", \"-jar\", \"/app/app.jar\")
"'
# ─── systemd 기반 Docker: MemoryKSM=yes 활용 ───
# /etc/systemd/system/docker-my-app.service
[Service]
MemoryKSM=yes
ExecStart=/usr/bin/docker run --name my-app my-image
# ─── 특정 컨테이너의 KSM 통계 확인 ───
CONTAINER_ID=$(docker inspect --format '{{.Id}}' my-container)
CGROUP_PATH="/sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope"
grep '^ksm ' "${CGROUP_PATH}/memory.stat"
설명
Kubernetes와 Docker에서 KSM은 호스트 레벨에서 활성화합니다. 컨테이너 내부의 프로세스는 prctl(PR_SET_MEMORY_MERGE)로 자신의 익명 메모리를 KSM에 등록합니다. systemd v254+에서는 MemoryKSM=yes로 서비스 단위 제어가 가능합니다. DaemonSet을 사용하면 Kubernetes 클러스터의 모든 노드에 일관된 KSM 설정을 적용할 수 있습니다.
컨테이너 밀도 향상 측정
# ─── KSM 활성화 전후 메모리 사용량 비교 스크립트 ───
#!/bin/bash
# measure-ksm-density.sh
# 동일 이미지 컨테이너 N개 실행 후 KSM 효과 측정
NUM_CONTAINERS=50
IMAGE="my-java-app:latest"
# 1단계: KSM 없이 컨테이너 실행
echo 0 > /sys/kernel/mm/ksm/run
for i in $(seq 1 $NUM_CONTAINERS); do
docker run -d --name "test-$i" "$IMAGE"
done
sleep 60
MEM_BEFORE=$(free -b | awk '/Mem:/{print $3}')
echo "KSM OFF: 사용 메모리 = $(( MEM_BEFORE / 1048576 )) MB"
# 2단계: KSM 활성화 후 수렴 대기
echo 1 > /sys/kernel/mm/ksm/run
echo 3000 > /sys/kernel/mm/ksm/pages_to_scan
echo 10 > /sys/kernel/mm/ksm/sleep_millisecs
# full_scans가 3회 이상이면 수렴
while [ $(cat /sys/kernel/mm/ksm/full_scans) -lt 3 ]; do
sleep 10
done
MEM_AFTER=$(free -b | awk '/Mem:/{print $3}')
PROFIT=$(cat /sys/kernel/mm/ksm/general_profit)
SHARING=$(cat /sys/kernel/mm/ksm/pages_sharing)
echo "KSM ON: 사용 메모리 = $(( MEM_AFTER / 1048576 )) MB"
echo "절약량 (general_profit) = $(( PROFIT / 1048576 )) MB"
echo "공유 페이지 수 = $SHARING ($(( SHARING * 4 / 1024 )) MB)"
echo "밀도 향상: 약 $(( (MEM_BEFORE - MEM_AFTER) * 100 / MEM_BEFORE ))%"
# 정리
for i in $(seq 1 $NUM_CONTAINERS); do
docker rm -f "test-$i"
done
설명
이 스크립트는 동일 이미지의 컨테이너를 N개 실행한 후, KSM 활성화 전후의 물리 메모리 사용량을 비교합니다. full_scans가 3회 이상이면 대부분의 병합 가능한 페이지가 처리된 것으로 볼 수 있습니다. 동일 Java 앱 50개 실행 시 일반적으로 20-40%의 메모리 절약을 기대할 수 있으며, JVM 클래스 메타데이터와 초기화된 힙 영역이 주요 병합 대상입니다.
memory.stat에서 ksm 항목을 통해 그룹별 KSM 병합 현황을 확인할 수 있습니다. Kubernetes에서는 노드 레벨에서만 KSM을 제어하며, 개별 Pod 단위 제어는 지원되지 않습니다.
NUMA-aware KSM
NUMA 시스템에서 KSM 병합은 메모리 접근 지연 시간에 영향을 줍니다. 서로 다른 NUMA 노드의 페이지를 병합하면, 한쪽 프로세스는 원격 노드 접근(remote 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은 서로 상충하는 관계입니다.
| 특성 | THP | KSM |
|---|---|---|
| 페이지 크기 | 2MB (PMD 레벨) | 4KB (PTE 레벨) |
| 최적화 대상 | TLB 미스 감소 | 메모리 사용량 감소 |
| 메모리 효과 | 내부 단편화(Fragmentation) 증가 가능 | 물리 페이지 수 감소 |
| 공존 방식 | KSM은 THP를 4KB로 분할(split) 후 병합 | |
KSM이 THP 영역의 페이지를 병합하려면 먼저 2MB huge page를 512개의 4KB 페이지로 분할해야 합니다. 이 분할(split)은 비용이 크고, THP의 TLB 미스 감소 효과를 상쇄합니다.
THP split이 발생하는 조건
다음 조건에서 KSM은 THP를 4KB 페이지로 분할합니다:
| 조건 | 트리거 경로 | 설명 |
|---|---|---|
| KSM 스캔 중 THP 발견 | ksm_scan_thread() → split_huge_page() | ksmd가 VM_MERGEABLE VMA를 스캔하면서 THP를 만나면 즉시 분할합니다 |
| madvise(MADV_MERGEABLE) 호출 | ksm_madvise() → __split_huge_pmd() | THP가 포함된 영역을 KSM에 등록하면 PMD 엔트리를 PTE 엔트리들로 확장합니다 |
| 부분 병합(partial merge) | try_to_merge_one_page() | THP의 512개 서브페이지 중 일부만 병합 가능하면 나머지는 일반 페이지로 유지됩니다 |
커널 코드 상호작용 경로
/* mm/ksm.c - ksmd 스캔 시 THP 처리 (간략화) */
static struct ksm_rmap_item *scan_get_next_rmap_item(...)
{
/* VMA 순회 중 PTE 레벨 접근 필요 */
if (pmd_trans_huge(*pmd)) {
/* THP 발견: 4KB 페이지들로 분할 */
__split_huge_pmd(vma, pmd, addr, false, NULL);
/* split 후 pmd는 일반 PTE 테이블을 가리킴
* → 이후 개별 PTE를 순회하며 KSM 후보로 등록 */
}
/* ... */
}
/* mm/huge_memory.c - __split_huge_pmd() (간략화) */
void __split_huge_pmd(struct vm_area_struct *vma,
pmd_t *pmd, unsigned long address,
bool freeze, struct folio *folio)
{
/* 1. PMD 엔트리를 잠금 */
spin_lock(&vma->vm_mm->page_table_lock);
/* 2. 512개 PTE 엔트리를 생성하여
* 각각 compound page의 서브페이지를 가리킴 */
/* 3. PMD를 PTE 테이블 포인터로 교체 */
pmd_populate(mm, pmd, pgtable);
/* 4. TLB flush (해당 2MB 영역) */
flush_tlb_range(vma, haddr, haddr + HPAGE_PMD_SIZE);
spin_unlock(&vma->vm_mm->page_table_lock);
}
설명
KSM은 PTE 레벨에서 동작하므로, THP(PMD 레벨 매핑)를 만나면 반드시 분할해야 합니다. __split_huge_pmd()는 2MB 영역의 PMD 엔트리를 512개의 PTE 엔트리로 변환합니다. 이 과정에서 새 PTE 테이블 할당, TLB flush, 참조 카운트 갱신이 필요하므로 비용이 큽니다. 분할 후 khugepaged는 이 영역을 다시 THP로 승격하려 시도하므로, split-collapse 순환이 발생할 수 있습니다.
ftrace/perf로 THP split 이벤트 추적
# ─── ftrace로 THP split 이벤트 추적 ───
# KSM에 의한 THP 분할 확인
echo 1 > /sys/kernel/debug/tracing/events/huge_memory/mm_khugepaged_scan_pmd/enable
echo 1 > /sys/kernel/debug/tracing/events/huge_memory/mm_collapse_huge_page/enable
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 워크로드 실행 후 결과 확인
cat /sys/kernel/debug/tracing/trace | head -50
# ─── perf로 THP split 빈도 측정 ───
perf stat -e 'thp_split_page,thp_split_pmd,thp_collapse_alloc' \
-a sleep 30
# ─── /proc/vmstat에서 THP split 누적 카운터 확인 ───
grep -E 'thp_split|thp_collapse' /proc/vmstat
# thp_split_page 1234 -- THP→4KB 분할 횟수
# thp_split_pmd 5678 -- PMD 레벨 분할 횟수
# thp_collapse_alloc 999 -- khugepaged의 THP 승격 시도
# ─── KSM과 THP 충돌 감지 스크립트 ───
# split이 collapse보다 훨씬 많으면 KSM-THP 충돌
split=$(awk '/thp_split_pmd/{print $2}' /proc/vmstat)
collapse=$(awk '/thp_collapse_alloc /{print $2}' /proc/vmstat)
if [ "$split" -gt $(( collapse * 3 )) ]; then
echo "경고: THP split($split) >> collapse($collapse)"
echo "KSM 환경에서 THP=madvise로 전환을 권장합니다"
fi
설명
ftrace의 huge_memory 이벤트 그룹에서 THP 분할과 승격 이벤트를 추적할 수 있습니다. perf stat으로 일정 시간 동안의 THP 이벤트 빈도를 측정하면 KSM에 의한 split 빈도를 정량화할 수 있습니다. thp_split_pmd가 thp_collapse_alloc보다 3배 이상 많다면 KSM과 khugepaged 간 충돌이 발생하고 있으므로, THP를 madvise 모드로 전환해야 합니다.
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)의 메모리 내용을 추론할 수 있습니다.
알려진 공격
| 공격 | 발표 | 원리 |
|---|---|---|
| Memory Disclosure via KSM | Suzaki et al., 2011 | KSM 병합 타이밍으로 VM 간 데이터 추론 |
| Flush+Reload on KSM | Gruss et al., 2015 | 캐시 타이밍 + KSM 병합 결합 공격 |
| CAIN Attack | Xiao et al., 2016 | VM 간 공유 라이브러리 탐지 -> 취약점(Vulnerability) 공격 대상 식별 |
| Dedup Est Machina | Bosman et al., 2016 | JavaScript에서 KSM 병합 탐지 -> ASLR 우회 |
완화 방법
- KSM 비활성화 -- 보안이 최우선인 환경(금융, 의료)에서는 KSM을 사용하지 않습니다.
- merge_across_nodes=0 + 프로세스 격리(Isolation) -- 노드 간 병합을 차단하고, 보안 도메인별 NUMA 노드를 분리합니다.
- 클라우드 환경 -- AWS, GCP, Azure 등 주요 CSP는 테넌트 간 KSM을 비활성화합니다.
- 지연 추가 -- 병합 후 일정 시간 지연을 두어 타이밍 차이를 관찰하기 어렵게 만듭니다.
CSP별 KSM 정책
| CSP/하이퍼바이저(Hypervisor) | 기본 정책 | 사유 |
|---|---|---|
| AWS (Xen/Nitro) | KSM 비활성화 | 테넌트 격리, side-channel 방지 |
| GCP (KVM) | KSM 비활성화 | 보안 우선, 전용 호스트에서만 선택적 허용 |
| Azure (Hyper-V) | TPS 비활성화 | 2014년 이후 보안 업데이트에서 기본 비활성화 |
| VMware vSphere | TPS inter-VM 비활성화 (6.0+) | VM 내(intra-VM)만 허용, inter-VM은 salt 필요 |
| KVM (자체 호스팅) | 사용자 선택 | 단일 테넌트 환경에서 활성화 권장 |
| Proxmox VE | KSM 비활성화 (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)을 소비합니다.
오버헤드 요소
| 요소 | 비용 | 비고 |
|---|---|---|
| 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초가 걸립니다. 실제 병합은 수 회 전체 스캔 후 수렴합니다.
성능 측정 방법
# ─── perf로 ksmd CPU 프로파일링 ───
# ksmd 스레드의 CPU 사용 함수 분석
KSMD_PID=$(pgrep ksmd)
perf record -g -p "$KSMD_PID" -- sleep 30
perf report --stdio --no-children | head -40
# 주요 핫스팟:
# memcmp_orig -- 4KB 바이트 비교 (가장 비용이 큼)
# calc_checksum -- CRC32 해시 계산
# rb_insert_color -- rbtree 삽입/균형 조정
# ─── perf stat으로 ksmd 하드웨어 카운터 측정 ───
perf stat -p "$KSMD_PID" -e \
'cycles,instructions,cache-misses,cache-references,LLC-load-misses' \
-- sleep 30
# cache-misses가 높으면: 메모리 버스 대역폭이 병목
# instructions/cycle이 낮으면: 메모리 접근 지연이 큼
# ─── ksmd의 COW fault 비율 측정 ───
# 30초 동안 cow_ksm 카운터 변화 관찰
cow_before=$(awk '/cow_ksm/{print $2}' /proc/vmstat)
sleep 30
cow_after=$(awk '/cow_ksm/{print $2}' /proc/vmstat)
echo "KSM COW fault/sec: $(( (cow_after - cow_before) / 30 ))"
설명
perf record로 ksmd의 CPU 사용 패턴을 분석하면 병목(Bottleneck) 지점을 파악할 수 있습니다. 일반적으로 memcmp(바이트 비교)가 가장 큰 비율을 차지하며, 이는 4KB 페이지 전체를 읽어야 하기 때문입니다. cache-misses가 높으면 메모리 버스 대역폭이 포화 상태이므로 pages_to_scan을 줄여야 합니다. cow_ksm 비율이 높으면 병합된 페이지가 자주 COW 분리되고 있어 KSM의 실효성이 낮음을 의미합니다.
bpftrace/perf로 워크로드별 오버헤드 측정
# ─── bpftrace: ksmd 한 주기의 소요 시간 분포 ───
bpftrace -e '
kprobe:ksm_do_scan { @start[tid] = nsecs; }
kretprobe:ksm_do_scan /@start[tid]/ {
@scan_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# 출력 예시: 스캔 주기별 소요 시간 히스토그램 (us)
# @scan_us:
# [64, 128) 23 |████████████ |
# [128, 256) 45 |████████████████████████████████ |
# [256, 512) 12 |████████ |
# ─── bpftrace: memcmp 호출 횟수와 시간 ───
bpftrace -e '
kprobe:memcmp_pages /comm == "ksmd"/ {
@start[tid] = nsecs;
@count++;
}
kretprobe:memcmp_pages /@start[tid]/ {
@cmp_ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
}
interval:s:10 { printf("memcmp calls in 10s: %d\n", @count); @count = 0; }'
# ─── 종합 오버헤드 측정 스크립트 ───
#!/bin/bash
# ksm-overhead.sh: 일정 시간 동안 KSM 오버헤드 측정
DURATION=60 # 측정 시간 (초)
echo "=== KSM 오버헤드 측정 (${DURATION}초) ==="
# 측정 시작 시점의 값
scans_s=$(cat /sys/kernel/mm/ksm/full_scans)
sharing_s=$(cat /sys/kernel/mm/ksm/pages_sharing)
shared_s=$(cat /sys/kernel/mm/ksm/pages_shared)
cow_s=$(awk '/cow_ksm/{print $2}' /proc/vmstat)
cpu_s=$(ps -p $(pgrep ksmd) -o cputime= | \
awk -F: '{print $1*3600+$2*60+$3}')
sleep "$DURATION"
# 측정 종료 시점의 값
scans_e=$(cat /sys/kernel/mm/ksm/full_scans)
sharing_e=$(cat /sys/kernel/mm/ksm/pages_sharing)
shared_e=$(cat /sys/kernel/mm/ksm/pages_shared)
cow_e=$(awk '/cow_ksm/{print $2}' /proc/vmstat)
cpu_e=$(ps -p $(pgrep ksmd) -o cputime= | \
awk -F: '{print $1*3600+$2*60+$3}')
profit=$(cat /sys/kernel/mm/ksm/general_profit)
echo "전체 스캔 완료: $(( scans_e - scans_s )) 회"
echo "공유 페이지 변화: $sharing_s → $sharing_e"
echo "KSM 페이지 변화: $shared_s → $shared_e"
echo "COW fault 발생: $(( cow_e - cow_s )) 회"
echo "ksmd CPU 사용: $(( cpu_e - cpu_s )) 초 / ${DURATION}초"
echo "CPU 사용률: $(( (cpu_e - cpu_s) * 100 / DURATION ))%"
echo "순 절약량: $(( profit / 1048576 )) MB"
설명
bpftrace를 사용하면 ksmd의 내부 함수 호출 시간을 나노초 단위로 측정할 수 있습니다. ksm_do_scan 프로브(Probe)로 한 주기의 스캔 시간 분포를 확인하고, memcmp_pages 프로브로 바이트 비교의 빈도와 소요 시간을 분석합니다. 종합 스크립트는 일정 시간 동안 KSM의 핵심 지표 변화를 캡처하여 CPU 사용률 대비 메모리 절약 효과를 정량화합니다. ksmd CPU 사용률이 5-10%를 초과하면서 general_profit 증가가 미미하다면, pages_to_scan을 줄이거나 advisor_mode를 활용해야 합니다.
모니터링
/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_scan | sleep_millisecs | use_zero_pages | merge_across_nodes |
|---|---|---|---|---|
| KVM VM 10대 (동일 OS) | 500 | 20 | 1 | 1 |
| KVM VM 50대 이상 | 2000 | 20 | 1 | 1 |
| NUMA 서버 (지연 민감) | 1000 | 20 | 1 | 0 |
| 컨테이너 100+ (동종) | 1000 | 50 | 1 | 1 |
| 보안 민감 환경 | 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
튜닝 체크리스트
- 효과 확인 --
pages_sharing이pages_unshared보다 현저히 적으면 KSM이 비효율적입니다.pages_volatile이 높으면 워크로드가 쓰기 집약적이므로 KSM 비활성화를 고려합니다. - 수렴 시간 확인 --
full_scans가 3~5회 이상 진행되어야 병합이 수렴합니다.pages_to_scan을 높여 수렴을 앞당길 수 있습니다. - CPU 모니터링 --
top에서ksmdCPU 사용률을 확인합니다. 5% 이상이면pages_to_scan을 줄이거나advisor_mode를 활성화합니다. - 메모리 오버헤드 확인 --
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의 개선 버전으로, 다음과 같은 차이점이 있습니다:
- 적응형 해싱: UKSM은 여러 해시 함수(SuperFastHash, DJB2, SDBM 등)를 사용하여 false positive를 줄입니다.
- 자동 스캔 속도 조절: 메모리 변동률에 따라 자동으로 스캔 속도를 조절합니다.
- 파일 백업 페이지 지원: 익명 페이지뿐 아니라 파일 매핑 페이지도 중복 제거합니다.
- 단점: 메인라인 커널에 포함되지 않아 패치 유지 보수가 필요하고, 보안 검증이 부족합니다.
KSM + zswap/ZRAM 조합
KSM과 메모리 압축(Memory Compaction) 기술은 서로 보완적입니다. KSM이 먼저 동일한 페이지를 제거하고, 나머지 고유 페이지 중 스왑 대상을 zswap이 압축합니다. 두 기술을 조합하면 메모리 효율을 극대화할 수 있습니다.
일반적인 처리 순서:
- KSM 병합: 동일 내용 페이지를 하나로 통합 (중복 제거)
- 페이지 회수: 여유 메모리 부족 시 LRU 기반 회수
- zswap 압축: 스왑 아웃 대상 페이지를 메모리 내 압축 저장
- 스왑 기록: 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_KSM | y (대부분 배포판) | KSM 서브시스템 활성화 |
CONFIG_KSM_ADVISOR | y (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 계열은 ksm과 ksmtuned 서비스를 제공합니다.
내부 자료구조
ksm_scan 커서
ksm_scan 전역 변수는 ksmd의 현재 스캔 위치를 기억합니다. ksmd가 휴식 후 재개할 때 이 커서에서부터 스캔을 계속합니다.
/* mm/ksm.c — struct ksm_scan 정의 (간략화) */
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 필드의 상위 비트를 플래그로 사용하여 현재 상태를 구분합니다.
| 상태 | 플래그 | 의미 | 위치 |
|---|---|---|---|
| NEW | 없음 | 할당 직후, 아직 스캔되지 않음 | rmap_list에만 존재 |
| CHECKSUM | SEQNR_MASK | oldchecksum이 설정됨, 다음 스캔에서 비교 예정 | rmap_list |
| UNSTABLE | UNSTABLE_FLAG | seqnr | unstable tree에 삽입됨, rb_node로 연결 | unstable tree + rmap_list |
| STABLE | STABLE_FLAG | stable tree의 stable_node에 hlist로 연결됨 | stable tree + rmap_list |
| INVALID | seqnr 불일치 | 이전 스캔 주기의 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; /* 플래그 초기화 */
}
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 page의 page->flags에 PG_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, 유휴 | 10 | 20 GB | 65% (13 GB) | ~5분 | 2-3% |
| 동일 Ubuntu, 웹서버 | 10 | 20 GB | 45% (9 GB) | ~8분 | 3-5% |
| 혼합 OS (Ubuntu+CentOS) | 10 | 20 GB | 25% (5 GB) | ~10분 | 4-6% |
| 동일 Java 앱 컨테이너 | 50 | 25 GB | 30% (7.5 GB) | ~15분 | 5-8% |
| 이기종 앱 혼합 | 20 | 40 GB | 10% (4 GB) | ~20분 | 5-7% |
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이 0 | run이 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
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_volatile이 pages_sharing보다 높으면 비활성화 고려 |
| KSM 절약분에 의존한 overcommit | 워크로드 변경 시 대량 COW → OOM | KSM 절약분의 20~30%를 여유로 확보. memory.max 설정 시 안전 마진 포함 |
run=2로 언머지 시 메모리 미확인 | 가용 메모리 부족 → OOM | free -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 페이지 공유 → 한꺼번에 COW | fork 후 exec 패턴이면 KSM 효과적. 장시간 실행 자식에는 개별 madvise 관리 |
효율 비율 = pages_sharing / (pages_sharing + pages_unshared + pages_volatile)이 비율이 0.5 미만이면 KSM의 CPU 비용 대비 효과가 낮습니다.
general_profit이 양수인지도 함께 확인하세요.
안티 패턴 재현과 해결 코드 비교
자주 발생하는 실수의 구체적인 코드 예시와 올바른 해결 방법을 비교합니다.
실수 1: madvise 미호출 (pages_sharing이 0인 채 유지)
/* ✗ 잘못된 코드: KSM 활성화만 하고 madvise 미호출 */
void *buf = mmap(NULL, size, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
/* KSM이 실행 중(run=1)이지만 이 영역은
* VM_MERGEABLE이 아니므로 스캔 대상에서 제외됨
* → pages_sharing은 영원히 0 */
/* ✓ 올바른 코드: madvise로 KSM 대상 등록 */
void *buf = mmap(NULL, size, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
madvise(buf, size, MADV_MERGEABLE);
/* 또는 프로세스 전체를 대상으로: */
prctl(PR_SET_MEMORY_MERGE, 1, 0, 0, 0);
실수 2: 쓰기 집약 영역에 KSM 적용
/* ✗ 잘못된 코드: 자주 변경되는 버퍼에 KSM 적용 */
char *work_buf = mmap(NULL, BUF_SIZE, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
madvise(work_buf, BUF_SIZE, MADV_MERGEABLE);
/* 매 초 버퍼 내용 변경 → pages_volatile 급증
* ksmd가 해시/비교하지만 병합 불가 → CPU만 낭비 */
while (1) {
process_data(work_buf); /* 매번 쓰기 */
}
/* ✓ 올바른 코드: 읽기 전용 영역만 선별 등록 */
char *ro_config = mmap(...); /* 설정 데이터 (거의 불변) */
char *work_buf = mmap(...); /* 작업 버퍼 (자주 변경) */
madvise(ro_config, cfg_size, MADV_MERGEABLE); /* 읽기 전용만 */
/* work_buf는 KSM에 등록하지 않음 */
실수 3: run=2 언머지 시 메모리 부족
# ✗ 잘못된 방법: 메모리 확인 없이 즉시 언머지
echo 2 > /sys/kernel/mm/ksm/run
# pages_sharing=500000 (2GB) → 2GB 추가 메모리 필요
# 여유 메모리 1GB → OOM 발생!
# ✓ 올바른 방법: 언머지 전 메모리 여유 확인
sharing=$(cat /sys/kernel/mm/ksm/pages_sharing)
needed_mb=$(( sharing * 4 / 1024 ))
avail_mb=$(awk '/MemAvailable/{print int($2/1024)}' /proc/meminfo)
echo "언머지에 필요한 메모리: ${needed_mb} MB"
echo "사용 가능 메모리: ${avail_mb} MB"
if [ "$avail_mb" -gt $(( needed_mb + 512 )) ]; then
echo "안전 마진 확보됨. 언머지를 시작합니다."
echo 2 > /sys/kernel/mm/ksm/run
# 완료 후 정지
while [ $(cat /sys/kernel/mm/ksm/pages_sharing) -gt 0 ]; do
sleep 1
done
echo 0 > /sys/kernel/mm/ksm/run
else
echo "경고: 메모리 부족! 언머지를 중단합니다."
echo "필요: ${needed_mb}MB, 가용: ${avail_mb}MB"
fi
실수 4: general_profit 음수 무시
# ✗ 잘못된 방법: KSM 효과를 확인하지 않고 계속 실행
echo 1 > /sys/kernel/mm/ksm/run
# 몇 시간 후 general_profit = -12345678 (음수!)
# → rmap_item 오버헤드가 절약량을 초과
# ✓ 올바른 방법: 주기적으로 효율 점검
profit=$(cat /sys/kernel/mm/ksm/general_profit)
sharing=$(cat /sys/kernel/mm/ksm/pages_sharing)
unshared=$(cat /sys/kernel/mm/ksm/pages_unshared)
volatile=$(cat /sys/kernel/mm/ksm/pages_volatile)
total=$(( sharing + unshared + volatile ))
if [ "$profit" -lt 0 ]; then
echo "경고: KSM general_profit이 음수 (${profit} bytes)"
echo "rmap_item 오버헤드가 절약량을 초과합니다."
echo "효율 비율: $(( sharing * 100 / (total + 1) ))%"
echo "비효율적인 프로세스에 MADV_UNMERGEABLE을 호출하거나"
echo "KSM 등록 영역을 축소하세요."
fi
설명
가장 흔한 실수는 KSM을 활성화(run=1)하고 madvise(MADV_MERGEABLE)을 호출하지 않는 것입니다. KSM은 VM_MERGEABLE 플래그가 설정된 VMA만 스캔하므로, 명시적 등록이 필수입니다. 쓰기 집약 워크로드에서는 pages_volatile이 높아지고 ksmd의 CPU만 소비하게 됩니다. run=2(언머지)는 모든 KSM 공유를 해제하므로 pages_sharing * 4KB만큼의 추가 메모리가 필요하며, 미리 여유를 확인해야 합니다. general_profit이 음수라면 rmap_item 메타데이터 오버헤드가 절약량을 초과한 것이므로, KSM 등록 범위를 줄여야 합니다.
KSM 전체 생명주기 요약
Linux 6.12 ~ 6.16 KSM 최신 동향
KSM은 2024년을 전후로 "자동화(KSM advisor, smart scan)"와 "관측성(per-process 통계)"이라는 두 축으로 크게 발전했습니다. 6.12 이후 KSM은 더 이상 "돌려놓고 방치"가 아니라 sysfs advisor가 부하에 맞게 자체 조정하는 프레임워크로 바뀌었습니다.
| 커널 | 릴리스 | KSM 주요 변경 | 실무 시사점 |
|---|---|---|---|
| 6.12 (LTS) | 2024-11 | ksm_advisor 정착 — /sys/kernel/mm/ksm/advisor_mode(none/scan_time), advisor_target_scan_time, advisor_max_cpu, advisor_min_pages(기본 500)/advisor_max_pages(기본 5000). Smart scan 기본 활성으로 페이지 스캔 20~25% 감소 | 수동 pages_to_scan 조정 없이도 부하 기반 자동 튜닝 가능 |
| 6.13 | 2025-01 | KSM 통계 세분화, smart scan 실패 빈도 추적 개선 | KSM 효율을 pages_shared/pages_sharing/full_scans 외에 스캔 정확도로 평가 가능 |
| 6.14 | 2025-03 | Per-process KSM 참여 통계 — /proc/<pid>/ksm_stat, ksm_merging_pages 노출 | 어느 프로세스가 KSM 공유의 주 수혜자/기여자인지 실시간 확인 가능 |
| 6.15 | 2025-05 | KSM과 MGLRU·memcg 상호작용 정리, smart scan 쓰로틀 개선 | 컨테이너 호스트에서 KSM이 MGLRU 회수와 충돌하는 사례 감소 |
| 6.16 | 2025-07 | KSM 대상 VMA 필터링 경로 정리, DAMON 연동 시나리오에 대비한 인프라 정리 | DAMON 기반 영역 선택 + KSM 공유로 컨테이너 밀도 최적화 실험 가능 |
advisor_mode=scan_time + 적절한 advisor_target_scan_time(예: 200ms)을 기본선으로 잡습니다. (2) 프로세스별 기여도는 /proc/<pid>/ksm_stat(6.14+)로 확인하세요. (3) madvise(MADV_MERGEABLE)를 명시적으로 지정한 영역만 공유되므로 KVM/QEMU는 자동이지만 일반 앱은 명시적 힌트가 필요합니다.
참고자료
커널 문서
- KSM Admin Guide -- KSM 관리자 가이드입니다
- KSM Internal Documentation -- KSM 내부 동작을 설명하는 문서입니다
- madvise(2) man page -- MADV_MERGEABLE 플래그에 대한 설명입니다
LWN 기사
- /dev/ksm: dynamic memory sharing (2008) -- KSM의 초기 설계에 대한 논의입니다
- KSM gets merged (2009) -- KSM이 커널에 병합된 과정을 다룹니다
- Process-level KSM control (2023) -- 프로세스 수준 KSM 제어 기능을 소개합니다
- Automerging with KSM (2023) -- KSM 자동 병합 기능에 대한 논의입니다
커널 소스
- mm/ksm.c -- KSM 핵심 구현 코드입니다
- include/linux/ksm.h -- KSM API 헤더 파일입니다
발표 자료
- Andrea Arcangeli, Izik Eidus, "KSM — Kernel Samepage Merging" (KVM Forum 2009) -- KSM의 설계 동기와 구현을 발표한 자료입니다