Ryotta's Linux 7.0 MM

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

54. 메모리 누수 추적 실습

Linux 7.0 기준 — kmemleak, page_owner를 사용한 커널 및 사용자 공간 메모리 누수 추적 방법과 실전 튜닝 가이드

개요

커널과 사용자 공간에서 발생하는 메모리 누수는 시스템 불안정성, 성능 저하, 서비스 중단을 초래할 수 있다. Linux 커널은 메모리 누수를 탐지하기 위한 여러 도구를 제공하며, 대표적으로 kmemleakpage_owner가 있다. kmemleak은 참조되지 않는 커널 메모리 블록을 주기적으로 스캔하여 누수를 보고하는 반면, page_owner는 모든 페이지 할당/해제 시 스택 트레ース를 기록하여 특정 페이지가 언제, 왜 할당되었는지 추적할 수 있다. 이 문서에서는 Linux 7.0 소스 기반으로 이 두 도구의 동작 원리와 실전 사용법을 분석한다.

실제 시스템에서는 kmemleak을 사용하여 커널 메모리 누수를 탐지하고, page_owner를 사용하여 특정 페이지의 할당 경로를 추적한다. KASAN은 메모리 접근 오류(버퍼 오버플로, Use-After-Free 등)를 탐지하며, 세 도구를 조합하면 메모리 관련 문제를 체계적으로 진단할 수 있다.

소스 파일 경로:

mm/kmemleak.c
mm/page_owner.c
include/linux/kmemleak.h
include/linux/page_owner.h

빠른 점검 명령

# kmemleak 활성화 상태 확인
cat /sys/kernel/debug/kmemleak

# kmemleak 스캔 트리거
echo scan > /sys/kernel/debug/kmemleak

# kmemleak 클리어 (이전 결과 제거)
echo clear > /sys/kernel/debug/kmemleak

# kmemleak 자동 스캔 간격 설정 (초 단위)
echo scan=60 > /sys/kernel/debug/kmemleak

# kmemleak 스택 스캔 켜기/끄기
echo stack=on > /sys/kernel/debug/kmemleak
echo stack=off > /sys/kernel/debug/kmemleak

# page_owner 활성화 (부트 파라미터 필요: page_owner=on)
cat /sys/kernel/debug/page_owner

# page_owner 출력 정렬 (할당 크기 기준)
cat /sys/kernel/debug/page_owner | sort -k 3 -n -r | head -20

# kmemleak 수동 스캔
echo scan > /sys/kernel/debug/kmemleak

# 메모리 누수 의심 시 slabinfo 확인
cat /proc/slabinfo | head -20

# kmemleak에서 발견된 누수 정보 파싱
cat /sys/kernel/debug/kmemleak | grep -A 5 "unreferenced"

# page_owner를 사용한 특정 PFN 추적
python3 - <<'PY'
path = "/sys/kernel/debug/page_owner"
pfn = 0x12345
with open(path, "rb") as f:
    f.seek(pfn)
    print(f.read(4096).decode("utf-8", "replace"))
PY

핵심 자료구조

struct kmemleak_object (kmemleak.c:135-161)

할당된 메모리 블록의 메타데이터를 저장하는 구조체. 모든 할당된 커널 메모리 블록에 대해 하나의 kmemleak_object가 생성된다.

struct kmemleak_object {
    raw_spinlock_t lock;           // 객체 보호용 스핀락
    unsigned int flags;            // 객체 상태 플래그 (OBJECT_ALLOCATED 등)
    struct list_head object_list;  // 전체 객체 리스트 연결
    struct list_head gray_list;    // 스캔 대상 리스트 연결
    struct rb_node rb_node;        // 레드-블랙 트리 노드 (주소 기반 검색)
    struct rcu_head rcu;           // RCU 기반 무잠금 순회
    atomic_t use_count;            // 참조 카운트 (0이면 해제 예약)
    unsigned int del_state;        // 삭제 상태
    unsigned long pointer;         // 할당된 메모리 블록 시작 주소
    size_t size;                   // 할당된 메모리 블록 크기
    unsigned long excess_ref;      // 초과 참조 포인터
    int min_count;                 // 누수 판정 최소 참조 수
    int count;                     // 현재 탐지된 참조 수
    u32 checksum;                  // 객체 무결성 검증용 체크섬
    depot_stack_handle_t trace_handle; // 할당 시 스택 트레ース 핸들
    struct hlist_head area_list;   // 스캔 영역 리스트
    unsigned long jiffies;         // 생성 타임스탬프
    pid_t pid;                     // 할당한 프로세스 ID
    char comm[TASK_COMM_LEN];      // 할당한 프로세스 이름
};

