Memory · Linux v6.6 · source + diagram note

compaction

high-order allocation 실패와 page migration.

전체 흐름

높은 order의 물리 페이지 할당이 조각화로 막힐 때 compaction은 이동 가능한 folio를 free page로 옮겨 큰 연속 free 공간을 만들려고 시도한다. 이동 횟수와 실제 할당 성공은 다르다.

그림 1. 직접 compaction의 경계 (v6.6)
  1. 고차 할당 요구요청 order 크기의 연속 free block 필요
  2. 적합성 판단zone watermark와 이동 목적지 여유 검사
  3. 양쪽 PFN 검사낮은 쪽은 이동 후보, 높은 쪽은 free 목적지
  4. 종료 검사적합한 block·capture·중단·스캔 완료 구분

직접 요청의 설명 순서다. 다른 compaction 호출자는 별도 경로를 갖는다. mm/compaction.c:2618; mm/compaction.c:2381

작업하는 일보장하지 않는 것
compaction이동 가능한 folio를 옮겨 연속 free 공간을 모으려 함사용 중인 메모리를 즉시 회수
reclaimpage를 되찾아 전체 free page 수를 늘림고차 연속 free block 형성
COMPACT_SUCCESS이번 compaction의 성공 조건 충족후속 할당의 성공

근거: mm/compaction.c:2381; mm/compaction.c:2169. 일반 order-0 할당과 직접 고차 compaction을 구분한다.

소스 코드 위치

먼저 확인할 Linux v6.6 소스 파일

원본 코드:
Linux v6.6: mm/compaction.c
Linux v6.6: mm/migrate.c
Linux v6.6: include/linux/compaction.h

설명: 진입 함수가 있는 파일과 구조체 정의, 아키텍처별 연결 코드, 콜백 구현을 함께 확인한다. 여러 파일을 대조해야 호출 관계와 각 코드의 역할을 알 수 있다.

대표 코드

isolate_migratepages · migrate scanner

원본 코드: mm/compaction.c, isolate_migratepages() 1975–2014행 발췌
		(cc->mode != MIGRATE_SYNC ? ISOLATE_ASYNC_MIGRATE : 0);
	bool fast_find_block;

	/*
	 * Start at where we last stopped, or beginning of the zone as
	 * initialized by compact_zone(). The first failure will use
	 * the lowest PFN as the starting point for linear scanning.
	 */
	low_pfn = fast_find_migrateblock(cc);
	block_start_pfn = pageblock_start_pfn(low_pfn);
	if (block_start_pfn < cc->zone->zone_start_pfn)
		block_start_pfn = cc->zone->zone_start_pfn;

	/*
	 * fast_find_migrateblock() has already ensured the pageblock is not
	 * set with a skipped flag, so to avoid the isolation_suitable check
	 * below again, check whether the fast search was successful.
	 */
	fast_find_block = low_pfn != cc->migrate_pfn && !cc->fast_search_fail;

	/* Only scan within a pageblock boundary */
	block_end_pfn = pageblock_end_pfn(low_pfn);

	/*
	 * Iterate over whole pageblocks until we find the first suitable.
	 * Do not cross the free scanner.
	 */
	for (; block_end_pfn <= cc->free_pfn;
			fast_find_block = false,
			cc->migrate_pfn = low_pfn = block_end_pfn,
			block_start_pfn = block_end_pfn,
			block_end_pfn += pageblock_nr_pages) {

		/*
		 * This can potentially iterate a massively long zone with
		 * many pageblocks unsuitable, so periodically check if we
		 * need to schedule.
		 */
		if (!(low_pfn % (COMPACT_CLUSTER_MAX * pageblock_nr_pages)))
			cond_resched();

확인 사항: 이 코드에서는 반환값보다 상태 변경을 먼저 확인한다. 어느 잠금을 획득한 뒤 어떤 필드를 바꾸는지, 실패 시 어느 레이블로 분기하는지, 변경된 상태를 다음 호출자가 어떤 전제로 사용하는지 추적한다.

compact_zone_order

원본 코드: mm/compaction.c, 2613–2651행 발췌; compact_zone() 꼬리 뒤 2618행부터 compact_zone_order()
	VM_BUG_ON(!list_empty(&cc->migratepages));

	return ret;
}

