Ryotta's Linux 7.0 MM

메모리 관리 서브시스템 완전 분석

# Page Owner — 페이지 할당 추적

관련 소스: mm/page_owner.c, include/linux/page_owner.h, include/linux/page_ext.h, tools/mm/page_owner_sort.c

개요 (Overview)

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 Owner 호출 흐름

핵심 자료구조

struct page_owner — 페이지 할당 추적 정보

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 */
};

struct page_ext — 페이지 확장 메타데이터

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 등 비트 플래그 */
};

page_ext 플래그 — 할당 상태 추적

/* include/linux/page_ext.h:36-43 */
enum page_ext_flags {
    PAGE_EXT_OWNER,              /* 이 페이지의 page_owner 정보가 유효한지 */
    PAGE_EXT_OWNER_ALLOCATED,    /* 현재 할당 중인지 (0 = 해제됨) */
};

struct page_ext_operations — page_ext 클라이언트 연산

/* 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;      /* 공유 플래그 필드 사용 여부 */
};

struct stack — 스택 레코드 연결 리스트

할당된 스택 레코드들을 순회하기 위한 연결 리스트 노드입니다.

/* mm/page_owner.c:39-42 */
struct stack {
    struct stack_record *stack_record;  /* stack_depot 스택 레코드 포인터 */
    struct stack *next;                 /* 다음 스택 노드 */
};

stack_print_ctx — debugfs 출력 컨텍스트

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 초기화 레지스터

page_owner_opspage_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()까지 먼저 호출됩니다.


핵심 함수

__set_page_owner() — 페이지 할당 시 owner 정보 기록

/* 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에 기록합니다.

__reset_page_owner() — 페이지 해제 시 owner 정보 업데이트

/* 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);
}

역할: 페이지 해제 시 해제 스택과 타임스탬프를 기록하고, 스택별 할당 페이지 수를 감소시킵니다.

save_stack() — 호출 스택을 stack_depot에 저장

/* 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 코드 내부에서 메모리가 할당되는 재귀 상황을 방지합니다.

read_page_owner() — debugfs에서 페이지 할당 정보 읽기

/* 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;

print_page_owner() — 페이지 할당 정보 포맷팅

/* 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)

조건별 비교

페이지 Owner 비교 표

비교 항목비활성화 시활성화 시
**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 필터)대량 할당만 필터링

page_owner_sort 정렬 옵션 비교

옵션정렬 기준기본 정렬 방향사용 예
`-t`할당 횟수 (default)내림차순가장 많은 할당 패턴
`-m`총 메모리 페이지 수내림차순가장 많은 메모리 사용
`-a`할당 타임스탬프오름차순오래된 할당부터
`-r`해제 타임스탬프오름차순최근 해제부터
`-p`PID오름차순프로세스별 그룹핑
`-P`TGID오름차순프로세스 그룹별
`-n`명령어 이름오름차순명령어별 그룹핑
`-s`스택 트레이스오름차순동일 스택 그룹핑

stack_depot 핸들 상태 비교

상태의미처리 방식
`handle == 0`스택 저장 실패출력 시 "stack trace missing" 표시
`handle == dummy_handle`초기화 전 또는 재귀 할당page_owner 내부 할당 시 반환
`handle == failure_handle`stack_depot_save 실패fallback 핸들
`handle == early_handle`초기 할당 페이지refcount 증감 생략
정상 핸들유효한 스택 레코드stack_depot_snprint로 출력

관련 문서

  • 메모리 관리 개요
  • Buddy Allocator
  • SLUB 할당자
  • 디버그 도구
  • Migration
  • Memory Cgroup