OBJECT_ALLOCATED 플래그 (kmemleak.c:164)

#define OBJECT_ALLOCATED    (1 << 0)  // 메모리 블록 할당됨
#define OBJECT_REPORTED     (1 << 1)  // 첫 보고 후 설정
#define OBJECT_NO_SCAN      (1 << 2)  // 스캔 제외
#define OBJECT_FULL_SCAN    (1 << 3)  // 전체 영역 스캔
#define OBJECT_PHYS         (1 << 4)  // 물리 주소 기반
#define OBJECT_PERCPU       (1 << 5)  // per-CPU 할당

struct kmemleak_scan_area (kmemleak.c:118-122)

스캔 영역을 정의하는 구조체. 큰 메모리 블록의 일부분만 스캔할 때 사용된다.

struct kmemleak_scan_area {
    struct hlist_node node;    // 해시 리스트 연결
    unsigned long start;       // 스캔 시작 주소
    size_t size;               // 스캔 크기
};

struct page_owner (page_owner.c:24-37)

페이지 할당 정보를 저장하는 구조체. 모든 할당된 페이지에 대해 page_ext를 통해 연결된다.

struct page_owner {
    unsigned short order;           // 할당된 페이지 순서 (0이면 1페이지)
    short last_migrate_reason;      // 마지막 마이그레이션 이유
    gfp_t gfp_mask;                // 할당 시 GFP 플래그
    depot_stack_handle_t handle;   // 할당 시 스택 트레ース 핸들
    depot_stack_handle_t free_handle; // 해제 시 스택 트레ース 핸들
    u64 ts_nsec;                   // 할당 타임스탬프 (나노초)
    u64 free_ts_nsec;              // 해제 타임스탬프 (나노초)
    char comm[TASK_COMM_LEN];      // 할당한 프로세스 이름
    pid_t pid;                     // 할당한 프로세스 ID
    pid_t tgid;                    // 할당한 스레드 그룹 ID
    pid_t free_pid;                // 해제한 프로세스 ID
    pid_t free_tgid;               // 해제한 스레드 그룹 ID
};

struct stack (page_owner.c:39-42)

스택 레코드를 연결하는 구조체. stack_depot와 연결하여 스택 트레ース를 관리한다.

struct stack {
    struct stack_record *stack_record;  // stack_depot 레코드
    struct stack *next;                 // 다음 스택 노드
};

page_ext_operations (page_owner.c:143-148)

page_owner의 연산자 테이블. page_ext 프레임워크와의 통합을 정의한다.

struct page_ext_operations page_owner_ops = {
    .size = sizeof(struct page_owner),  // 저장할 메타데이터 크기
    .need = need_page_owner,            // page_owner 필요 여부 확인
    .init = init_page_owner,            // 초기화 함수
    .need_shared_flags = true,          // 공유 플래그 사용
};

핵심 함수

kmemleak_alloc (kmemleak.c:1090-1097)

새로운 커널 메모리 블록 할당을 kmemleak에 등록한다.

void __ref kmemleak_alloc(const void *ptr, size_t size, int min_count,
                          gfp_t gfp)
{
    pr_debug("%s(0x%px, %zu, %d)\n", __func__, ptr, size, min_count);
    // kmemleak 활성화 && 유효한 포인터인 경우 객체 생성
    if (kmemleak_enabled && ptr && !IS_ERR(ptr))
        create_object((unsigned long)ptr, size, min_count, gfp);
}

역할: kmalloc, kmem_cache_alloc 등에서 메모리 할당 시 호출되어 kmemleak_object를 생성하고 object_list에 추가한다.

kmemleak_free (kmemleak.c:1151-1157)

커널 메모리 블록 해제를 kmemleak에 등록한다.