static enum compact_result compact_zone_order(struct zone *zone, int order,
		gfp_t gfp_mask, enum compact_priority prio,
		unsigned int alloc_flags, int highest_zoneidx,
		struct page **capture)
{
	enum compact_result ret;
	struct compact_control cc = {
		.order = order,
		.search_order = order,
		.gfp_mask = gfp_mask,
		.zone = zone,
		.mode = (prio == COMPACT_PRIO_ASYNC) ?
					MIGRATE_ASYNC :	MIGRATE_SYNC_LIGHT,
		.alloc_flags = alloc_flags,
		.highest_zoneidx = highest_zoneidx,
		.direct_compaction = true,
		.whole_zone = (prio == MIN_COMPACT_PRIORITY),
		.ignore_skip_hint = (prio == MIN_COMPACT_PRIORITY),
		.ignore_block_suitable = (prio == MIN_COMPACT_PRIORITY)
	};
	struct capture_control capc = {
		.cc = &cc,
		.page = NULL,
	};

	/*
	 * Make sure the structs are really initialized before we expose the
	 * capture control, in case we are interrupted and the interrupt handler
	 * frees a page.
	 */
	barrier();
	WRITE_ONCE(current->capture_control, &capc);

	ret = compact_zone(&cc, &capc);

확인 사항: 이 코드에서는 반환값보다 상태 변경을 먼저 확인한다. 어느 잠금을 획득한 뒤 어떤 필드를 바꾸는지, 실패 시 어느 레이블로 분기하는지, 변경된 상태를 다음 호출자가 어떤 전제로 사용하는지 추적한다.

함수별 분석

compact_zone_order()가 compact_zone()을 호출한다. 후자는 격리 후보를 찾고 성공한 경우에만 migration을 호출한다.

그림 2. 요청에서 folio 이동까지의 함수 방향
  1. try_to_compact_pageszone별 직접 요청
  2. compact_zone_ordercompact_control·capture 설정
  3. compact_zonescanner와 반복 종료 관리
  4. isolate → migrateSUCCESS면 migrate_pages 호출

둘째 함수가 셋째 함수를 호출한다. compact_zone_order()가 folio를 직접 격리하지 않는다. mm/compaction.c:2618; mm/compaction.c:2381

함수책임분기·결과
compact_zone_orderorder·mode·zone을 compact_control에 설정compact_zone 호출 뒤 capture 여부 검사
compact_zone두 scanner 시작 위치와 루프 제어compact_finished가 CONTINUE일 동안 반복
isolate_migratepages낮은 PFN에서 이동 후보 격리NONE / SUCCESS / ABORT
migrate_pagescompaction_alloc로 목적지 folio를 찾아 이동 시도실패 후보 putback 또는 재검사

v6.6 고정 소스: mm/compaction.c:2618, mm/compaction.c:2381, mm/compaction.c:1967.

구조체와 필드

compact_control은 zone·order·mode와 두 스캐너의 PFN, 격리된 후보와 목적지 목록을 함께 담는다.

그림 3. 반대 방향의 두 scanner (v6.6)
낮은 PFN → migrate_pfnisolate_migratepages가 이동 후보를 찾아 migratepages에 격리.
높은 PFN ← free_pfncompaction_alloc가 freepages를 사용하고 필요하면 free page를 추가 격리.

cached PFN·free-list 빠른 탐색·skip hint가 있으므로 매번 인접 pageblock을 한 칸씩 움직이지 않는다. mm/compaction.c:2381; mm/compaction.c:1763

필드뜻읽을 때 주의
order / mode요청 크기와 migration 방식ASYNC와 SYNC_LIGHT 등 결과가 다를 수 있음
migrate_pfn낮은 쪽 이동 후보 탐색 위치cached 시작점·skip hint 반영
free_pfn높은 쪽 free 대상 탐색 위치free-list 빠른 탐색도 가능
migratepages / freepages격리된 후보와 목적지 목록격리 자체는 migration 완료가 아님

실행 단계와 상태 변화

격리, 실제 이동, 적합한 고차 free block의 발견은 서로 다른 사건이다. 격리 실패와 중단도 같은 결과가 아니다.

그림 4. 격리 결과별 다음 동작
ISOLATE_NONE이번 반복에서는 migration 없이 다음 검사.
ISOLATE_SUCCESSmigrate_pages와 compaction_alloc로 이동 시도.
ISOLATE_ABORT후보 putback 뒤 COMPACT_CONTENDED로 중단.
compact_finished적합한 block·scanner 만남·경합 등을 재검사.

v6.6 compact_zone()의 분기: mm/compaction.c:2490; 결과 검사: mm/compaction.c:2169.

결과조건해석
COMPACT_SUCCESS직접 요청에서 적합한 free block 또는 captured page후속 allocation 성공 보장은 아님
COMPACT_COMPLETEscanner 만남, whole_zone 검사스캔 완료
COMPACT_PARTIAL_SKIPPEDscanner 만남 후 일부 범위 생략 또는 proactive 조기 종료부분 검사 종료
COMPACT_CONTENDED경합·fatal signal·격리 중단이번 시도 중단
COMPACT_NO_SUITABLE_PAGE → COMPACT_CONTINUE아직 적합한 free block이 없음계속 검사

불변 조건과 실패 사례

lifetime

compaction의 핵심 객체가 외부에 공개된 뒤에는 마지막 참조가 사라질 때까지 해제 경로가 callback, timer, IRQ, worker와 경합하지 않아야 한다.

ordering

다른 CPU나 하위 계층이 변경된 필드를 읽는다면 잠금과 메모리 배리어가 접근 순서를 어떻게 보장하는지 확인한다. 참조 횟수와 RCU로 관리하는 객체 수명도 별도로 확인한다.

오류 복구

중간 단계에서 실패하면 이미 확보한 자원을 의존 관계에 맞춰 해제해야 한다. goto 레이블이 많다면 각 분기에 도달했을 때 확보한 자원을 표로 정리한다.

bring-up symptom

임베디드 보드를 처음 구동할 때는 패닉 대신 로그 중단, 시간 초과, 장치 초기화 지연, 과도한 인터럽트 같은 증상으로 문제가 드러날 수 있다.

주의

compaction을 reclaim과 같은 free 작업으로 봄

주의

unmovable page 하나가 pageblock을 막는 효과를 과소평가함

주의

CMA/THP 요구를 일반 order-0 allocation과 섞음

계측과 검증

로그를 무작정 추가하지 말고 확인할 가설을 먼저 세운다. 상태표에서 처리가 멈춘 단계를 찾은 뒤, 상태를 바꾸는 함수와 그 결과를 사용하는 함수를 함께 살펴본다.

관찰 지점확인할 사실한계
trace_mm_compaction_begin / migratepages / finished스캔 시작, 이동 시도·성공 수, 결과 상태이동 수가 allocation 성공을 뜻하지 않음
/proc/pagetypeinfoorder·migratetype별 free block 분포단일 시점의 분포로 원인 확정 불가
vm.compact_memory명시적 compaction 실험직접 고차 요청의 mode·목표와 다름

고정 소스: mm/compaction.c:2381; mm/compaction.c:2169. 계측 기능과 파일 가용성은 커널 설정에 따른다.

compaction.o는 빌트인 항목입니다. available_filter_functions에서 실제 추적 가능한 이름을 확인하고, 함수 이름 패턴으로 범위를 좁힙니다. 아래 명령은 추적 설정을 바꾸므로 별도 실험 환경에서만 실행합니다.

기본 추적 경로:
# 예시: tracefs가 켜진 실험 환경에서 compaction 함수명으로 좁힌다.
mount -t tracefs nodev /sys/kernel/tracing
echo function_graph > /sys/kernel/tracing/current_tracer
echo 'compact_zone*' > /sys/kernel/tracing/set_ftrace_filter
cat /sys/kernel/tracing/trace_pipe

추가 확인 사항

  • compaction 의 state machine을 네 단계로 줄였을 때, 실제 코드에서 빠지는 intermediate state는 무엇인가?
  • compaction 의 fast path가 생략한 검사는 어느 init path 또는 slow path에서 보증되는가?
  • 실험으로 확인한다면 'compaction을 reclaim과 같은 free 작업으로 봄' 문제를 어떤 tracepoint와 counter로 분리할 수 있는가?
  • 실험으로 확인한다면 'unmovable page 하나가 pageblock을 막는 효과를 과소평가함' 문제를 어떤 tracepoint와 counter로 분리할 수 있는가?