# Reverse Mapping (rmap)
관련 소스:mm/rmap.c,include/linux/rmap.h
Reverse Mapping(rmap)은 물리 페이지(folio)로부터 그 페이지가 매핑된 모든 가상 주소를 찾는 메커니즘입니다. Linux 커널에서 페이지 회수(reclaim), KSM(Kernel Samepage Merging), 페이지 마이그레이션, 페이지 제거(zap) 등 핵심 동작은 "물리 페이지 → 어떤 VMA에 매핑되어 있는가?"를 추적해야 하며, rmap이 이를 담당합니다.
rmap은 두 가지 유형의 매핑을 구분합니다: 익명(anon) 페이지는 anon_vma 체인을 통해 추적하고, 파일(file) 기반 페이지는 address_space->i_mmap interval tree를 통해 추적합니다. 각 folio의 mapping 필드가 anon_vma 또는 address_space를 가리키며, rmap walk 시 해당 구조체를 순회하여 모든 매핑 VMA를 찾습니다.
일상 비유로 보면 rmap은 도서관의 대출 카드 목록과 비슷합니다. 책 한 권(folio)은 실제로 여러 사람의 손에 건네질 수 있고, 반납·이동·폐기 시점에는 "이 책을 누가 어디에서 빌려 갔는가"를 바로 찾아야 합니다. rmap은 바로 그 역방향 색인입니다.
이 문서에서 보는 rmap은 페이지 폴트와 COW까지 이어집니다. 처음 접근한 주소는 페이지 폴트로 물리 페이지를 만들고, fork() 뒤 쓰기 시점에는 COW로 새 페이지를 복사합니다. 이후 회수나 마이그레이션이 오면 rmap이 각 VMA를 다시 찾아 mapcount와 PTE를 정리합니다.
소스 파일 경로:
mm/rmap.c ← rmap 핵심 구현 (3147줄)
include/linux/rmap.h ← rmap 공개 API, 구조체 정의 (995줄)
mm/internal.h ← 내부 유틸리티
rmap만 단독으로 보지 말고, 전체 메모리 압력과 페이지 폴트 추세를 같이 보면 원인을 잡기 쉽습니다.
# anon_vma 캐시 통계
grep -i "anon_vma" /proc/slabinfo
# 모든 메모리 맵에서 rmap 관련 정보 확인
cat /proc/<pid>/smaps | head -50
# 페이지 매핑 카운트 확인 (mapcount)
cat /proc/kpagecount | head -20
# rmap 관련 트레이스 이벤트 확인
ls /sys/kernel/debug/tracing/events/rmap/ 2>/dev/null
# folio mapcount 디버그 (CONFIG_DEBUG_VM 빌드)
cat /sys/kernel/debug/rmap_stats 2>/dev/null
# KSM이 rmap을 사용하는지 확인
cat /sys/kernel/mm/ksm/pages_shared
cat /sys/kernel/mm/ksm/pages_sharing
# 페이지 테이블 walk를 통한 매핑 확인
cat /proc/<pid>/pagemap | head -20
# 메모리 회수 시 rmap walk 동작 확인
echo 1 > /proc/sys/vm/drop_caches
dmesg | tail -20
# 페이지 폴트와 익명/파일 페이지 규모 확인
cat /proc/vmstat | grep -E 'pgfault|pgmajfault|nr_anon_pages|nr_file_pages|nr_page_table_pages'
cat /proc/meminfo | grep -E 'AnonPages|Mapped|Cached|SwapCached|PageTables'
# 전체 메모리 압력과 분포 확인
cat /proc/pressure/memory
cat /proc/buddyinfo
cat /proc/slabinfo | grep -E 'anon_vma|anon_vma_chain|vm_area_struct'
| 지표 | 의미 | 함께 보는 이유 |
|---|---|---|
| `AnonPages` | 익명 페이지 규모 | `rmap_walk_anon()`과 COW 활동이 많을수록 같이 커지기 쉽습니다. |
| `Cached` | 파일 페이지 규모 | `rmap_walk_file()` 경로와 page cache 회수가 자주 일어나는지 보기에 좋습니다. |
| `pgfault` / `pgmajfault` | 페이지 폴트 추세 | 새 매핑과 실제 I/O가 얼마나 자주 발생하는지 확인합니다. |
| `PageTables` | 페이지 테이블 비용 | mapcount 변화와 PTE 정리 부담을 같이 가늠할 수 있습니다. |
| `anon_vma` / `anon_vma_chain` | rmap 메타데이터 | fork, COW, 익명 페이지 수가 늘 때 slab 사용량이 함께 커집니다. |
익명 페이지의 역방향 매핑을 관리하는 핵심 구조체입니다. fork된 프로세스 간에 트리 형태로 연결됩니다.
// include/linux/rmap.h:32-68
struct anon_vma {
struct anon_vma *root; // 이 anon_vma 트리의 루트
struct rw_semaphore rwsem; // 수정: 쓰기잠금, 순회: 읽기잠금
atomic_t refcount; // 참조 카운트
unsigned long num_children; // 자식 anon_vma 수 (자기 포함)
unsigned long num_active_vmas; // 이 anon_vma를 가리키는 VMA 수
struct anon_vma *parent; // 부모 anon_vma
struct rb_root_cached rb_root; // 관련 VMA의 interval tree
};
VMA와 anon_vma를 연결하는 체인 구조체입니다. 하나의 VMA는 여러 anon_vma에 연결될 수 있고, 하나의 anon_vma는 여러 VMA를 가질 수 있습니다.
// include/linux/rmap.h:83-92
struct anon_vma_chain {
struct vm_area_struct *vma; // 연결된 VMA
struct anon_vma *anon_vma; // 연결된 anon_vma
struct list_head same_vma; // 같은 VMA의 다른 AVC 목록
struct rb_node rb; // anon_vma interval tree 노드
unsigned long rb_subtree_last; // interval tree 서브트리 마지막 값
};
rmap walk를 제어하는 컨트롤 구조체입니다. 각 walk 유형에 맞는 콜백을 설정합니다.
// include/linux/rmap.h:951-965
struct rmap_walk_control {
void *arg; // 콜백에 전달되는 인자
bool try_lock; // 락 획득 실패 시 즉시 반환
bool contended; // 락 경쟁으로 중단됨 플래그
bool (*rmap_one)(struct folio *folio, struct vm_area_struct *vma,
unsigned long addr, void *arg); // 매 VMA에서 호출될 콜백
int (*done)(struct folio *folio); // 순회 종료 조건 검사
struct anon_vma *(*anon_lock)(const struct folio *folio,
struct rmap_walk_control *rwc); // anon 락 획득
bool (*invalid_vma)(struct vm_area_struct *vma, void *arg); // 건너뛸 VMA 판별
};
특정 페이지가 어떤 VMA에 매핑되어 있는지 순회하는 구조체입니다.
// include/linux/rmap.h:864-874
struct page_vma_mapped_walk {
unsigned long pfn; // 찾는 페이지의 PFN
unsigned long nr_pages; // folio의 페이지 수
pgoff_t pgoff; // folio의 페이지 오프셋
struct vm_area_struct *vma; // 대상 VMA
unsigned long address; // 현재 가상 주소
pmd_t *pmd; // 매핑된 PMD 엔트리
pte_t *pte; // 매핑된 PTE 엔트리
spinlock_t *ptl; // 페이지 테이블 잠금
unsigned int flags; // PVMW_SYNC, PVMW_MIGRATION 등
};
// include/linux/rmap.h:94-105
enum ttu_flags {
TTU_USE_SHARED_ZEROPAGE = 0x2, // large folio 미사용 페이지용
TTU_SPLIT_HUGE_PMD = 0x4, // THP PMD를 PTE로 분할
TTU_IGNORE_MLOCK = 0x8, // mlock 무시
TTU_SYNC = 0x10, // racy 검사 회피 (동기화)
TTU_HWPOISON = 0x20, // hwpoison 엔트리로 변환
TTU_BATCH_FLUSH = 0x40, // TLB flush 배치 처리
TTU_RMAP_LOCKED = 0x80, // rmap 락 미획득 (호출자가 보유)
};
folio의 모든 매핑 VMA를 순회합니다. folio 유형에 따라 anon, file, KSM 순회로 분기합니다.
// mm/rmap.c:3093-3101
void rmap_walk(struct folio *folio, struct rmap_walk_control *rwc)
{
if (unlikely(folio_test_ksm(folio)))
rmap_walk_ksm(folio, rwc); // KSM folio 처리
else if (folio_test_anon(folio))
rmap_walk_anon(folio, rwc, false); // 익명 페이지 처리
else
rmap_walk_file(folio, rwc, false); // 파일 기반 페이지 처리
}
분기 로직:
rmap_walk_ksm(): KSM stable/unstable tree 순회rmap_walk_anon(): anon_vma->rb_root interval tree 순회rmap_walk_file(): mapping->i_mmap interval tree 순회anon_vma 트리에서 모든 관련 VMA를 찾아 rmap_one 콜백을 실행합니다.
// mm/rmap.c:2956-3000
static void rmap_walk_anon(struct folio *folio,
struct rmap_walk_control *rwc, bool locked)
{
struct anon_vma *anon_vma;
pgoff_t pgoff_start, pgoff_end;
struct anon_vma_chain *avc;
/* folio lock이 있어야 mapping이 바뀌지 않습니다. */
VM_WARN_ON_FOLIO(!folio_test_locked(folio), folio);
if (locked) {
anon_vma = folio_anon_vma(folio); // 이미 락된 경우
/* anon_vma가 사라졌다면 안 됩니다. */
VM_BUG_ON_FOLIO(!anon_vma, folio);
} else {
anon_vma = rmap_walk_anon_lock(folio, rwc); // 락 획득
}
if (!anon_vma) return;
pgoff_start = folio_pgoff(folio);
pgoff_end = pgoff_start + folio_nr_pages(folio) - 1;
/* interval tree에서 겹치는 모든 VMA를 순회합니다. */
anon_vma_interval_tree_foreach(avc, &anon_vma->rb_root,
pgoff_start, pgoff_end) {
struct vm_area_struct *vma = avc->vma;
unsigned long address = vma_address(vma, pgoff_start,
folio_nr_pages(folio));
VM_BUG_ON_VMA(address == -EFAULT, vma);
cond_resched();
if (rwc->invalid_vma && rwc->invalid_vma(vma, rwc->arg))
continue; // 건너뛸 VMA
if (!rwc->rmap_one(folio, vma, address, rwc->arg))
break; // 콜백이 false 반환 시 순회 중단
if (rwc->done && rwc->done(folio))
break; // 종료 조건 충족
}
if (!locked)
anon_vma_unlock_read(anon_vma);
}
folio의 모든 페이지 테이블 매핑을 제거합니다. 페이지 회수(swap out), OOM kill 시 사용됩니다.
// mm/rmap.c:2386-2399
void try_to_unmap(struct folio *folio, enum ttu_flags flags)
{
struct rmap_walk_control rwc = {
.rmap_one = try_to_unmap_one, // 매 VMA에서 PTE 제거
.arg = (void *)flags,
.done = folio_not_mapped, // 모든 매핑 제거 시 종료
.anon_lock = folio_lock_anon_vma_read,
};
if (flags & TTU_RMAP_LOCKED)
rmap_walk_locked(folio, &rwc); // 이미 rmap 락 보유
else
rmap_walk(folio, &rwc); // 락 없이 순회
}
try_to_unmap_one 핵심 분기:
VM_LOCKED VMA → mlock 복원 후 중단TTU_SPLIT_HUGE_PMD 플래그 시 PTE로 분할page_swap_entry())모든 PTE를 migration entry로 교체합니다. 실제 메모리 복사 전에 사용됩니다.
// mm/rmap.c:2731-2767
void try_to_migrate(struct folio *folio, enum ttu_flags flags)
{
struct rmap_walk_control rwc = {
.rmap_one = try_to_migrate_one, // 매 VMA에서 migration entry 설치
.arg = (void *)flags,
.done = folio_not_mapped,
.anon_lock = folio_lock_anon_vma_read,
};
// 유효하지 않은 플래그 검증
if (WARN_ON_ONCE(flags & ~(TTU_RMAP_LOCKED | TTU_SPLIT_HUGE_PMD |
TTU_SYNC | TTU_BATCH_FLUSH)))
return;
// 디바이스 전용 folio 처리 제한
if (folio_is_zone_device(folio) &&
(!folio_is_device_private(folio) && !folio_is_device_coherent(folio)))
return;
// exec() 중 임시 VMA 건너뛰기
if (!folio_test_ksm(folio) && folio_test_anon(folio))
rwc.invalid_vma = invalid_migration_vma;
if (flags & TTU_RMAP_LOCKED)
rmap_walk_locked(folio, &rwc);
else
rmap_walk(folio, &rwc);
}
folio가 참조(referenced)되었는지 모든 매핑에서 검사합니다. 페이지 회수 시 유휴(idle) 판단에 사용됩니다.
// mm/rmap.c:1059-1094
int folio_referenced(struct folio *folio, int is_locked,
struct mem_cgroup *memcg, vm_flags_t *vm_flags)
{
bool we_locked = false;
struct folio_referenced_arg pra = {
.mapcount = folio_mapcount(folio),
.memcg = memcg,
};
struct rmap_walk_control rwc = {
.rmap_one = folio_referenced_one, // 매 VMA에서 referenced 검사
.arg = (void *)&pra,
.anon_lock = folio_lock_anon_vma_read,
.try_lock = true, // 락 경쟁 시 회피
.invalid_vma = invalid_folio_referenced_vma,
};
*vm_flags = 0;
if (!pra.mapcount)
return 0;
if (!folio_raw_mapping(folio))
return 0;
/* folio_trylock으로 안전하게 잠금을 획득합니다. */
if (!is_locked) {
we_locked = folio_trylock(folio);
if (!we_locked) return 1;
}
rmap_walk(folio, &rwc);
*vm_flags = pra.vm_flags;
if (we_locked)
folio_unlock(folio);
return rwc.contended ? -1 : pra.referenced;
}
try_to_unmap(folio, flags)
└─ rmap_walk(folio, &rwc)
├─ [anon] rmap_walk_anon(folio, rwc, false)
│ ├─ rmap_walk_anon_lock(folio, rwc) ← anon_vma 락 획득
│ ├─ anon_vma_interval_tree_foreach(avc, &anon_vma->rb_root)
│ │ ├─ rwc->invalid_vma(vma, arg) ← 건너뛸 VMA 확인
│ │ ├─ rwc->rmap_one(folio, vma, address, arg)
│ │ │ └─ try_to_unmap_one(folio, vma, address, arg)
│ │ │ ├─ page_vma_mapped_walk(&pvmw) ← PTE 순회
│ │ │ │ ├─ [THP] split_huge_pmd_locked() → 재순회
│ │ │ │ ├─ [anon] folio_try_share_anon_rmap_pte()
│ │ │ │ ├─ get_and_clear_ptes() ← PTE 제거
│ │ │ │ ├─ set_pte_at(swp_pte) ← swap entry 설치
│ │ │ │ └─ folio_remove_rmap_ptes() ← mapcount 감소
│ │ │ └─ mmu_notifier_invalidate_range_end()
│ │ └─ rwc->done(folio) ← folio_not_mapped() 확인
│ └─ anon_vma_unlock_read(anon_vma)
├─ [file] rmap_walk_file(folio, rwc, false)
│ ├─ i_mmap_lock_read(mapping)
│ ├─ vma_interval_tree_foreach(vma, &mapping->i_mmap)
│ │ └─ rwc->rmap_one(folio, vma, address, arg)
│ └─ i_mmap_unlock_read(mapping)
└─ [KSM] rmap_walk_ksm(folio, rwc)
| 항목 | rmap_walk_anon | rmap_walk_file | rmap_walk_ksm |
|---|---|---|---|
| **대상** | 익명 페이지 (malloc 등) | 파일 매핑 (mmap 등) | KSM 병합 페이지 |
| **데이터 구조** | anon_vma->rb_root (interval tree) | address_space->i_mmap (interval tree) | stable_node tree |
| **락** | anon_vma->root->rwsem (읽기) | mapping->i_mmap_rwsem (읽기) | KSM lock |
| **순회 대상** | anon_vma_chain → VMA | VMA interval tree → VMA | stable_node → rmap_item → VMA |
| **호출 경로** | 회수, OOM, KSM 스캔 | 회수, truncate, fsync | KSM 병합 |
| 항목 | try_to_unmap | try_to_migrate |
|---|---|---|
| **목적** | 페이지 매핑 완전 제거 | PTE를 migration entry로 교체 |
| **PTE 후속 처리** | hwpoison/swap entry 또는 제거 | writable/readable/exclusive migration entry |
| **VM_LOCKED 처리** | mlock 복원 후 중단 | 무시 (mlock 불필요) |
| **TTU_HWPOISON** | 지원 | 미지원 |
| **TTU_IGNORE_MLOCK** | 지원 | 해당 없음 |
| **호출 시점** | 페이지 회수(swap out), OOM | 페이지 마이그레이션 시작 전 |
| 함수 | 시나리오 | 레벨 | mapcount 처리 |
|---|---|---|---|
| `folio_add_anon_rmap_ptes()` | 기존 folio에 PTE 매핑 추가 | PTE | atomic_inc_and_test |
| `folio_add_anon_rmap_pmd()` | 기존 folio에 PMD 매핑 추가 | PMD | atomic_inc (entire_mapcount) |
| `folio_add_new_anon_rmap()` | 새 folio에 첫 매핑 | PTE/PMD | atomic_set(0) (새로 시작) |
| `folio_add_file_rmap_ptes()` | 파일 매핑 PTE 추가 | PTE | atomic_inc_and_test |
| `folio_add_file_rmap_pmd()` | 파일 매핑 PMD 추가 | PMD | atomic_inc (entire_mapcount) |
| 플래그 | try_to_unmap | try_to_migrate | 비고 |
|---|---|---|---|
| TTU_SPLIT_HUGE_PMD | THP PMD를 PTE로 분할 후 재순회 | migration entry 설치 전 분할 | |
| TTU_SYNC | PTE 제거 후 동기화 대기 | 마이그레이션 대기 | |
| TTU_HWPOISON | hwpoison swap entry 설치 | 미지원 | |
| TTU_BATCH_FLUSH | TLB flush 배치 처리 | 미지원 | |
| TTU_RMAP_LOCKED | rmap 락 미획득 | rmap 락 미획득 | |
| TTU_IGNORE_MLOCK | VM_LOCKED 무시 | 해당 없음 |
rmap 관련 잠금 순서 (mm/rmap.c 주석에서 발췌):
inode->i_rwsem (쓰기/트렁케이트 시)
mm->mmap_lock
mapping->invalidate_lock
folio_lock
hugetlbfs_i_mmap_rwsem_key
vma_start_write
mapping->i_mmap_rwsem
anon_vma->rwsem
mm->page_table_lock or pte_lock
swap_lock
mmlist_lock