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)은 물리 메모리에서 동일한 내용을 가진 익명 페이지(anonymous page)를 자동으로 감지하고 하나의 물리 페이지로 병합하는 커널 서브시스템입니다. 병합된 페이지는 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 22.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() 디스패치 로직
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하게 발견 시 제거 |
/* 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 바이트 순서 (불안정) |
/* 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 페이지를 할당할 필요가 없으므로 메모리 절약 효과가 추가됩니다.
/* 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()를 호출하면 주소 공간이 교체되어 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 오버헤드를 차감한 순 절약량을 얻을 수 있습니다.
병합 내부 구현 상세
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;
}
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;
}
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 밸런싱, 메모리 핫플러그, 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);
}
스왑과 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 저장) |
스왑 인 시 동작
스왑 인 시에는 두 가지 경로가 있습니다:
/* 스왑 인 시 KSM 페이지 복원 경로 */
/* 경로 1: 첫 번째 접근 - 스왑에서 읽어서 새 페이지 생성 */
/* -> 이 페이지가 새로운 KSM 페이지가 됨 */
/* -> stable_node의 kpfn을 새 페이지로 업데이트 */
/* 경로 2: 이후 접근 - 이미 스왑 인된 KSM 페이지 존재 */
/* -> swap_cache에서 KSM 페이지를 찾아 공유 */
/* -> 추가 디스크 I/O 없음 */
/* ksm_swpin_copy: 스왑 인 시 COW가 필요한 경우 카운터 */
/* (KSM 페이지가 이미 COW 분리된 상태에서 스왑 인) */
/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 |
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
memory.stat에서 ksm 항목을 통해 그룹별 KSM 병합 현황을 확인할 수 있습니다. Kubernetes에서는 노드 레벨에서만 KSM을 제어하며, 개별 Pod 단위 제어는 지원되지 않습니다.
NUMA-aware KSM
NUMA 시스템에서 KSM 병합은 메모리 접근 지연(Latency) 시간에 영향을 줍니다. 서로 다른 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 미스 감소 효과를 상쇄합니다.
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초가 걸립니다. 실제 병합은 수 회 전체 스캔 후 수렴합니다.
모니터링
/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가 휴식 후 재개할 때 이 커서에서부터 스캔을 계속합니다.
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이 양수인지도 함께 확인하세요.
KSM 전체 생명주기 요약
참고자료
커널 문서
- 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의 설계 동기와 구현을 발표한 자료입니다