관련 소스: mm/migrate.c (2750줄), mm/migrate_device.c (1491줄)
관련 문서: 메모리 관리 개요 · Compaction · CMA · NUMA · HMM · Hotplug
페이지 Migration은 리눅스 커널이 실행 중인 프로세스의 페이지를 물리적으로 다른 위치로 이동시키는 메커니즘입니다. 이는 Memory Compaction(단편화 해소), NUMA 밸런싱(노드 간 이동), CMA(연속 메모리 확보), 메모리 핫플러그(온라인/오프라인), HMM(이기종 메모리 관리) 등 여러 서브시스템에서 핵심 빌딩 블록으로 사용됩니다. 원본 페이지의 모든 매핑(PTE)을 migration swap entry로 교체한 뒤, 새 페이지로 데이터를 복사하고 매핑을 복원하는 3단계 프로세스로 동작합니다.
Migration의 핵심 아이디어는 "페이지가 이동되는 동안 프로세스가 접근하면 대기하게 만드는 것"입니다. PTE에 migration entry를 삽입하면, 접근 시 폴트가 발생하고 migration_entry_wait()가 완료될 때까지 블록됩니다. 이를 통해 무결성을 보장하면서 물리 메모리 내에서 페이지를 투명하게 이동할 수 있습니다.
도서관으로 비유하면, 책을 다른 서가로 옮기는 동안 임시 예약표를 붙여 두고, 다시 꽂히기 전까지는 대출 요청을 잠깐 멈추게 하는 방식과 비슷합니다.
소스 파일:
mm/migrate.c — 핵심 마이그레이션 엔진 (2750줄)
mm/migrate_device.c — 디바이스 메모리 마이그레이션 (1491줄)
include/linux/migrate.h — 외부 API, struct movable_operations, migrate_vma
include/linux/migrate_mode.h — enum migrate_mode, enum migrate_reason
# 마이그레이션 통계 확인
cat /proc/vmstat | grep -E 'pgmigrate|thp_migration|numa_pages_migrated'
# NUMA 밸런싱 활성화 상태 확인
cat /proc/sys/kernel/numa_balancing
# 현재 NUMA 정책과 노드 배치 확인
numactl --show
numastat -p $$
# 메모리 핫플러그 블록 상태 확인
cat /sys/devices/system/memory/auto_online_blocks 2>/dev/null
ls /sys/devices/system/memory/ | grep memory | head -5
# Compaction과 함께 마이그레이션 동작 관찰
cat /proc/vmstat | grep -E 'compact_stall|compact_success|compact_fail'
cat /proc/buddyinfo | head -4
# 페이지 폴트 기반 NUMA 이벤트 확인
cat /proc/vmstat | grep numa_hit
# NUMA 토폴로지와 노드 메모리 분포 확인
numactl -H
cat /proc/zoneinfo | grep -E 'Node|zone|present|managed|spanned' | head -40
# 마이그레이션 관련 커널 트레이스 (root 권한)
sudo perf trace -e 'migrate:*' --duration 10 2>/dev/null || true
# CMA 영역 정보 확인
cat /proc/meminfo | grep Cma
# 메모리 압박과 회수 강도 확인
cat /proc/pressure/memory
cat /proc/vmstat | grep -E 'allocstall|pgscan|pgsteal'
# 핫플러그/이기종 메모리 관련 메시지 확인
dmesg | grep -Ei 'migrate|numa|hotplug|cxl' | tail -20
balloon, zsmalloc, hotplug 같은 이동 가능한 페이지 소비자가 쓰는 콜백 인터페이스입니다. LRU에 직접 올라가지 않는 페이지도 드라이버가 다시 받아 주고, 이동이 끝나면 원래 자료구조로 되돌릴 수 있게 합니다.
/* include/linux/migrate.h:44-49 */
struct movable_operations {
bool (*isolate_page)(struct page *, isolate_mode_t);
int (*migrate_page)(struct page *dst, struct page *src,
enum migrate_mode);
void (*putback_page)(struct page *);
};
GPU/가속기 등 디바이스 메모리와 시스템 메모리 간 페이지 이동에 사용됩니다. src[]와 dst[] 배열은 MIGRATE_PFN_* 플래그와 PFN을 인코딩하여 각 가상 주소의 페이지 상태를 표현합니다.
src[]와 dst[]는 (end - start) >> PAGE_SHIFT 개수만큼 준비되어야 하며, migrate_vma_setup() 이후에는 src[]를 건드리면 안 됩니다. migrate_vma_pages()가 끝난 뒤에는 dst[]도 더 이상 바꾸지 않아야 하고, fault_page는 migrate_to_ram() 흐름에서 디바이스 사설 페이지를 RAM으로 되돌릴 때 쓰입니다.
/* include/linux/migrate.h:158-191 */
struct migrate_vma {
struct vm_area_struct *vma;
/*
* src와 dst 배열은
* (end - start) >> PAGE_SHIFT 개수만큼 충분히 커야 한다.
*
* 호출자는 migrate_vma_setup() 이후 src 배열을 수정하면 안 되고,
* migrate_vma_pages()가 반환된 뒤에는 dst 배열도 바꾸면 안 된다.
*/
unsigned long *dst;
unsigned long *src;
unsigned long cpages;
unsigned long npages;
unsigned long start;
unsigned long end;
/*
* device private memory에서 옮길 때 page_pgmap(page)->owner에
* 들어 있는 소유자 값과 맞춰 두는 필드다. 이 경우 flags도
* MIGRATE_VMA_SELECT_DEVICE_PRIVATE로 설정해야 한다.
* 호출자가 mmu notifier 콜백을 사용할 때는, 옮기지 않을
* device private 페이지에 대한 device MMU invalidation을 피하려고
* 이 값을 항상 채워야 한다.
*/
void *pgmap_owner;
unsigned long flags;
/*
* migrate_to_ram() 콜백의 일부로 페이지를 옮길 때 vmf->page를 넣는다.
*/
struct page *fault_page;
};
배치 마이그레이션 중 발생하는 성공/실패/THP 분할 통계를 추적합니다. migrate_pages() 함수 종료 후 vmstat 카운터에 반영됩니다.
/* mm/migrate.c:1592-1601 */
struct migrate_pages_stats {
int nr_succeeded; /* base page 단위로 성공한 일반/대형 folio 수 */
int nr_failed_pages; /* base page 단위로 실패한 일반/대형 folio 수.
시도하지 않은 folio는 제외 */
int nr_thp_succeeded; /* THP 성공 수 */
int nr_thp_failed; /* THP 실패 수 */
int nr_thp_split; /* 마이그레이션 전 분할된 THP 수 */
int nr_split; /* THP를 포함한 대형 folio 분할 수 */
};
migrate_vma API에서 src[]와 dst[] 배열의 각 엔트리는 PFN과 플래그를 비트 마스킹하여 인코딩됩니다. 비트 0-5는 플래그, 비트 6 이상은 PFN입니다.
| 플래그 | 비트 | 의미 |
|---|---|---|
| `MIGRATE_PFN_VALID` | bit 0 | 유효한 PFN 엔트리 |
| `MIGRATE_PFN_MIGRATE` | bit 1 | 마이그레이션 대상으로 표시 |
| `MIGRATE_PFN_WRITE` | bit 3 | 쓰기 권한이 있는 페이지 |
| `MIGRATE_PFN_COMPOUND` | bit 4 | 복합(compound) 페이지 |
| `MIGRATE_PFN_SHIFT` | 6 | PFN 시작 비트 위치 |
마이그레이션의 메인 진입점입니다. hugetlb 페이지를 먼저 처리한 뒤, 나머지 folio를 배치(batch) 단위로 나누어 비동기 또는 동기 방식으로 마이그레이션합니다.
/* mm/migrate.c:2072-2074 */
int migrate_pages(struct list_head *from, new_folio_t get_new_folio,
free_folio_t put_new_folio, unsigned long private,
enum migrate_mode mode, int reason, unsigned int *ret_succeeded)
동작 흐름:
1. migrate_hugetlbs()로 HugeTLB folio를 먼저 처리
2. 남은 folio를 NR_MAX_BATCHED_MIGRATION(THP 사용 시 HPAGE_PMD_NR, 그 외 512) 단위로 배치 분할
3. MIGRATE_ASYNC이면 migrate_pages_batch(), 그 외이면 migrate_pages_sync()
4. 동기 모드에서는 먼저 ASYNC로 시도한 뒤 실패분을 개별 동기 처리
5. 분할된 split_folios도 ASYNC 모드로 한 번 더 시도
6. count_vm_events()로 vmstat 카운터 업데이트
소스 folio의 모든 PTE를 migration entry로 교체하는 1단계 함수입니다. 페이지 잠금, writeback 대기, anon_vma 획득, try_to_migrate() 호출이 포함됩니다.
/* mm/migrate.c:1203-1206 */
static int migrate_folio_unmap(new_folio_t get_new_folio,
free_folio_t put_new_folio, unsigned long private,
struct folio *src, struct folio **dstp, enum migrate_mode mode,
struct list_head *ret)
동작 흐름:
1. get_new_folio()로 대상 folio 할당
2. 소스 folio 잠금 (MIGRATE_ASYNC에서는 trylock, 실패 시 건너뜀)
3. writeback 중이면 MIGRATE_SYNC에서만 대기
4. 익명 페이지이면 folio_get_anon_vma()로 anon_vma 참조 획득
5. try_to_migrate()로 모든 PTE를 migration entry로 교체
6. 매핑 해제 성공 시 __migrate_folio_record()로 상태 저장 후 반환
매핑이 해제된 소스 folio를 대상 folio로 실제로 이동시키는 2단계 함수입니다. move_to_new_folio()를 통해 address_space를 교체하고, 성공 시 remove_migration_ptes()로 새 folio를 다시 매핑합니다.
/* mm/migrate.c:1353-1356 */
static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
struct folio *src, struct folio *dst,
enum migrate_mode mode, enum migrate_reason reason,
struct list_head *ret)
동작 흐름:
1. __migrate_folio_extract()로 이전 상태 복원 정보 추출
2. movable_ops 페이지이면 migrate_movable_ops_page() 호출
3. move_to_new_folio() → mapping->a_ops->migrate_folio() 콜백 호출
4. 성공 시 folio_add_lru(dst)로 대상 LRU 추가
5. 매핑된 상태였으면 remove_migration_ptes(src, dst)로 PTE 복원
6. 소스 folio 해제 및 migrate_folio_done() 호출
실제 folio 교체를 처리하는 내부 함수입니다. 매핑 유형(무매핑, inaccessible, filesystem, anonymous, fallback)에 따라 적절한 마이그레이션 콜백을 선택합니다.
/* mm/migrate.c:1090-1092 */
static int move_to_new_folio(struct folio *dst, struct folio *src,
enum migrate_mode mode)
분기 로직:
mapping == NULL → migrate_folio() (단순 매핑 없는 folio)mapping_inaccessible(mapping) → -EOPNOTSUPP (접근 불가 매핑)mapping->a_ops->migrate_folio 존재 → filesystem/swap 콜백 호출fallback_migrate_folio() (writepages 경고 + 강제 해제)NUMA 밸런싱에서 잘못 배치된 단일 folio를 지정 노드로 이동시킵니다. migrate_misplaced_folio_prepare()로 격리된 folio를 받아 migrate_pages()를 호출합니다.
/* mm/migrate.c:2722-2723 */
int migrate_misplaced_folio(struct folio *folio, int node)
동작 흐름:
1. folio를 migratepages 리스트에 추가
2. alloc_misplaced_dst_folio() 콜백으로 대상 노드에 할당
3. MIGRATE_ASYNC + MR_NUMA_MISPLACED로 마이그레이션 시도
4. 실패 시 putback_movable_pages()로 복원
5. 성공 시 NUMA_PAGE_MIGRATE vmstat 카운터 업데이트
6. 티어링 모드에서 저속→고속 노드 이동 시 PGPROMOTE_SUCCESS 기록
migrate_vma_setup()은 VMA 유효성 검사와 src[] 수집, migrate_vma_pages()는 드라이버가 복사한 뒤 페이지 상태 갱신, migrate_vma_finalize()는 CPU 페이지 테이블 복원을 맡습니다.
/* include/linux/migrate.h:193-195 */
int migrate_vma_setup(struct migrate_vma *args);
void migrate_vma_pages(struct migrate_vma *migrate);
void migrate_vma_finalize(struct migrate_vma *migrate);
동작 흐름:
1. migrate_vma_setup()이 VMA, src[], dst[], fault_page를 검사하고 페이지를 잠근 뒤 언매핑
2. 드라이버가 dst[]를 기준으로 디바이스 메모리를 할당하고 데이터를 복사
3. migrate_vma_pages()가 성공한 PFN에 MIGRATE_PFN_MIGRATE를 남김
4. migrate_vma_finalize()가 CPU 페이지 테이블을 새 페이지 또는 원래 페이지로 정리
구현은 migrate_vma_collect_pmd()가 PMD/PTE를 훑어 src[]를 채우고, migrate_device_unmap()이 장치 사설 페이지를 떼어낸 뒤 migrate_device_pages()와 migrate_device_finalize()가 메타데이터와 PTE를 마무리합니다.
migrate_pages() ← 최상위 API
├── migrate_hugetlbs() ← HugeTLB 먼저 처리
│ └── unmap_and_move_huge_page()
│ ├── get_new_folio() ← 대상 할당
│ ├── try_to_migrate() ← PTE → migration entry
│ └── move_to_new_folio() ← 실제 이동
├── migrate_pages_batch() ← 일반 folio 배치 처리
│ ├── [pass 1] unmapping 단계
│ │ ├── try_split_folio() ← THP 분할 필요 시
│ │ ├── migrate_folio_unmap() ← PTE 교체
│ │ │ ├── get_new_folio()
│ │ │ ├── try_to_migrate()
│ │ │ └── __migrate_folio_record()
│ │ └── unmap_folios/dst_folios 리스트 구축
│ └── [pass 2] moving 단계
│ └── migrate_folios_move()
│ └── migrate_folio_move()
│ ├── move_to_new_folio() ← 매핑 교체
│ ├── folio_add_lru() ← 대상 LRU 추가
│ └── remove_migration_ptes() ← PTE 복원
└── migrate_pages_sync() ← 동기 모드 시
├── migrate_pages_batch(ASYNC) ← 먼저 ASYNC로 시도
└── 개별 동기 처리 (남은 실패분)
migrate_vma_setup() ← 디바이스 마이그레이션 진입
├── migrate_vma_collect() ← 페이지 테이블 워크
│ └── walk_page_range(migrate_vma_walk_ops)
│ └── migrate_vma_collect_pmd()
│ ├── migrate_vma_collect_huge_pmd()
│ └── [PTE 순회] → src[] 배열 채우기
└── migrate_vma_unmap() ← 매핑 해제
└── migrate_device_unmap()
├── folio_isolate_lru()
└── try_to_migrate()
| 모드 | 동작 방식 | 잠금 블록 | writeback 대기 | 사용 시점 |
|---|---|---|---|---|
| `MIGRATE_ASYNC` | 절대 블록하지 않음 | trylock만 사용 | 대기 안 함 | Compaction, proactive |
| `MIGRATE_SYNC_LIGHT` | 대부분의 잠금에서 블록 가능 | lock 허용 | uptodata 아닌 경우만 | NUMA 밸런싱 |
| `MIGRATE_SYNC` | 모든 잠금에서 블록 | lock 허용 | 대기 | move_pages 시스템 콜 |
| 사유 | 상수 | 트리거 |
|---|---|---|
| Compaction | `MR_COMPACTION` | 단편화 해소를 위한 kcompactd/direct compaction |
| Memory Failure | `MR_MEMORY_FAILURE` | 하드웨어 메모리 오류 (ECC 수정 불가) |
| Memory Hotplug | `MR_MEMORY_HOTPLUG` | 메모리 블록 오프라인 |
| Syscall | `MR_SYSCALL` | `move_pages()` 시스템 콜 |
| mbind | `MR_MEMPOLICY_MBIND` | `mbind()`로 NUMA 정책 변경 시 |
| NUMA Misplaced | `MR_NUMA_MISPLACED` | AutoNUMA가 잘못 배치된 페이지 감지 |
| Contig Range | `MR_CONTIG_RANGE` | 연속 범위 할당 (CMA 등) |
| Long-term Pin | `MR_LONGTERM_PIN` | VFIO/장치 드라이버의 장기 핀ning |
| Demotion | `MR_DEMOTION` | 메모리 티어링에서 고속→저속 하향 |
| DAMON | `MR_DAMON` | DAMON 기반 접근 패턴 분석 |
| 항목 | 일반 마이그레이션 (migrate.c) | 디바이스 마이그레이션 (migrate_device.c) |
|---|---|---|
| 진입 방식 | `migrate_pages()` + `migrate_reason` | `migrate_vma_setup()` → `migrate_vma_pages()` → `migrate_vma_finalize()` |
| 대상 | LRU에 있는 일반 folio | ZONE_DEVICE folio (GPU/HBM) |
| 격리 | `folio_isolate_lru()` | `folio_get()`으로 고정 (LRU 없음) |
| 매핑 해제 | `try_to_migrate()` + rmap walk | `try_to_migrate()` + mm_walk |
| 콜백 | `mapping->a_ops->migrate_folio()` | 드라이버가 dst 할당 + 복사 |
| API | `migrate_pages()` | `migrate_vma_setup/pages/finalize()` |
| THP 처리 | 배치 내 분할 지원 | `migrate_vma_split_folio()` |
| 결과 확인 | 반환값 + ret_succeeded | `src[]` 배열의 MIGRATE_PFN_MIGRATE 플래그 |
| 반환값 | 의미 | 처리 방법 |
|---|---|---|
| 0 | 성공 | src folio 해제, dst LRU 추가 |
| `-EAGAIN` | 일시적 실패 | 리스트에 유지, 재시도 |
| `-ENOMEM` | 메모리 부족 | 남은 처리 중단, 실패 목록으로 이동 |
| `-EBUSY` | 영구 실패 | 실패 목록으로 이동, 재시도 안 함 |
| `-EOPNOTSUPP` | 미지원 매핑 | inaccessible 매핑 |