void __ref kmemleak_free(const void *ptr)
{
    pr_debug("%s(0x%px)\n", __func__, ptr);
    // kmemleak 활성화 && 유효한 포인터인 경우 객체 삭제
    if (kmemleak_free_enabled && ptr && !IS_ERR(ptr))
        delete_object_full((unsigned long)ptr, 0);
}

역할: kfree, kmem_cache_free 등에서 메모리 해제 시 호출되어 해당 kmemleak_object를 삭제한다.

kmemleak_scan (kmemleak.c:1694-1791)

주기적으로 메모리를 스캔하여 누수를 탐지한다.

static void kmemleak_scan(void)
{
    struct kmemleak_object *object;
    struct zone *zone;
    int new_leaks = 0;

    jiffies_last_scan = jiffies;

    // 1단계: 모든 객체의 참조 수 초기화 (흰색으로 칠하기)
    rcu_read_lock();
    list_for_each_entry_rcu(object, &object_list, object_list) {
        raw_spin_lock_irq(&object->lock);
        // lowmem 외부 객체는 검은색으로 칠하기
        if ((object->flags & OBJECT_PHYS) &&
           !(object->flags & OBJECT_NO_SCAN)) {
            unsigned long phys = object->pointer;
            if (PHYS_PFN(phys) < min_low_pfn ||
                PHYS_PFN(phys + object->size) > max_low_pfn)
                __paint_it(object, KMEMLEAK_BLACK);
        }
        // 참조 수 초기화하고 회색 리스트에 추가
        object->count = 0;
        if (color_gray(object) && get_object(object))
            list_add_tail(&object->gray_list, &gray_list);
        raw_spin_unlock_irq(&object->lock);
    }
    rcu_read_unlock();

    // 2단계: per-CPU 섹션 스캔
    // 3단계: 모든 존의 페이지 스캔
    // 4단계: 태스크 스택 스캔
    // 5단계: 회색 리스트 순회하며 참조 수 카운트
}

역할: 회색-검은색 알고리즘을 사용하여 참조되지 않는 메모리 블록을 탐지한다.

kmemleak_write (kmemleak.c:2098-2170)

static ssize_t kmemleak_write(struct file *file, const char __user *user_buf,
                              size_t size, loff_t *ppos)
{
    if (strncmp(buf, "clear", 5) == 0) {
        if (kmemleak_enabled)
            kmemleak_clear();
        else
            __kmemleak_do_cleanup();
        goto out;
    }

    if (!kmemleak_enabled) {
        ret = -EPERM;
        goto out;
    }

    if (strncmp(buf, "off", 3) == 0)
        kmemleak_disable();
    else if (strncmp(buf, "stack=on", 8) == 0)
        kmemleak_stack_scan = 1;
    else if (strncmp(buf, "stack=off", 9) == 0)
        kmemleak_stack_scan = 0;
    else if (strncmp(buf, "scan=on", 7) == 0)
        start_scan_thread();
    else if (strncmp(buf, "scan=off", 8) == 0)
        stop_scan_thread();
    else if (strncmp(buf, "scan=", 5) == 0) {
        unsigned secs;
        unsigned long msecs;

        ret = kstrtouint(buf + 5, 0, &secs);
        if (ret < 0)
            goto out;

        msecs = secs * MSEC_PER_SEC;
        stop_scan_thread();
        if (msecs) {
            WRITE_ONCE(jiffies_scan_wait, msecs_to_jiffies(msecs));
            start_scan_thread();
        }
    } else if (strncmp(buf, "scan", 4) == 0)
        kmemleak_scan();

역할: /sys/kernel/debug/kmemleakscan=60, scan, clear, stack=on/off를 써서 스캔 주기와 수동 점검을 제어한다.

__set_page_owner (page_owner.c:335-346)

페이지 할당 정보를 page_owner에 기록한다.

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

역할: 페이지 할당 시 스택 트레ース와 함께 프로세스 정보를 기록한다.

__reset_page_owner (page_owner.c:298-333)

페이지 해제 정보를 page_owner에 기록한다.

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);

    // 해제 시 스택 트레ース 저장
    handle = save_stack(__GFP_NOWARN);
    __update_page_owner_free_handle(page, handle, order, current->pid,
                                    current->tgid, free_ts_nsec);

    // 할당 핸들의 카운트 감소 (early_handle 제외)
    if (alloc_handle != early_handle)
        dec_stack_record_count(alloc_handle, 1 << order);
}

