전체 흐름
메모리 관리에서는 주소 변환, 물리 페이지의 소유권, 회수 정책, 할당 지연 시간이 서로 영향을 준다. 이 층에서는 '어느 page가 지금 누구 소유인가'와 '그 소유권을 어떤 lock/refcount가 증명하는가'를 끝까지 추적해야 한다.
함수의 입력과 처리 결과를 먼저 확인한다. 구조체를 생성하고 연결하는 시점, 다른 코드에서 사용할 수 있게 하는 시점, 더 이상 사용하지 않아 회수하는 시점을 구분한다.
정상 처리와 오류 처리를 함께 확인한다. 오류 복구, 장치의 실행 중 연결·분리(hotplug), 자원 해제 과정에서도 상태가 올바르게 정리되는지 점검한다.
왼쪽부터 처리 단계를 나열했다. 각 코드 조각에서 바뀌는 상태를 해당 단계와 대조한다.
소스 코드 위치
먼저 확인할 Linux 6.18.37 LTS 소스 파일
Linux 6.18.37 LTS: mm/page_alloc.c
Linux 6.18.37 LTS: include/linux/mmzone.h
Linux 6.18.37 LTS: include/linux/page-flags.h
설명: 진입 함수가 있는 파일과 구조체 정의, 아키텍처별 연결 코드, 콜백 구현을 함께 확인한다. 여러 파일을 대조해야 호출 관계와 각 코드의 역할을 알 수 있다.
대표 코드: 공개 호출과 실제 구현
__alloc_pages() 매크로와 구현
struct page *__alloc_pages_noprof(gfp_t gfp, unsigned int order, int preferred_nid,
nodemask_t *nodemask);
#define __alloc_pages(...) alloc_hooks(__alloc_pages_noprof(__VA_ARGS__))
원본 코드: mm/page_alloc.c, 5268–5278행
struct page *__alloc_pages_noprof(gfp_t gfp, unsigned int order,
int preferred_nid, nodemask_t *nodemask)
{
struct page *page;
page = __alloc_frozen_pages_noprof(gfp, order, preferred_nid, nodemask);
if (page)
set_page_refcounted(page);
return page;
}
EXPORT_SYMBOL(__alloc_pages_noprof);
확인 사항: 공개 이름 __alloc_pages는 alloc_hooks()로 구현 함수 __alloc_pages_noprof()를 감싸는 매크로입니다. 구현은 __alloc_frozen_pages_noprof()의 결과가 있으면 set_page_refcounted()를 호출합니다. 그 아래 경로는 get_page_from_freelist()의 첫 시도와 slowpath:5228–5264를 따릅니다. 반환된 page는 caller가 소유하며, free list의 블록 존재가 함수의 사후 조건은 아닙니다.
__free_pages() 이후의 PCP / buddy 갈림길
void __free_pages(struct page *page, unsigned int order)
{
___free_pages(page, order, FPI_NONE);
}
원본 코드: mm/page_alloc.c, 2934–2946행
static void __free_frozen_pages(struct page *page, unsigned int order,
fpi_t fpi_flags)
{
unsigned long __maybe_unused UP_flags;
struct per_cpu_pages *pcp;
struct zone *zone;
unsigned long pfn = page_to_pfn(page);
int migratetype;
if (!pcp_allowed_order(order)) {
__free_pages_ok(page, order, fpi_flags);
return;
}
원본 코드: mm/page_alloc.c, 2973–2982행
pcp_trylock_prepare(UP_flags);
pcp = pcp_spin_trylock(zone->per_cpu_pageset);
if (pcp) {
free_frozen_page_commit(zone, pcp, page, migratetype, order, fpi_flags);
pcp_spin_unlock(pcp);
} else {
free_one_page(zone, page, pfn, order, fpi_flags);
}
pcp_trylock_finish(UP_flags);
}
확인 사항: __free_pages()는 ___free_pages()를 호출합니다. 5311–5333행에서 put_page_testzero(page)가 참이면 내부 free helper로 가고, 거짓이어도 non-compound high-order의 !head 경로는 tail page들을 따로 반환할 수 있습니다. pcp_allowed_order()가 거짓이면 바로 __free_pages_ok()로 갑니다. 일반적인 허용 order 경로에서는 PCP lock 획득 시 per-CPU list, 실패 시 free_one_page()로 가지만, MIGRATE_ISOLATE와 PREEMPT_RT/NMI 예외:2958–2971는 lock 이전에 별도 분기합니다. free_frozen_pages()는 2984–2987행의 별도 진입이며 __free_pages()가 직접 호출하는 이름은 아닙니다.
위 발췌는 고정 v6.18.37 원문과 행 단위로 대조했습니다. mm/page_alloc.c SHA-256: 4274e384a11c5a74fecc48adec30fd324bc2427bc3d1033c6ffd6ceef9a18fd2; include/linux/gfp.h SHA-256: 3a42404c7d5daabe13d27e21972b7d649c0d6a5db4a7be469e6db436ea4e25e2.
함수별 분석
physical page를 order 단위로 쪼개고 합치며, fragmentation과 allocation latency 사이의 균형을 잡는 핵심 allocator다.
원본 코드를 함수별로 나누어 살펴본다. 각 함수가 준비하는 값과 다음 함수가 그 값을 사용하는 방식을 확인한다.
화살표는 직접 함수 호출 관계가 아니라 한 page의 가능한 수명 흐름이다. 할당과 해제 사이에는 caller의 사용·참조 해제가 있고, 해제 후 PCP와 buddy 경로가 갈라진다.
1. __alloc_pages_noprof() (공개 매크로의 구현)
__alloc_pages_noprof()는 하위의 후보 탐색·slowpath 결과가 성공했을 때 caller에게 넘길 page의 refcount를 설정합니다. zone.free_area는 후보를 찾는 한 자료구조이며, 성공 시 선택된 free block은 소비됩니다.
반환값이 NULL인지, 첫 get_page_from_freelist() 시도에서 얻었는지 slowpath로 갔는지, 성공 후 set_page_refcounted()가 호출됐는지를 분리해 확인합니다. free block의 남은 개수는 이 반환값만으로 판단할 수 없습니다.
자주 틀리는 해석: order와 pageblock migratetype을 혼동함
2. __free_pages()와 __free_frozen_pages()
__free_pages()는 참조 상태와 non-compound high-order tail 예외를 처리한 뒤 내부 free helper를 거칠 수 있습니다. 허용된 order에서는 per-CPU pageset에 먼저 머물 수 있어 즉시 zone.free_area에 나타나지 않습니다. page->private의 buddy order는 free block head가 buddy list에 등록되는 경로에서 읽어야 합니다.
pcp_allowed_order()와 PCP lock 결과를 먼저 확인한 뒤, PCP 저장인지 free_one_page()를 통한 buddy list 반환인지 분리합니다. 두 경로를 하나의 즉시 병합으로 읽으면 nr_free와 실제 해제 시점을 혼동합니다.
자주 틀리는 해석: pcplist 때문에 global free_area만 보고 판단함
| 함수 | 입력 | 상태 변경 | 확인할 사항 |
|---|---|---|---|
| __alloc_pages_noprof | gfp, order, preferred_nid, nodemask | 성공한 page의 refcount 설정 후 반환 | get_page_from_freelist 첫 시도와 slowpath 중 어느 쪽이 반환했는가 |
| __free_pages → __free_frozen_pages | page, order, 참조 상태·tail 예외, PCP 허용 여부 | PCP 저장 또는 buddy list 반환 | PCP lock을 얻었는가, 직접 free_one_page()로 갔는가 |
구조체와 필드
구조체의 필드를 언제 바꿀 수 있고 다른 코드가 언제 읽을 수 있는지 확인한다. 필드의 의미와 함께 잠금 등 접근을 보호하는 규칙을 살펴본다.
이 네 필드는 확인할 목록이지 실행 순서가 아닙니다. PCP와 zone.free_area는 서로 다른 보관 위치이며, buddy order는 buddy list에 등록된 free block head에서 읽습니다.
어떤 코드가 필드를 변경하고 어떤 방식으로 접근을 보호하는지, 객체를 언제 해제하는지 원본 코드에서 확인한다.
| 필드 | 읽는 법 |
|---|---|
| zone.free_area | order별 free list |
| page->private | buddy order 저장 |
| pageblock migratetype | movable/unmovable grouping |
| pcplist | per-cpu page cache |
실행 단계와 상태 변화
실행 단계를 따로 정리하면 정상 경로와 실패 경로를 나란히 비교할 수 있다. 커널 문제는 최종 결과보다 준비가 덜 된 중간 상태를 다른 코드에 공개한 뒤, 실패 시 제대로 정리하지 못할 때 자주 발생한다.
화살표는 할당 후 나중에 해제되는 수명을 줄인 것입니다. 해제는 PCP에 머물거나 buddy로 돌아가며, buddy 경로라도 조건에 따라 병합이 멈춥니다.
| 상태 | 의미 | 진입 조건 | 깨지는 지점 |
|---|---|---|---|
| free block present | order별 후보 존재 | 앞 단계의 준비와 오류 처리가 끝난 뒤 | 다음 단계가 준비되지 않은 상태를 사용하거나 필요한 정리를 늦출 때 |
| split | 큰 block을 작은 order로 분해 | 앞 단계의 준비와 오류 처리가 끝난 뒤 | 다음 단계가 준비되지 않은 상태를 사용하거나 필요한 정리를 늦출 때 |
| allocated | page ownership이 caller로 이동 | 앞 단계의 준비와 오류 처리가 끝난 뒤 | 다음 단계가 준비되지 않은 상태를 사용하거나 필요한 정리를 늦출 때 |
| freed / merged | 해제 후 PCP 보관 또는 buddy 병합 | 참조 상태 확인 또는 non-compound high-order tail 처리 후 | PCP 저장을 즉시 free_area 병합으로 읽거나 buddy 검사 실패를 놓칠 때 |
free_area의 계수 변화
앞의 기존 그림 1은 order별 free_area 배열을, 기존 그림 4는 PFN 0–7 블록의 분할 트리와 XOR buddy 찾기를 보여줍니다. 여기서는 그 다음 질문인 리스트 슬롯·nr_free의 전후 변화와 병합 중단 조건을 확인합니다. PFN 256–263 한 블록만 있는 zone에서 order 0 하나를 할당하는 가상 예시이며, 이 zone에는 다른 free block이 없고 한 migratetype(MIGRATE_MOVABLE)의 list만 채워진 것으로 가정합니다.
그림을 가로로 이동해 읽거나 free list 계수·중단 조건 도해 크게 열기 ↗
nr_free badge는 해당 order의 블록 수입니다. 빈 칸, 색상, PFN은 설명용이며 실제 메모리 배치나 실행 trace가 아닙니다.| 검사 결과 | 코드 분기 | free list와 nr_free 결과 |
|---|---|---|
| buddy가 일반 할당 page | PageBuddy도 guard도 아니면 조회 실패로 find_buddy_page_pfn() == NULL | buddy list를 제거하지 않고 현재 order의 블록을 등록; 그 order의 nr_free + 1 |
| buddy의 order가 다름 | 같은 order buddy가 아니므로 조회 실패; if (!buddy)에서 병합 중단 | 다른 order의 기존 블록을 건드리지 않고 현재 order 블록만 등록 |
| pageblock 경계의 타입 충돌 | order >= pageblock_order, migratetype이 다르고 한쪽이 병합 불가능하면 goto done_merging | buddy는 자신의 migratetype list에 남고, 현재 블록은 현재 타입의 list에 등록 |
| 모든 검사 통과 | buddy를 해당 order의 list에서 제거한 뒤 order++로 재검사 | 제거한 order의 nr_free − 1; 마지막에 더 큰 order list에 블록 하나 등록 |
free_area[order].nr_free는 해당 order의 모든 migratetype list를 합친 free block 수입니다. 위 도해에서만 list 하나로 제한했으므로 슬롯의 블록 수와 badge가 같습니다. 할당 전은 8 page짜리 블록 하나, 할당 후는 4 + 2 + 1 = 7 page짜리 블록 세 개, 전부 병합한 후는 다시 8 page짜리 블록 하나입니다. nr_free 값들을 그대로 더해 자유 page 수로 읽지 않습니다. set_buddy_order()는 free block head의 page->private와 PageBuddy를 설정하고, list 제거는 이를 지웁니다.
마지막 열은 PFN 256이 per-CPU pageset을 거치지 않고 __free_one_page()에 도달하고 세 buddy가 모두 병합 가능한 경우만 나타냅니다. 실제 order 0 해제는 per-CPU pageset을 먼저 거칠 수 있고, compaction capture·guard page·fallback 같은 다른 경로도 있습니다. 표의 첫 두 행은 같은 order의 free buddy가 있어야 한다는 조회 조건을 풀어 쓴 예시이며, 세 번째 행은 이 버전의 pageblock 경계 분기를 그대로 옮긴 것입니다.
소스 근거: 고정 Linux v6.18.37 mm/page_alloc.c (SHA-256 4274e384a11c5a74fecc48adec30fd324bc2427bc3d1033c6ffd6ceef9a18fd2). free block 표시:752–756, list·nr_free 증감:832–909, 병합 중단·재등록:978–1059, 분할·회계:1740–1777, 후보 조회:1927–1949. buddy 검사는 mm/internal.h:639–709의 page_is_buddy()/find_buddy_page_pfn()도 대조합니다. 이 파일의 SHA-256은 16659eab691d9c9a05f7350bdf5b51e5945812a6daea63a3efac9d743b45ad69입니다. DUJINLABS 자체 제작 도해.
불변 조건과 실패 사례
buddy allocator의 핵심 객체가 외부에 공개된 뒤에는 마지막 참조가 사라질 때까지 해제 경로가 callback, timer, IRQ, worker와 경합하지 않아야 한다.
다른 CPU나 하위 계층이 변경된 필드를 읽는다면 잠금과 메모리 배리어가 접근 순서를 어떻게 보장하는지 확인한다. 참조 횟수와 RCU로 관리하는 객체 수명도 별도로 확인한다.
중간 단계에서 실패하면 이미 확보한 자원을 의존 관계에 맞춰 해제해야 한다. goto 레이블이 많다면 각 분기에 도달했을 때 확보한 자원을 표로 정리한다.
임베디드 보드를 처음 구동할 때는 패닉 대신 로그 중단, 시간 초과, 장치 초기화 지연, 과도한 인터럽트 같은 증상으로 문제가 드러날 수 있다.
order와 pageblock migratetype을 혼동함
pcplist 때문에 global free_area만 보고 판단함
high-order 실패를 전체 메모리 부족으로 단정함
계측과 검증
로그를 무작정 추가하지 말고 확인할 가설을 먼저 세운다. 상태표에서 처리가 멈춘 단계를 찾은 뒤, 상태를 바꾸는 함수와 그 결과를 사용하는 함수를 함께 살펴본다.
로그가 출력되었다고 처리가 끝난 것으로 판단하지 않는다. 원본 코드에서 로그 앞뒤의 필드 변경을 확인한다.
| 도구 | 보는 것 |
|---|---|
| trace_mm_page_alloc | buddy allocator 실행이 어느 단계에서 멈추는지 확인 |
| cat /proc/buddyinfo | buddy allocator 실행이 어느 단계에서 멈추는지 확인 |
| page_owner | buddy allocator 실행이 어느 단계에서 멈추는지 확인 |
| perf probe __alloc_pages | buddy allocator 실행이 어느 단계에서 멈추는지 확인 |
# 예시: 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
추가 확인 사항
- buddy allocator 의 state machine을 네 단계로 줄였을 때, 실제 코드에서 빠지는 intermediate state는 무엇인가?
- buddy allocator 의 fast path가 생략한 검사는 어느 init path 또는 slow path에서 보증되는가?
- 실험으로 확인한다면 'order와 pageblock migratetype을 혼동함' 문제를 어떤 tracepoint와 counter로 분리할 수 있는가?
- 실험으로 확인한다면 'pcplist 때문에 global free_area만 보고 판단함' 문제를 어떤 tracepoint와 counter로 분리할 수 있는가?