# Page Owner — 페이지 할당 추적
관련 소스:mm/page_owner.c,include/linux/page_owner.h,include/linux/page_ext.h,tools/mm/page_owner_sort.c
Page Owner는 커널의 페이지 할당자를 추적하여 메모리 누수나 메모리 과다 사용 원인을 진단하는 디버그 서브시스템입니다. 각 페이지가 할당될 때 호출 스택, GFP 플래그, 프로세스 ID 등의 정보를 page_ext 확장 메모리에 기록하고, 해제 시 해제 스택도 함께 저장합니다. 이를 통해 cat /sys/kernel/debug/page_owner로 현재 할당된 모든 페이지의 할당 주체를 확인할 수 있습니다.
Page Owner는 기본적으로 비활성화되어 있으며, 부팅 커맨드라인에 page_owner=on을 추가하여 활성화합니다. 비활성화 시에는 hotpath에 2개의 unlikely 분기만 추가되어 성능 영향이 미미합니다. 활성화 시에도 page_ext 메모리와 stack_depot 스택 저장소만 추가로 사용합니다.
소스 파일:
mm/page_owner.c ← 핵심 구현 (할당 추적, debugfs 인터페이스)
include/linux/page_owner.h ← 외부 API (set_page_owner, reset_page_owner 등)
include/linux/page_ext.h ← page_ext 확장 메커니즘
tools/mm/page_owner_sort.c ← 사용자空间 정렬 도구
# 1. page_owner 활성화 상태 확인 (부팅 시)
cat /proc/cmdline | grep page_owner
# 2. 페이지 할당 정보 출력
cat /sys/kernel/debug/page_owner
# 3. 스택 기반 정렬 출력 (할당 많은 순)
cat /sys/kernel/debug/page_owner_stacks/show_stacks
# 4. 핸들 기반 출력 (빠른 요약)
cat /sys/kernel/debug/page_owner_stacks/show_handles
# 5. 카운트 임계값 설정 후 필터링
echo 7000 > /sys/kernel/debug/page_owner_stacks/count_threshold
cat /sys/kernel/debug/page_owner_stacks/show_stacks
# 6. 사용자空间 정렬 도구 실행
cd tools/mm && make page_owner_sort
cat /sys/kernel/debug/page_owner > page_owner_full.txt
./page_owner_sort page_owner_full.txt sorted_page_owner.txt
# 7. 메모리 기준 정렬 (-m)
./page_owner_sort -m page_owner_full.txt sorted_by_mem.txt
# 8. 특정 프로세스 필터링 (--pid)
./page_owner_sort --pid=1234 page_owner_full.txt filtered.txt
# 9. 특정 프로세스 그룹 필터링 (--tgid)
./page_owner_sort --tgid=100,200 page_owner_full.txt filtered_tgids.txt
# 10. 특정 명령어 필터링 (--name)
./page_owner_sort --name sshd,nginx page_owner_full.txt filtered_names.txt
# 11. 특정 필드만 출력 (--cull)
./page_owner_sort --cull=stacktrace,pid page_owner_full.txt culled.txt
# 12. 커스텀 정렬 (--sort)
./page_owner_sort --sort=-n,+pid page_owner_full.txt sorted_custom.txt
page_owner 구조체는 page_ext에 포함되어 각 페이지마다 하나의 인스턴스가 존재합니다.
/* mm/page_owner.c:24-37 */
struct page_owner {
unsigned short order; /* 할당된 페이지 order (2^order개) */
short last_migrate_reason; /* 마지막 마이그레이션 사유 (-1 = 없음) */
gfp_t gfp_mask; /* 할당 시 사용된 GFP 플래그 */
depot_stack_handle_t handle; /* 할당 시 스택 핸들 (stack_depot) */
depot_stack_handle_t free_handle; /* 해제 시 스택 핸들 */
u64 ts_nsec; /* 할당 시점 (나노초) */
u64 free_ts_nsec; /* 해제 시점 (나노초) */
char comm[TASK_COMM_LEN]; /* 할당한 프로세스 명령어 (16바이트) */
pid_t pid; /* 할당한 프로세스 PID */
pid_t tgid; /* 할당한 프로세스 TGID */
pid_t free_pid; /* 해제한 프로세스 PID */
pid_t free_tgid; /* 해제한 프로세스 TGID */
};
page_owner 데이터는 page_ext 구조체에 포함되어 저장됩니다. page_ext는 sparse memory 시스템에서 각 페이지에 대해 할당되는 확장 메모리 블록입니다.
/* include/linux/page_ext.h:52-54 */
struct page_ext {
unsigned long flags; /* PAGE_EXT_OWNER, PAGE_EXT_OWNER_ALLOCATED 등 비트 플래그 */
};
/* include/linux/page_ext.h:36-43 */
enum page_ext_flags {
PAGE_EXT_OWNER, /* 이 페이지의 page_owner 정보가 유효한지 */
PAGE_EXT_OWNER_ALLOCATED, /* 현재 할당 중인지 (0 = 해제됨) */
};
/* include/linux/page_ext.h:25-31 */
struct page_ext_operations {
size_t offset; /* page_ext 내 클라이언트 데이터 오프셋 */
size_t size; /* 클라이언트 데이터 크기 */
bool (*need)(void); /* page_ext가 필요한지 여부 */
void (*init)(void); /* page_ext 할당 후 초기화 콜백 */
bool need_shared_flags; /* 공유 플래그 필드 사용 여부 */
};
할당된 스택 레코드들을 순회하기 위한 연결 리스트 노드입니다.
/* mm/page_owner.c:39-42 */
struct stack {
struct stack_record *stack_record; /* stack_depot 스택 레코드 포인터 */
struct stack *next; /* 다음 스택 노드 */
};
show_stacks, show_handles 등 debugfs 파일 읽기 시 출력 플래그를 관리합니다.
/* mm/page_owner.c:52-55 */
struct stack_print_ctx {
struct stack *stack; /* 현재 순회 중인 스택 노드 */
u8 flags; /* STACK_PRINT_FLAG_STACK | PAGES | HANDLE 조합 */
};
page_owner_ops는 page_ext에 페이지 추적 데이터를 붙이는 등록 정보입니다. 부팅 인자로 기능을 켜면 초기화 루틴에서 더미/실패/초기 스택을 준비하고, 이후 page_owner_inited static key를 활성화합니다.
/* mm/page_owner.c:93-148 */
static __init bool need_page_owner(void)
{
return page_owner_enabled;
}
static __init void init_page_owner(void)
{
if (!page_owner_enabled)
return;
register_dummy_stack();
register_failure_stack();
register_early_stack();
init_early_allocated_pages();
dummy_stack.stack_record = __stack_depot_get_stack_record(dummy_handle);
failure_stack.stack_record = __stack_depot_get_stack_record(failure_handle);
if (dummy_stack.stack_record)
refcount_set(&dummy_stack.stack_record->count, 1);
if (failure_stack.stack_record)
refcount_set(&failure_stack.stack_record->count, 1);
dummy_stack.next = &failure_stack;
stack_list = &dummy_stack;
static_branch_enable(&page_owner_inited);
}
struct page_ext_operations page_owner_ops = {
.size = sizeof(struct page_owner),
.need = need_page_owner,
.init = init_page_owner,
.need_shared_flags = true,
};
early_param("page_owner", early_page_owner_param)가 page_owner_enabled를 세우고, page_owner=on이면 stack_depot_request_early_init()까지 먼저 호출됩니다.
/* mm/page_owner.c:335-346 */
noinline void __set_page_owner(struct page *page, unsigned short order,
gfp_t gfp_mask)
{
u64 ts_nsec = local_clock();
depot_stack_handle_t handle;
handle = save_stack(gfp_mask); /* 현재 호출 스택 저장 */
/* page_owner 필드에 할당 정보 기록 */
__update_page_owner_handle(page, handle, order, gfp_mask, -1,
ts_nsec, current->pid, current->tgid,
current->comm);
inc_stack_record_count(handle, gfp_mask, 1 << order); /* 스택별 페이지 수 증가 */
}
역할: 페이지 할당 시 호출 스택과 프로세스 정보를 page_ext에 기록합니다.
/* mm/page_owner.c:298-333 */
void __reset_page_owner(struct page *page, unsigned short order)
{
struct page_ext *page_ext;
depot_stack_handle_t handle;
depot_stack_handle_t alloc_handle;
struct page_owner *page_owner;
u64 free_ts_nsec = local_clock(); /* 해제 시점 기록 */
page_ext = page_ext_get(page);
if (unlikely(!page_ext))
return;
page_owner = get_page_owner(page_ext);
alloc_handle = page_owner->handle;
page_ext_put(page_ext);
/* 해제 시 스택 저장 (GFP_NOWAIT로 재귀 방지) */
handle = save_stack(__GFP_NOWARN);
__update_page_owner_free_handle(page, handle, order, current->pid,
current->tgid, free_ts_nsec);
/* 초기 할당 페이지(early_handle)가 아닌 경우 refcount 감소 */
if (alloc_handle != early_handle)
dec_stack_record_count(alloc_handle, 1 << order);
}
역할: 페이지 해제 시 해제 스택과 타임스탬프를 기록하고, 스택별 할당 페이지 수를 감소시킵니다.
/* mm/page_owner.c:155-172 */
static noinline depot_stack_handle_t save_stack(gfp_t flags)
{
unsigned long entries[PAGE_OWNER_STACK_DEPTH];
depot_stack_handle_t handle;
unsigned int nr_entries;
/* 재귀 방지: page_owner 내부 할당 시 스택 저장 스킵 */
if (current->in_page_owner)
return dummy_handle;
set_current_in_page_owner(); /* 재귀 플래그 설정 */
nr_entries = stack_trace_save(entries, ARRAY_SIZE(entries), 2); /* 2단계 프레임 스킵 */
handle = stack_depot_save(entries, nr_entries, flags);
if (!handle)
handle = failure_handle; /* 저장 실패 시 더미 핸들 사용 */
unset_current_in_page_owner();
return handle;
}
역할: 현재 CPU의 호출 스택을 stack_depot에 저장하고 핸들을 반환합니다. page_owner 코드 내부에서 메모리가 할당되는 재귀 상황을 방지합니다.
/* mm/page_owner.c:659-755 (핵심 로직) */
static ssize_t read_page_owner(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
/* pfn 위치부터 시작하여 할당된 페이지 탐색 */
for (; pfn < max_pfn; pfn++) {
/* Buddy 페이지는 스킵 */
if (PageBuddy(page)) {
unsigned long freepage_order = buddy_order_unsafe(page);
if (freepage_order <= MAX_PAGE_ORDER)
pfn += (1UL << freepage_order) - 1;
continue;
}
/* page_owner 플래그 확인 */
if (!test_bit(PAGE_EXT_OWNER, &page_ext->flags))
goto ext_put_continue;
if (!test_bit(PAGE_EXT_OWNER_ALLOCATED, &page_ext->flags))
goto ext_put_continue;
/* 고유 order의 첫 페이지만 출력 (중복 방지) */
if (!IS_ALIGNED(pfn, 1 << page_owner->order))
goto ext_put_continue;
/* print_page_owner()로 사용자空间에 복사 */
return print_page_owner(buf, count, pfn, page, &page_owner_tmp, handle);
}
return 0;
}
역할: /sys/kernel/debug/page_owner 파일 read 시 호출됩니다. 유효한 할당 페이지를 찾아 정보를 출력합니다.
/* mm/page_owner.c:668-749 */
if (!static_branch_unlikely(&page_owner_inited))
return -EINVAL;
page = NULL;
if (*ppos == 0)
pfn = min_low_pfn;
else
pfn = *ppos;
/* 유효한 PFN 또는 MAX_ORDER_NR_PAGES 구간 시작으로 이동 */
while (!pfn_valid(pfn) && (pfn & (MAX_ORDER_NR_PAGES - 1)) != 0)
pfn++;
for (; pfn < max_pfn; pfn++) {
/* 새 MAX_ORDER_NR_PAGES 구간이 비어 있으면 건너뜀 */
if ((pfn & (MAX_ORDER_NR_PAGES - 1)) == 0 && !pfn_valid(pfn)) {
pfn += MAX_ORDER_NR_PAGES - 1;
continue;
}
page = pfn_to_page(pfn);
if (PageBuddy(page)) {
unsigned long freepage_order = buddy_order_unsafe(page);
if (freepage_order <= MAX_PAGE_ORDER)
pfn += (1UL << freepage_order) - 1;
continue;
}
page_ext = page_ext_get(page);
if (unlikely(!page_ext))
continue;
if (!test_bit(PAGE_EXT_OWNER, &page_ext->flags))
goto ext_put_continue;
if (!test_bit(PAGE_EXT_OWNER_ALLOCATED, &page_ext->flags))
goto ext_put_continue;
page_owner = get_page_owner(page_ext);
if (!IS_ALIGNED(pfn, 1 << page_owner->order))
goto ext_put_continue;
handle = READ_ONCE(page_owner->handle);
if (!handle)
goto ext_put_continue;
*ppos = pfn + 1;
page_owner_tmp = *page_owner;
page_ext_put(page_ext);
return print_page_owner(buf, count, pfn, page,
&page_owner_tmp, handle);
ext_put_continue:
page_ext_put(page_ext);
}
return 0;
/* mm/page_owner.c:547-603 */
static ssize_t print_page_owner(char __user *buf, size_t count,
unsigned long pfn, struct page *page,
struct page_owner *page_owner, depot_stack_handle_t handle)
{
/* 할당 정보: order, GFP, PID, TGID, comm, 타임스탬프 */
ret = scnprintf(kbuf, count,
"Page allocated via order %u, mask %#x(%pGg), pid %d, tgid %d (%s), ts %llu ns\n",
page_owner->order, page_owner->gfp_mask,
&page_owner->gfp_mask, page_owner->pid,
page_owner->tgid, page_owner->comm,
page_owner->ts_nsec);
/* 마이그레이션 정보 */
if (page_owner->last_migrate_reason != -1) {
ret += scnprintf(kbuf + ret, count - ret,
"Page has been migrated, last migrate reason: %s\n",
migrate_reason_names[page_owner->last_migrate_reason]);
}
/* memcg 충전 정보 */
ret = print_page_owner_memcg(kbuf, count, ret, page);
}
역할: 페이지 할당 정보를 human-readable 형태로 포맷팅하여 사용자空间 버퍼에 복사합니다.
/* mm/page_owner.c:555-603 */
count = min_t(size_t, count, PAGE_SIZE);
kbuf = kmalloc(count, GFP_KERNEL);
if (!kbuf)
return -ENOMEM;
ret = scnprintf(kbuf, count,
"Page allocated via order %u, mask %#x(%pGg), pid %d, tgid %d (%s), ts %llu ns\n",
page_owner->order, page_owner->gfp_mask,
&page_owner->gfp_mask, page_owner->pid,
page_owner->tgid, page_owner->comm,
page_owner->ts_nsec);
pageblock_mt = get_pageblock_migratetype(page);
page_mt = gfp_migratetype(page_owner->gfp_mask);
ret += scnprintf(kbuf + ret, count - ret,
"PFN 0x%lx type %s Block %lu type %s Flags %pGp\n",
pfn,
migratetype_names[page_mt],
pfn >> pageblock_order,
migratetype_names[pageblock_mt],
&page->flags);
ret += stack_depot_snprint(handle, kbuf + ret, count - ret, 0);
if (ret >= count)
goto err;
if (page_owner->last_migrate_reason != -1) {
ret += scnprintf(kbuf + ret, count - ret,
"Page has been migrated, last migrate reason: %s\n",
migrate_reason_names[page_owner->last_migrate_reason]);
}
ret = print_page_owner_memcg(kbuf, count, ret, page);
ret += snprintf(kbuf + ret, count - ret, "\n");
if (ret >= count)
goto err;
if (copy_to_user(buf, kbuf, ret))
ret = -EFAULT;
kfree(kbuf);
return ret;
err:
kfree(kbuf);
return -ENOMEM;
할당 경로:
__alloc_pages()
→ set_page_owner() ← inline wrapper (page_owner.h)
→ __set_page_owner() ← 실제 구현 (page_owner.c:335)
→ save_stack() ← 호출 스택 저장 (page_owner.c:155)
→ stack_trace_save() ← 커널 스택 트레이스 수집
→ stack_depot_save() ← stack_depot에 저장
→ __update_page_owner_handle() ← page_ext에 필드 기록 (page_owner.c:244)
→ inc_stack_record_count() ← 스택별 페이지 수 증가 (page_owner.c:206)
해제 경로:
__free_pages()
→ reset_page_owner() ← inline wrapper
→ __reset_page_owner() ← 실제 구현 (page_owner.c:298)
→ save_stack() ← 해제 스택 저장
→ __update_page_owner_free_handle() ← 해제 정보 기록 (page_owner.c:273)
→ dec_stack_record_count() ← 스택별 페이지 수 감소 (page_owner.c:231)
읽기 경로:
cat /sys/kernel/debug/page_owner
→ read_page_owner() ← debugfs read 핸들러 (page_owner.c:659)
→ pfn 순회 (Buddy/Reserved 스킵)
→ print_page_owner() ← 정보 포맷팅 (page_owner.c:547)
→ stack_depot_snprint() ← 스택 트레이스 출력
→ print_page_owner_memcg() ← memcg 정보 출력 (page_owner.c:511)
→ copy_to_user() ← 사용자空间으로 복사
스택 순회 경로:
cat /sys/kernel/debug/page_owner_stacks/show_stacks
→ page_owner_stack_open() ← seq_file 오픈 (page_owner.c:936)
→ stack_start() ← stack_list 헤드부터 순회 시작 (page_owner.c:855)
→ stack_print() ← 각 스택의 할당 페이지 수 출력 (page_owner.c:892)
→ stack_next() ← 다음 스택으로 이동 (page_owner.c:878)
| 비교 항목 | 비활성화 시 | 활성화 시 |
|---|---|---|
| **hotpath 분기** | 없음 | 2개 unlikely (jump label 패치로 NOP) |
| **page_ext 사용** | 없음 | 페이지당 `sizeof(struct page_owner)` 추가 |
| **stack_depot 사용** | 없음 | 고유 스택당 스택 레코드 저장 |
| **성능 영향** | 없음 | 미미 (jump label 비활성화 시 NOP) |
| **메모리 오버헤드** | 없음 | page_ext + stack_depot 할당 |
| 출력 인터페이스 | 내용 | 사용 시나리오 |
|---|---|---|
| `/sys/kernel/debug/page_owner` | 전체 페이지별 상세 정보 | 특정 페이지 할당 원인 추적 |
| `show_stacks` | 스택별 할당 페이지 수 + 스택 트레이스 | 메모리 많이 쓰는 코드 식별 |
| `show_handles` | 스택 핸들 번호 + 페이지 수만 | 빠른 요약, 모니터링 |
| `show_stacks_handles` | 스택 트레이스 + 핸들 번호 | 스택-핸들 매칭 |
| `count_threshold` | 임계값 설정 (show_stacks 필터) | 대량 할당만 필터링 |
| 옵션 | 정렬 기준 | 기본 정렬 방향 | 사용 예 |
|---|---|---|---|
| `-t` | 할당 횟수 (default) | 내림차순 | 가장 많은 할당 패턴 |
| `-m` | 총 메모리 페이지 수 | 내림차순 | 가장 많은 메모리 사용 |
| `-a` | 할당 타임스탬프 | 오름차순 | 오래된 할당부터 |
| `-r` | 해제 타임스탬프 | 오름차순 | 최근 해제부터 |
| `-p` | PID | 오름차순 | 프로세스별 그룹핑 |
| `-P` | TGID | 오름차순 | 프로세스 그룹별 |
| `-n` | 명령어 이름 | 오름차순 | 명령어별 그룹핑 |
| `-s` | 스택 트레이스 | 오름차순 | 동일 스택 그룹핑 |
| 상태 | 의미 | 처리 방식 |
|---|---|---|
| `handle == 0` | 스택 저장 실패 | 출력 시 "stack trace missing" 표시 |
| `handle == dummy_handle` | 초기화 전 또는 재귀 할당 | page_owner 내부 할당 시 반환 |
| `handle == failure_handle` | stack_depot_save 실패 | fallback 핸들 |
| `handle == early_handle` | 초기 할당 페이지 | refcount 증감 생략 |
| 정상 핸들 | 유효한 스택 레코드 | stack_depot_snprint로 출력 |