읽는 지도
메모리 코드는 address translation, physical page ownership, reclaim policy, allocation latency가 한꺼번에 얽힌다. 이 층을 박사급으로 읽는다는 것은 '어느 page가 지금 누구 소유인가'와 '그 소유권을 어떤 lock/refcount가 증명하는가'를 끝까지 추적한다는 뜻이다.
함수 이름보다 입력 객체와 출력 객체를 먼저 본다. 이 토픽에서 어떤 구조체가 생성, 연결, publish, retire되는지 표시한다.
정상 경로와 실패 경로를 같은 무게로 본다. kernel code의 품질은 성공 path보다 error unwind, hotplug, teardown에서 더 잘 드러난다.
왼쪽은 입력 또는 상위 계층이고, 오른쪽으로 갈수록 실제 상태 변경이 커진다. 문C식으로 읽을 때는 이 그림을 먼저 머리에 놓고 코드 조각을 끼워 넣는다.
원본 코드 좌표
Linux 6.18.37 LTS에서 열 파일
Linux 6.18.37 LTS: mm/compaction.c
Linux 6.18.37 LTS: mm/migrate.c
Linux 6.18.37 LTS: include/linux/compaction.h
설명: 첫 파일은 보통 진입 함수가 있는 곳이고, 나머지는 구조체 정의, architecture glue, callback 구현을 확인할 때 같이 연다. 파일을 여러 개 놓고 봐야 이 토픽의 boundary가 보인다.
대표 코드
compact_zone
(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();
읽는 법: 이 절편에서 먼저 볼 것은 return 값이 아니라 상태 변경이다. 어떤 lock을 잡은 뒤 어떤 필드를 바꾸는지, 실패하면 어느 label로 빠지는지, 그리고 바뀐 상태를 다음 호출자가 어떤 전제로 소비하는지 표시한다.
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);
읽는 법: 이 절편에서 먼저 볼 것은 return 값이 아니라 상태 변경이다. 어떤 lock을 잡은 뒤 어떤 필드를 바꾸는지, 실패하면 어느 label로 빠지는지, 그리고 바뀐 상태를 다음 호출자가 어떤 전제로 소비하는지 표시한다.
함수별 독해
high-order allocation을 위해 movable page를 밀어내고 free page를 모아 contiguous block을 만드는 경로다.
이 섹션은 원본 코드 발췌를 함수 이름 단위로 끊어, 각 함수가 어떤 전제 조건을 만들고 다음 함수가 무엇을 소비하는지 추적한다.
각 노드는 독립 함수가 아니라 전제 조건을 생산하고 소비하는 연결점이다. 코드를 읽을 때는 노드 사이에서 어떤 필드가 바뀌는지 표시한다.
1. compact_zone
compact_zone 주변에서는 compact_control를 중심으로 본다. 이 필드는 scan 위치와 mode 역할을 하므로, 함수가 끝날 때 high-order request 상태가 실제로 성립했는지 확인해야 한다.
원본 코드에서 볼 순서는 입력 범위 검증, 중심 필드 갱신, 다른 계층에 보이는 publish 지점, 실패 시 되돌림 순서다. 이 네 칸이 맞아야 다음 함수가 contiguous block 필요을 전제로 삼을 수 있다.
자주 틀리는 해석: compaction을 reclaim과 같은 free 작업으로 봄
2. compact_zone_order
compact_zone_order 주변에서는 migrate scanner를 중심으로 본다. 이 필드는 옮길 page 탐색 역할을 하므로, 함수가 끝날 때 migration candidates isolated 상태가 실제로 성립했는지 확인해야 한다.
원본 코드에서 볼 순서는 입력 범위 검증, 중심 필드 갱신, 다른 계층에 보이는 publish 지점, 실패 시 되돌림 순서다. 이 네 칸이 맞아야 다음 함수가 이동할 folio 격리을 전제로 삼을 수 있다.
자주 틀리는 해석: unmovable page 하나가 pageblock을 막는 효과를 과소평가함
| 함수 | 입력 | 상태 변경 | 검증 질문 |
|---|---|---|---|
| compact_zone | compact_control, caller context, subsystem 전제 조건 | high-order request: contiguous block 필요 | compaction을 reclaim과 같은 free 작업으로 봄 문제를 코드상 어느 조건문 또는 error label에서 분리하는가 |
| compact_zone_order | migrate scanner, caller context, subsystem 전제 조건 | migration candidates isolated: 이동할 folio 격리 | unmovable page 하나가 pageblock을 막는 효과를 과소평가함 문제를 코드상 어느 조건문 또는 error label에서 분리하는가 |
구조체 / 필드 해설
여기서는 “어떤 구조체가 있다”가 아니라 그 필드가 어느 단계에서 쓰기 가능하고 어느 단계부터 관찰 가능한지를 본다. 박사급 리뷰에서는 필드의 뜻보다 보호 규칙이 먼저다.
필드는 구조체 안에 흩어져 있지만, 실제 실행에서는 위 순서로 의미가 이어진다.
| 필드 | 읽는 법 | 확인 |
|---|---|---|
| compact_control | scan 위치와 mode | 누가 쓰고, 누가 보호하고, 언제 lifetime이 끝나는지 원본 코드에서 확인 |
| migrate scanner | 옮길 page 탐색 | 누가 쓰고, 누가 보호하고, 언제 lifetime이 끝나는지 원본 코드에서 확인 |
| free scanner | 받아줄 free page 탐색 | 누가 쓰고, 누가 보호하고, 언제 lifetime이 끝나는지 원본 코드에서 확인 |
| pageblock migratetype | 이동 가능성 단위 | 누가 쓰고, 누가 보호하고, 언제 lifetime이 끝나는지 원본 코드에서 확인 |
상태 전이 그림
상태 전이를 따로 그리는 이유는 정상 path와 실패 path를 같은 표에서 보기 위해서다. 커널 regression은 흔히 마지막 상태가 아니라 중간 상태를 외부에 publish한 뒤 rollback하지 못해서 생긴다.
상태 전이는 단방향처럼 그렸지만, 실패 경로에서는 대부분 역순 rollback이 붙는다.
| 상태 | 의미 | 진입 조건 | 깨지는 지점 |
|---|---|---|---|
| high-order request | contiguous block 필요 | 앞 단계 함수가 전제 조건을 만들고 error path가 정리된 뒤 | 다음 단계가 이 상태를 너무 일찍 소비하거나 늦게 정리할 때 |
| migration candidates isolated | 이동할 folio 격리 | 앞 단계 함수가 전제 조건을 만들고 error path가 정리된 뒤 | 다음 단계가 이 상태를 너무 일찍 소비하거나 늦게 정리할 때 |
| free target found | 새 위치 확보 | 앞 단계 함수가 전제 조건을 만들고 error path가 정리된 뒤 | 다음 단계가 이 상태를 너무 일찍 소비하거나 늦게 정리할 때 |
| block compacted | 큰 order 가능성 증가 | 앞 단계 함수가 전제 조건을 만들고 error path가 정리된 뒤 | 다음 단계가 이 상태를 너무 일찍 소비하거나 늦게 정리할 때 |
불변조건 / 실패 케이스
compaction 의 중심 객체가 publish된 뒤에는 마지막 참조가 사라지기 전까지 teardown path가 모든 callback, timer, IRQ, worker와 경합하지 않아야 한다.
상태 필드를 바꾼 뒤 다른 CPU나 하위 계층이 관찰할 수 있다면 lock, barrier, refcount, RCU 중 어느 장치가 visibility를 보장하는지 확인한다.
중간 단계 실패는 성공 단계의 역순으로 되돌아가야 한다. goto label이 많은 코드는 label 이름보다 어느 resource가 이미 획득됐는지를 표로 적는다.
embedded bring-up에서는 panic보다 silence, timeout, deferred probe, interrupt flood처럼 간접 증상으로 드러나는 경우가 많다.
compaction을 reclaim과 같은 free 작업으로 봄
unmovable page 하나가 pageblock을 막는 효과를 과소평가함
CMA/THP 요구를 일반 order-0 allocation과 섞음
계측 / 검증
계측은 printk 위치 경쟁이 아니라 가설 검증이다. 먼저 위 상태표에서 멈춘 state를 정하고, 그 state를 바꾸는 함수와 그 결과를 소비하는 함수를 동시에 본다.
| 도구 | 보는 것 | 해석 |
|---|---|---|
| trace_mm_compaction_* | compaction 의 상태 전이가 어느 지점에서 멈추는지 확인 | 로그가 찍힌 위치를 완료 시점으로 단정하지 말고, 바로 앞뒤 필드 변경을 원본에서 확인 |
| cat /proc/pagetypeinfo | compaction 의 상태 전이가 어느 지점에서 멈추는지 확인 | 로그가 찍힌 위치를 완료 시점으로 단정하지 말고, 바로 앞뒤 필드 변경을 원본에서 확인 |
| compact_memory sysctl | compaction 의 상태 전이가 어느 지점에서 멈추는지 확인 | 로그가 찍힌 위치를 완료 시점으로 단정하지 말고, 바로 앞뒤 필드 변경을 원본에서 확인 |
| page_owner | compaction 의 상태 전이가 어느 지점에서 멈추는지 확인 | 로그가 찍힌 위치를 완료 시점으로 단정하지 말고, 바로 앞뒤 필드 변경을 원본에서 확인 |
# 예시: tracefs가 켜진 보드에서 토픽별 event를 좁혀 본다.
mount -t tracefs nodev /sys/kernel/tracing
echo function_graph > /sys/kernel/tracing/current_tracer
echo ':mod:*' > /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로 분리할 수 있는가?