역할: 페이지 해제 시 해제 스택 트레ース를 기록하고 할당 핸들의 참조 카운트를 감소시킨다.

read_page_owner (page_owner.c:660-755)

static ssize_t read_page_owner(struct file *file, char __user *buf, size_t count,
                               loff_t *ppos)
{
    if (!static_branch_unlikely(&page_owner_inited))
        return -EINVAL;

    if (*ppos == 0)
        pfn = min_low_pfn;
    else
        pfn = *ppos;

    for (; pfn < max_pfn; pfn++) {
        page = pfn_to_page(pfn);
        page_ext = page_ext_get(page);
        if (unlikely(!page_ext))
            continue;

        page_owner = get_page_owner(page_ext);

        if (!test_bit(PAGE_EXT_OWNER_ALLOCATED, &page_ext->flags))
            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;
}

역할: llseek로 PFN을 맞춘 뒤 page_extPAGE_EXT_OWNER_ALLOCATED를 확인하고 페이지 소유 정보를 출력한다.


호출 흐름

kmemleak 누수 탐지 흐름

kmemleak_alloc() 호출 (SLUB/SLAB 할당 시)
  └→ create_object()
       └→ __alloc_object() - 메모리 풀에서 kmemleak_object 할당
       └→ __link_object() - object_list와 object_tree_root에 추가

kmemleak_free() 호출 (kfree/kmem_cache_free 시)
  └→ delete_object_full()
       └→ __find_and_remove_object() - 객체 검색 및 제거
       └→ __delete_object() - OBJECT_ALLOCATED 플래그 제거
       └→ put_object() - RCU 콜백으로 해제 예약

kmemleak_scan() 주기적 호출 (스레드)
  └→ 모든 객체의 count 초기화 (흰색)
  └→ 회색 리스트에 추가
  └→ scan_gray_list() / scan_object() - 객체와 스캔 영역 분기 처리
  └→ scan_block() - 메모리 블록 스캔하여 참조 발견
  └→ object->count 증가 (검은색으로 칠하기)
  └→ count == 0 && min_count > 0인 객체 = 누수로 판정
  └→ kmemleak_report() - 누수 보고

page_owner 페이지 추적 흐름

__set_page_owner() 호출 (페이지 할당 시)
  └→ save_stack() - 스택 트레이스 저장 (stack_depot)
  └→ __update_page_owner_handle() - page_owner 메타데이터 업데이트
  └→ inc_stack_record_count() - 스택 레코드 카운트 증가

__reset_page_owner() 호출 (페이지 해제 시)
  └→ save_stack() - 해제 스택 트레이스 저장
  └→ __update_page_owner_free_handle() - 해제 핸들 업데이트
  └→ dec_stack_record_count() - 스택 레코드 카운트 감소

read_page_owner() / lseek_page_owner() 호출 (/sys/kernel/debug/page_owner 읽기)
  └→ 모든 페이지 순회하며 PAGE_EXT_OWNER 플래그 확인
  └→ PAGE_EXT_OWNER_ALLOCATED 플래그 확인 (할당된 페이지만)
  └→ print_page_owner() - 페이지 정보와 스택 트레이스 출력

조건별 비교

kmemleak vs page_owner 비교

특성kmemleakpage_owner
**추적 대상**커널 메모리 할당 (kmalloc, kmem_cache 등)모든 페이지 할당 (Buddy, SLUB 등)
**동작 방식**주기적 스캔으로 미참조 객체 탐지할당/해제 시 스택 트레이스 기록
**오버헤드**스캔 시 CPU 사용량 증가스택 트레이스 저장으로 메모리 사용 증가
**정확도**오탐 가능 (간접 참조 미인식)정확한 할당 경로 추적
**활성화**`echo scan > /sys/kernel/debug/kmemleak`부트 파라미터 `page_owner=on` 필요
**출력**누수 의심 객체 목록모든 할당된 페이지의 스택 트레이스

KASAN vs kmemleak 비교

특성KASANkmemleak
**목적**메모리 접근 오류 탐지메모리 누수 탐지
**탐지 항목**버퍼 오버플로, UAF, OOB미참조 메모리 블록
**동작 방식**메모리 접근 시 실시간 검증주기적 스캔
**오버헤드**높음 (접근마다 검증)중간 (스캔 시)
**활성화**커널 컨피그 `CONFIG_KASAN=y`커널 컨피그 `CONFIG_DEBUG_KMEMLEAK=y`

GFP 플래그별 kmemleak 동작

GFP 플래그kmemleak 동작
`GFP_KERNEL`객체 생성, 스캔 대상
`GFP_NOWAIT`객체 생성, 스캔 대상
`GFP_NOIO`객체 생성, 스캔 대상
`GFP_NOFS`객체 생성, 스캔 대상
`__GFP_NOLEAKTRACE`객체 생성 제외 (SLAB_NOLEAKTRACE)
`__GFP_ZERO`객체 생성, 0 초기화된 메모리

page_owner 출력 형식

필드설명
`order`할당된 페이지 순서 (0이면 4KB)
`mask`GFP 플래그 (16진수)
`pid`할당한 프로세스 ID
`tgid`할당한 스레드 그룹 ID
`ts`할당 타임스탬프 (나노초)
`PFN`페이지 프레임 번호
`type`마이그레이션 타입 (MOVABLE, UNMOVABLE 등)
`Flags`페이지 플래그

실전 사용 시나리오

시나리오 1: 커널 모듈 메모리 누수 탐지

# 1단계: kmemleak 스캔 간격 설정
echo scan=60 > /sys/kernel/debug/kmemleak

# 2단계: 의심스러운 모듈 로드/언로드
insmod suspect_module.ko
rmmod suspect_module.ko

# 3단계: kmemleak 스캔 트리거
echo scan > /sys/kernel/debug/kmemleak

# 4단계: 결과 확인
cat /sys/kernel/debug/kmemleak

시나리오 2: 특정 페이지 할당 추적

# 1단계: page_owner 활성화 (부트 파라미터 필요)
# GRUB: page_owner=on 추가 후 재부팅

# 2단계: 특정 PFN 할당 정보 확인
python3 - <<'PY'
path = "/sys/kernel/debug/page_owner"
pfn = 0x12345
with open(path, "rb") as f:
    f.seek(pfn)
    print(f.read(4096).decode("utf-8", "replace"))
PY

# 3단계: 할당 크기 기준 정렬
cat /sys/kernel/debug/page_owner | sort -k 3 -n -r | head -20

# 4단계: 특정 프로세스 할당 추적
cat /sys/kernel/debug/page_owner | grep "pid 1234"

시나리오 3: 스크립트를 이용한 자동 누수 탐지

#!/bin/bash
# kmemleak 자동 스캔 스크립트

echo "=== kmemleak 자동 스캔 시작 ==="

# 스캔 간격 설정
echo scan=300 > /sys/kernel/debug/kmemleak

# 스캔 실행
echo scan > /sys/kernel/debug/kmemleak

# 결과 저장
cat /sys/kernel/debug/kmemleak > /home/ryotta205/Chamber_linux/kmemleak_result_$(date +%Y%m%d_%H%M%S).txt

echo "=== 스캔 완료 ==="
echo "결과 파일: /home/ryotta205/Chamber_linux/kmemleak_result_$(date +%Y%m%d_%H%M%S).txt"

튜닝 가이드

kmemleak 튜닝 파라미터

파라미터기본값설명
`scan=60`600초자동 스캔 간격
`stack=on/off`1태스크 스택 스캔 활성화/비활성화
`scan`-즉시 스캔 트리거
`clear`-보고된 객체를 회색 처리하거나 내부 객체 정리

page_owner 튜닝 고려사항

  • page_owner=on 부트 파라미터 필수
  • 활성화 시 메모리 오버헤드 발생 (페이지당 64바이트)
  • 디버깅 목적으로만 사용 권장
  • production 환경에서는 page_owner=on 비활성화 유지

  • 관련 문서

  • 27-debug.md — 디버그 도구 (KASAN, KFENCE, Kmemleak)
  • 01-page_alloc.md — Buddy Allocator
  • 02-slab.md — SLUB 할당자
  • 41-page_owner.md — Page Owner
  • 42-percpu_mem.md — Per-CPU 메모리

  • SVG 다이어그램

    메모리 누수 추적 도구 계층 구조
    kmemleak 회색-검은색 알고리즘 흐름